From 95d5d8f1e03f3a1352ab87db312253690a1b95e5 Mon Sep 17 00:00:00 2001 From: rdmclin2 Date: Sat, 3 Aug 2024 22:25:44 +0800 Subject: [PATCH 1/2] =?UTF-8?q?:sparkles:=20chore:=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E8=88=9E=E8=B9=88=E5=88=97=E8=A1=A8=EF=BC=8C=E5=8E=BB=E6=8E=89?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/my/Redirect.tsx | 21 --- src/app/my/SideBar/MyList/Item.tsx | 47 ------ src/app/my/SideBar/MyList/index.tsx | 33 ---- src/app/my/SideBar/index.tsx | 42 ----- src/app/my/SideBar/type.ts | 5 - src/app/my/agent/layout.tsx | 9 - src/app/my/agent/page.tsx | 11 -- src/app/my/dance/layout.tsx | 9 - src/app/my/dance/page.tsx | 11 -- src/app/my/layout.desktop.tsx | 30 ---- src/app/my/page.tsx | 5 - src/app/my/style.ts | 12 -- src/features/Actions/PlayControl.tsx | 62 ------- src/features/AudioPlayer/Control/index.tsx | 33 ---- src/features/AudioPlayer/Control/style.ts | 23 --- src/features/AudioPlayer/Duration/index.tsx | 51 ------ src/features/AudioPlayer/Duration/style.ts | 14 -- src/features/AudioPlayer/Volume/index.tsx | 64 ------- src/features/AudioPlayer/Volume/style.ts | 18 -- src/features/AudioPlayer/index.tsx | 110 ------------ src/features/AudioPlayer/style.ts | 60 ------- src/features/ChatInfo/Operations/Item.tsx | 56 ------- src/features/ChatInfo/Operations/index.tsx | 88 ---------- src/features/ChatInfo/index.tsx | 6 +- src/features/ChatInfo/type.ts | 2 +- src/features/DanceList/Item/Actions.tsx | 60 +++++++ src/features/MarketInfo/index.tsx | 4 +- .../PlayList/Actions/ClearPlayList.tsx | 28 ---- src/features/PlayList/Actions/Dance.tsx | 19 --- src/features/PlayList/Item/index.tsx | 96 ----------- src/features/PlayList/Item/style.ts | 37 ---- src/features/PlayList/index.tsx | 75 --------- src/locales/default/common.ts | 5 +- src/panels/AgentPanel/Agent/List/index.tsx | 56 ------- src/panels/AgentPanel/Agent/index.tsx | 48 ------ src/panels/AgentPanel/index.tsx | 28 ---- src/panels/AgentPanel/style.ts | 12 -- src/panels/DancePanel/Dance/Card/index.tsx | 107 ------------ src/panels/DancePanel/Dance/Card/style.ts | 35 ---- src/panels/DancePanel/Dance/List/index.tsx | 56 ------- src/panels/DancePanel/Dance/index.tsx | 33 ---- src/panels/DancePanel/Dance/style.ts | 26 --- .../Market/Card/SubscribeButton.tsx | 45 ++--- .../Market/Card/UnSubscribeButton.tsx | 41 +++++ src/panels/DancePanel/Market/Card/index.tsx | 46 ++--- src/panels/DancePanel/index.tsx | 14 +- src/panels/index.tsx | 1 - src/store/dance/index.ts | 4 +- src/store/dance/selectors/playlist.ts | 24 --- src/store/dance/slices/dancelist.ts | 23 ++- src/store/dance/slices/playlist.ts | 158 ------------------ 51 files changed, 159 insertions(+), 1744 deletions(-) delete mode 100644 src/app/my/Redirect.tsx delete mode 100644 src/app/my/SideBar/MyList/Item.tsx delete mode 100644 src/app/my/SideBar/MyList/index.tsx delete mode 100644 src/app/my/SideBar/index.tsx delete mode 100644 src/app/my/SideBar/type.ts delete mode 100644 src/app/my/agent/layout.tsx delete mode 100644 src/app/my/agent/page.tsx delete mode 100644 src/app/my/dance/layout.tsx delete mode 100644 src/app/my/dance/page.tsx delete mode 100644 src/app/my/layout.desktop.tsx delete mode 100644 src/app/my/page.tsx delete mode 100644 src/app/my/style.ts delete mode 100644 src/features/Actions/PlayControl.tsx delete mode 100644 src/features/AudioPlayer/Control/index.tsx delete mode 100644 src/features/AudioPlayer/Control/style.ts delete mode 100644 src/features/AudioPlayer/Duration/index.tsx delete mode 100644 src/features/AudioPlayer/Duration/style.ts delete mode 100644 src/features/AudioPlayer/Volume/index.tsx delete mode 100644 src/features/AudioPlayer/Volume/style.ts delete mode 100644 src/features/AudioPlayer/index.tsx delete mode 100644 src/features/AudioPlayer/style.ts delete mode 100644 src/features/ChatInfo/Operations/Item.tsx delete mode 100644 src/features/ChatInfo/Operations/index.tsx create mode 100644 src/features/DanceList/Item/Actions.tsx delete mode 100644 src/features/PlayList/Actions/ClearPlayList.tsx delete mode 100644 src/features/PlayList/Actions/Dance.tsx delete mode 100644 src/features/PlayList/Item/index.tsx delete mode 100644 src/features/PlayList/Item/style.ts delete mode 100644 src/features/PlayList/index.tsx delete mode 100644 src/panels/AgentPanel/Agent/List/index.tsx delete mode 100644 src/panels/AgentPanel/Agent/index.tsx delete mode 100644 src/panels/AgentPanel/index.tsx delete mode 100644 src/panels/AgentPanel/style.ts delete mode 100644 src/panels/DancePanel/Dance/Card/index.tsx delete mode 100644 src/panels/DancePanel/Dance/Card/style.ts delete mode 100644 src/panels/DancePanel/Dance/List/index.tsx delete mode 100644 src/panels/DancePanel/Dance/index.tsx delete mode 100644 src/panels/DancePanel/Dance/style.ts create mode 100644 src/panels/DancePanel/Market/Card/UnSubscribeButton.tsx delete mode 100644 src/store/dance/selectors/playlist.ts delete mode 100644 src/store/dance/slices/playlist.ts diff --git a/src/app/my/Redirect.tsx b/src/app/my/Redirect.tsx deleted file mode 100644 index 4e094162..00000000 --- a/src/app/my/Redirect.tsx +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; - -const Redirect = () => { - const router = useRouter(); - - useEffect(() => { - // const hasData = localStorage.getItem('V_IDOL_WELCOME'); - // if (hasData) { - router.push('/my/agent'); - // } else { - // router.push('/welcome'); - // } - }, []); - - return null; -}; - -export default Redirect; diff --git a/src/app/my/SideBar/MyList/Item.tsx b/src/app/my/SideBar/MyList/Item.tsx deleted file mode 100644 index 6664891f..00000000 --- a/src/app/my/SideBar/MyList/Item.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Icon, List } from '@lobehub/ui'; -import { createStyles } from 'antd-style'; -import { type LucideIcon } from 'lucide-react'; -import { CSSProperties, ReactNode, memo } from 'react'; - -const { Item } = List; - -const useStyles = createStyles(({ css, token, responsive }) => ({ - container: css` - position: relative; - padding-top: 16px; - padding-bottom: 16px; - border-radius: ${token.borderRadius}px; - ${responsive.mobile} { - border-radius: 0; - } - `, - noHover: css` - pointer-events: none; - `, -})); - -export interface ItemProps { - active?: boolean; - className?: string; - hoverable?: boolean; - icon: LucideIcon; - label: ReactNode; - style?: CSSProperties; -} - -const SettingItem = memo( - ({ label, icon, hoverable = true, active = false, style, className }) => { - const { cx, styles } = useStyles(); - return ( - } - className={cx(styles.container, !hoverable && styles.noHover, className)} - style={style} - title={label as string} - > - ); - }, -); - -export default SettingItem; diff --git a/src/app/my/SideBar/MyList/index.tsx b/src/app/my/SideBar/MyList/index.tsx deleted file mode 100644 index 2761da48..00000000 --- a/src/app/my/SideBar/MyList/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Bot, Mic2 } from 'lucide-react'; -import Link from 'next/link'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { MyTabs } from '../type'; -import Item from './Item'; - -export interface MyListProps { - activeTab?: MyTabs; - mobile?: boolean; -} -const MyList = memo(({ activeTab, mobile }) => { - const { t } = useTranslation('my'); - - const items = [ - { icon: Bot, label: t('myRole'), value: MyTabs.Agent }, - { icon: Mic2, label: t('myDance'), value: MyTabs.Dance }, - ]; - - return items.map(({ value, icon, label }) => ( - - - - )); -}); - -export default MyList; diff --git a/src/app/my/SideBar/index.tsx b/src/app/my/SideBar/index.tsx deleted file mode 100644 index e99ea625..00000000 --- a/src/app/my/SideBar/index.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { createStyles, useResponsive } from 'antd-style'; -import { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Flexbox } from 'react-layout-kit'; - -import MyList, { MyListProps } from './MyList'; - -const useStyles = createStyles(({ token, css }) => ({ - container: css` - border-inline-end: 1px solid ${token.colorBorder}; - `, - logo: css` - fill: ${token.colorText}; - `, - top: css` - font-size: 20px; - font-weight: bold; - `, -})); - -const SideBar = memo(({ activeTab }) => { - const { styles } = useStyles(); - - const { mobile } = useResponsive(); - - const { t } = useTranslation('my'); - - return ( - - - {t('my')} - - - - - - ); -}); - -export default SideBar; diff --git a/src/app/my/SideBar/type.ts b/src/app/my/SideBar/type.ts deleted file mode 100644 index 0ee606d4..00000000 --- a/src/app/my/SideBar/type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum MyTabs { - Agent = 'agent', - Config = 'config', - Dance = 'dance', -} diff --git a/src/app/my/agent/layout.tsx b/src/app/my/agent/layout.tsx deleted file mode 100644 index eb28549c..00000000 --- a/src/app/my/agent/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import { MyTabs } from '@/app/my/SideBar/type'; - -import MyLayout from '../layout.desktop'; - -export default ({ children }: PropsWithChildren) => { - return {children}; -}; diff --git a/src/app/my/agent/page.tsx b/src/app/my/agent/page.tsx deleted file mode 100644 index 25945229..00000000 --- a/src/app/my/agent/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import React from 'react'; - -import MyAgent from '@/panels/AgentPanel/Agent'; - -const Market = () => { - return ; -}; - -export default Market; diff --git a/src/app/my/dance/layout.tsx b/src/app/my/dance/layout.tsx deleted file mode 100644 index 6573ed66..00000000 --- a/src/app/my/dance/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { PropsWithChildren } from 'react'; - -import { MyTabs } from '@/app/my/SideBar/type'; - -import MyLayout from '../layout.desktop'; - -export default ({ children }: PropsWithChildren) => { - return {children}; -}; diff --git a/src/app/my/dance/page.tsx b/src/app/my/dance/page.tsx deleted file mode 100644 index f69e0f46..00000000 --- a/src/app/my/dance/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import React from 'react'; - -import MyDance from '@/panels/DancePanel/Dance'; - -const Market = () => { - return ; -}; - -export default Market; diff --git a/src/app/my/layout.desktop.tsx b/src/app/my/layout.desktop.tsx deleted file mode 100644 index 533c80b5..00000000 --- a/src/app/my/layout.desktop.tsx +++ /dev/null @@ -1,30 +0,0 @@ -'use client'; - -import { ReactNode, memo } from 'react'; -import { Flexbox } from 'react-layout-kit'; - -import SideBar from '@/app/my/SideBar'; -import { MyTabs } from '@/app/my/SideBar/type'; -import AppLayout from '@/layout/AppLayout'; -import { HeaderNavKey } from '@/layout/type'; - -export interface LayoutProps { - activeTab: MyTabs; - children?: ReactNode; -} - -const LayoutDesktop = (props: LayoutProps) => { - const { children, activeTab } = props; - return ( - - - - - {children} - - - - ); -}; - -export default memo(LayoutDesktop); diff --git a/src/app/my/page.tsx b/src/app/my/page.tsx deleted file mode 100644 index 46fd2895..00000000 --- a/src/app/my/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import Redirect from '@/app/my/Redirect'; - -const Index = () => ; - -export default Index; diff --git a/src/app/my/style.ts b/src/app/my/style.ts deleted file mode 100644 index 3a1ed92b..00000000 --- a/src/app/my/style.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createStyles } from 'antd-style'; - -export const useStyles = createStyles(({ css }) => ({ - content: css` - display: flex; - flex-direction: row; - flex-grow: 1; - - width: 100%; - height: 100%; - `, -})); diff --git a/src/features/Actions/PlayControl.tsx b/src/features/Actions/PlayControl.tsx deleted file mode 100644 index b7ff35a6..00000000 --- a/src/features/Actions/PlayControl.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ActionIcon } from '@lobehub/ui'; -import { createStyles } from 'antd-style'; -import classNames from 'classnames'; -import { Music2 } from 'lucide-react'; -import React from 'react'; - -import { DESKTOP_HEADER_ICON_SIZE } from '@/constants/token'; -import { useDanceStore } from '@/store/dance'; - -const useStyles = createStyles(({ css }) => ({ - spin: css` - @keyframes rotate-animation { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } - - animation: rotate-animation 20s linear infinite; - `, -})); - -export { useStyles }; - -interface Props { - className?: string; - style?: React.CSSProperties; -} - -export default (props: Props) => { - const { style, className } = props; - const { styles } = useStyles(); - const [isPlaying, togglePlayPause, playlist] = useDanceStore((s) => [ - s.isPlaying, - s.togglePlayPause, - s.playlist, - ]); - - return playlist.length ? ( - - ) : null; - - // return ( - // - // ); -}; diff --git a/src/features/AudioPlayer/Control/index.tsx b/src/features/AudioPlayer/Control/index.tsx deleted file mode 100644 index 0aa35ab2..00000000 --- a/src/features/AudioPlayer/Control/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { PauseCircle, PlayCircle, SkipBack, SkipForward } from 'lucide-react'; - -import { DanceStore, useDanceStore } from '@/store/dance'; - -import { useStyles } from './style'; - -const controlSelectors = (s: DanceStore) => { - return { - isPlaying: s.isPlaying, - nextDance: s.nextDance, - prevDance: s.prevDance, - togglePlayPause: s.togglePlayPause, - }; -}; - -const Control = () => { - const { prevDance, nextDance, isPlaying, togglePlayPause } = useDanceStore(controlSelectors); - const { styles } = useStyles(); - - return ( -
- - {isPlaying ? ( - - ) : ( - - )} - -
- ); -}; - -export default Control; diff --git a/src/features/AudioPlayer/Control/style.ts b/src/features/AudioPlayer/Control/style.ts deleted file mode 100644 index 3f74c846..00000000 --- a/src/features/AudioPlayer/Control/style.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createStyles } from 'antd-style'; - -const useStyles = createStyles(({ token, css }) => ({ - back: css` - cursor: pointer; - margin-right: ${token.marginMD}px; - `, - control: css` - display: flex; - align-items: center; - justify-content: center; - `, - forward: css` - cursor: pointer; - margin-left: ${token.marginMD}px; - `, - playPause: css` - cursor: pointer; - color: ${token.colorPrimary}; - `, -})); - -export { useStyles }; diff --git a/src/features/AudioPlayer/Duration/index.tsx b/src/features/AudioPlayer/Duration/index.tsx deleted file mode 100644 index 734f8a06..00000000 --- a/src/features/AudioPlayer/Duration/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ConfigProvider, Slider } from 'antd'; -import { memo } from 'react'; - -import { useStyles } from './style'; - -interface DurationProps { - currentProgress: number; - duration: number; -} - -function formatDurationDisplay(duration: number) { - const min = Math.floor(duration / 60); - const sec = Math.floor(duration - min * 60); - return [min, sec].map((n) => (n < 10 ? '0' + n : n)).join(':'); -} - -const Duration = (props: DurationProps) => { - const { duration, currentProgress } = props; - const { styles } = useStyles(); - - return ( -
- - {formatDurationDisplay(currentProgress)} - - - - - - {formatDurationDisplay(duration)} - -
- ); -}; - -export default memo(Duration); diff --git a/src/features/AudioPlayer/Duration/style.ts b/src/features/AudioPlayer/Duration/style.ts deleted file mode 100644 index d053a052..00000000 --- a/src/features/AudioPlayer/Duration/style.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createStyles } from 'antd-style'; - -const useStyles = createStyles(({ css }) => ({ - counter: css` - font-size: 12px; - `, - duration: css` - display: flex; - align-items: center; - justify-content: center; - `, -})); - -export { useStyles }; diff --git a/src/features/AudioPlayer/Volume/index.tsx b/src/features/AudioPlayer/Volume/index.tsx deleted file mode 100644 index 674ae249..00000000 --- a/src/features/AudioPlayer/Volume/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ConfigProvider, Slider } from 'antd'; -import { Volume2, VolumeXIcon } from 'lucide-react'; -import React, { memo, useState } from 'react'; - -import { useStyles } from './style'; - -interface VolumeProps { - audioRef: React.RefObject; - setVolume: (volume: number) => void; - volume: number; -} - -const Volume = (props: VolumeProps) => { - const { volume, setVolume, audioRef } = props; - const [tempVolume, setTempVolume] = useState(0); - const { styles } = useStyles(); - - return ( -
- {volume === 0 ? ( - setVolume(tempVolume)} - size={20} - /> - ) : ( - { - setTempVolume(volume); - setVolume(0); - }} - size={20} - /> - )} - - { - if (!audioRef.current) return; - audioRef.current.volume = volume; - setVolume(volume); - }} - className={styles.slider} - step={0.05} - tooltip={{ open: false }} - value={volume} - /> - -
- ); -}; - -export default memo(Volume); diff --git a/src/features/AudioPlayer/Volume/style.ts b/src/features/AudioPlayer/Volume/style.ts deleted file mode 100644 index 4a56bb24..00000000 --- a/src/features/AudioPlayer/Volume/style.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createStyles } from 'antd-style'; - -const useStyles = createStyles(({ css }) => ({ - volume: css` - display: flex; - align-items: center; - `, - volumeIcon: css` - cursor: pointer; - margin-right: 8px; - `, - slider: css` - min-width: 92px; - margin: 0; - `, -})); - -export { useStyles }; diff --git a/src/features/AudioPlayer/index.tsx b/src/features/AudioPlayer/index.tsx deleted file mode 100644 index 4f39edc1..00000000 --- a/src/features/AudioPlayer/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { Avatar } from '@lobehub/ui'; -import { Progress, Typography } from 'antd'; -import classNames from 'classnames'; -import { isEqual } from 'lodash-es'; -import React, { memo, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import Control from '@/features/AudioPlayer/Control'; -import Duration from '@/features/AudioPlayer/Duration'; -import Volume from '@/features/AudioPlayer/Volume'; -import { useLoadAudio } from '@/hooks/useLoadAudio'; -import { useLoadDance } from '@/hooks/useLoadDance'; -import { useDanceStore } from '@/store/dance'; -import { playListSelectors } from '@/store/dance/selectors/playlist'; -import { useGlobalStore } from '@/store/global'; - -import { useStyles } from './style'; - -interface PlayerProps { - className?: string; - style?: React.CSSProperties; -} - -function Player(props: PlayerProps) { - const { style, className } = props; - const ref = useRef(null); - const [volume, setVolume] = useState(0.2); - const [duration, setDuration] = useState(0); - const [currentProgress, setCurrentProgress] = useState(0); - const { nextDance, currentPlay, isPlaying } = useDanceStore( - (s) => ({ - currentPlay: playListSelectors.currentPlay(s), - isPlaying: s.isPlaying, - nextDance: s.nextDance, - }), - isEqual, - ); - const viewer = useGlobalStore((s) => s.viewer); - - const { downloading: audioDownloading, percent: audioPercent, fetchAudioUrl } = useLoadAudio(); - const { downloading: danceDownloading, percent: dancePercent, fetchDanceBuffer } = useLoadDance(); - const { t } = useTranslation('common'); - - const { styles } = useStyles(); - - useEffect(() => { - if (isPlaying && currentPlay) { - const audioPromise = fetchAudioUrl(currentPlay.danceId, currentPlay.audio); - const dancePromise = fetchDanceBuffer(currentPlay.danceId, currentPlay.src); - Promise.all([audioPromise, dancePromise]).then((res) => { - if (!res) return; - const [audioUrl, danceBuffer] = res; - viewer.model?.dance(danceBuffer); - if (ref.current) ref.current.src = audioUrl; - if (ref.current) ref.current.play(); - }); - } else { - viewer.model?.stopDance(); - ref.current?.pause(); - if (ref.current) ref.current.currentTime = 0; - } - }, [isPlaying, currentPlay, viewer]); - - return ( -
-
- ); -} - -export default memo(Player); diff --git a/src/features/AudioPlayer/style.ts b/src/features/AudioPlayer/style.ts deleted file mode 100644 index fa0af6db..00000000 --- a/src/features/AudioPlayer/style.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createStyles } from 'antd-style'; - -const useStyles = createStyles(({ token, css }) => ({ - container: css` - width: 100%; - `, - content: css` - display: flex; - flex-direction: column; - flex-grow: 2; - margin-left: ${token.marginXS}px; - `, - controller: css` - display: flex; - align-items: center; - justify-content: space-between; - `, - info: css` - display: flex; - align-items: center; - `, - name: css` - justify-content: flex-start; - width: 108px; - font-size: ${token.fontSizeSM}px; - `, - player: css` - display: flex; - align-items: center; - `, - right: css` - display: flex; - gap: ${token.marginXS}px; - align-items: center; - `, - spin: css` - @keyframes rotate-animation { - from { - transform: rotate(0deg); - } - - to { - transform: rotate(360deg); - } - } - - animation: rotate-animation 20s linear infinite; - `, - progress: css` - position: absolute; - top: 0; - left: 0; - - background-color: rgba(${token.colorBgLayout}, 0.8); - backdrop-filter: saturate(180%) blur(10px); - border-radius: 100%; - `, -})); - -export { useStyles }; diff --git a/src/features/ChatInfo/Operations/Item.tsx b/src/features/ChatInfo/Operations/Item.tsx deleted file mode 100644 index bcbcf9b3..00000000 --- a/src/features/ChatInfo/Operations/Item.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Icon, List } from '@lobehub/ui'; -import { createStyles } from 'antd-style'; -import { type LucideIcon } from 'lucide-react'; -import { CSSProperties, ReactNode, memo } from 'react'; - -const { Item } = List; - -const useStyles = createStyles(({ css, token, responsive }) => ({ - container: css` - position: relative; - - align-items: center; - - padding-top: 16px; - padding-bottom: 16px; - - border-radius: ${token.borderRadius}px; - ${responsive.mobile} { - border-radius: 0; - } - `, - noHover: css` - pointer-events: none; - `, -})); - -export interface ItemProps { - actions?: ReactNode; - active?: boolean; - className?: string; - hoverable?: boolean; - icon: LucideIcon; - label: ReactNode; - onClick?: () => void; - style?: CSSProperties; -} - -const SettingItem = memo( - ({ label, icon, hoverable = true, actions, onClick, style, className }) => { - const { cx, styles } = useStyles(); - return ( - } - className={cx(styles.container, !hoverable && styles.noHover, className)} - style={style} - onClick={onClick} - title={label as string} - > - {actions} - - ); - }, -); - -export default SettingItem; diff --git a/src/features/ChatInfo/Operations/index.tsx b/src/features/ChatInfo/Operations/index.tsx deleted file mode 100644 index ea69f940..00000000 --- a/src/features/ChatInfo/Operations/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { ExclamationCircleFilled } from '@ant-design/icons'; -import { Modal } from 'antd'; -import { Eraser, Music } from 'lucide-react'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useGlobalStore } from '@/store/global'; -import { useSessionStore } from '@/store/session'; - -import Item from './Item'; - -const { confirm } = Modal; - -export interface MyListProps { - mobile?: boolean; -} - -const Operations = memo(({ mobile }) => { - const [openPanel] = useGlobalStore((s) => [s.openPanel]); - const [clearHistory] = useSessionStore((s) => [s.clearHistory]); - const { t } = useTranslation(['common', 'panel']); - const items = [ - // { - // icon: SquarePen, - // label: '新话题', - // key: 'new-topic', - // onClick: () => {}, - // }, - // { - // icon: History, - // label: '聊天历史记录', - // key: 'history', - // onClick: () => { - // // openPanel('role'); - // }, - // }, - // { - // icon: Settings2Icon, - // label: '对话设定', - // key: 'setting', - // onClick: () => { - // Modal.info({ title: '对话设定', content: '暂未开放' }); - // }, - // }, - // { - // icon: VideoIcon, - // key: 'video', - // label: '交互模式', - // actions: , - // }, - { - icon: Music, - key: 'music', - label: t('dance.musicAndDance', { ns: 'panel' }), - onClick: () => { - openPanel('dance'); - }, - }, - { - icon: Eraser, - label: t('actions.clearContext'), - key: 'context', - onClick: () => { - confirm({ - title: t('actions.clearHistoryTitle'), - icon: , - content: t('actions.clearHistoryTip'), - okText: t('confirm'), - cancelText: t('cancel'), - onOk() { - clearHistory(); - }, - onCancel() {}, - }); - }, - }, - ]; - - return ( - <> - {items.map(({ icon, label, onClick }) => ( - - ))} - - ); -}); - -export default Operations; diff --git a/src/features/ChatInfo/index.tsx b/src/features/ChatInfo/index.tsx index 748519ea..cc3769ec 100644 --- a/src/features/ChatInfo/index.tsx +++ b/src/features/ChatInfo/index.tsx @@ -8,8 +8,8 @@ import { Flexbox } from 'react-layout-kit'; import { CHAT_HEADER_HEIGHT, CHAT_INFO_MAX_WIDTH, CHAT_INFO_WIDTH } from '@/constants/token'; import ChatList from '@/features/ChatList'; +import DanceList from '@/features/DanceList'; import MotionList from '@/features/MotionList'; -import PlayList from '@/features/PlayList'; import PostureList from '@/features/PostureList'; import { useGlobalStore } from '@/store/global'; @@ -63,7 +63,7 @@ export default () => { }, { label: t('info.dance'), - key: Tab.PlayList, + key: Tab.DanceList, }, { label: t('info.motions'), @@ -81,7 +81,7 @@ export default () => { {' '} {tab === Tab.ChatList && } - {tab === Tab.PlayList && } + {tab === Tab.DanceList && } {tab === Tab.Motions && } {tab === Tab.Posture && } diff --git a/src/features/ChatInfo/type.ts b/src/features/ChatInfo/type.ts index e79f1604..c0dec34b 100644 --- a/src/features/ChatInfo/type.ts +++ b/src/features/ChatInfo/type.ts @@ -1,6 +1,6 @@ export enum Tab { ChatList = 'chats', + DanceList = 'dances', Motions = 'motions', - PlayList = 'playlist', Posture = 'posture', } diff --git a/src/features/DanceList/Item/Actions.tsx b/src/features/DanceList/Item/Actions.tsx new file mode 100644 index 00000000..fe9f5640 --- /dev/null +++ b/src/features/DanceList/Item/Actions.tsx @@ -0,0 +1,60 @@ +import { ActionIcon } from '@lobehub/ui'; +import { App, Dropdown, MenuProps } from 'antd'; +import { MoreVertical, Trash2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; + +import { useDanceStore } from '@/store/dance'; +import { Dance } from '@/types/dance'; + +interface ActionsProps { + danceItem: Dance; + setOpen: (open: boolean) => void; +} + +export default (props: ActionsProps) => { + const { danceItem, setOpen } = props; + const { modal } = App.useApp(); + const { t } = useTranslation(['common', 'panel']); + const [removeDanceItem] = useDanceStore((s) => [s.removeDanceItem]); + + const items: MenuProps['items'] = [ + { + danger: true, + icon: , + key: 'delete', + label: t('actions.unsubscribe', { ns: 'common' }), + onClick: ({ domEvent }) => { + domEvent.stopPropagation(); + modal.confirm({ + centered: true, + okButtonProps: { danger: true }, + async onOk() { + removeDanceItem(danceItem.danceId); + }, + okText: t('actions.unsubscribe', { ns: 'common' }), + cancelText: t('cancel'), + title: t('dance.cancelAddPlay', { musicName: danceItem?.name, ns: 'panel' }), + }); + }, + }, + ]; + return ( + { + domEvent.stopPropagation(); + }, + }} + onOpenChange={(open) => setOpen(open)} + trigger={['click']} + > + { + e.stopPropagation(); + }} + /> + + ); +}; diff --git a/src/features/MarketInfo/index.tsx b/src/features/MarketInfo/index.tsx index ccf0e725..afa8b50f 100644 --- a/src/features/MarketInfo/index.tsx +++ b/src/features/MarketInfo/index.tsx @@ -44,8 +44,8 @@ const Header = () => { if (isSubscribed) { actions.push( - , - , + , + , ); } else { actions.push( diff --git a/src/features/PlayList/Actions/ClearPlayList.tsx b/src/features/PlayList/Actions/ClearPlayList.tsx deleted file mode 100644 index 18c58ca4..00000000 --- a/src/features/PlayList/Actions/ClearPlayList.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ActionIcon } from '@lobehub/ui'; -import { Popconfirm } from 'antd'; -import { Trash2 } from 'lucide-react'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import { useDanceStore } from '@/store/dance'; - -export default () => { - const { t } = useTranslation('common'); - const clearPlayList = useDanceStore((s) => s.clearPlayList); - - return ( - { - clearPlayList(); - }} - title={t('actions.clearPlayList') + '?'} - > - - - ); -}; diff --git a/src/features/PlayList/Actions/Dance.tsx b/src/features/PlayList/Actions/Dance.tsx deleted file mode 100644 index 763c4c11..00000000 --- a/src/features/PlayList/Actions/Dance.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { ActionIcon } from '@lobehub/ui'; -import { PlusCircle } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; - -import { useGlobalStore } from '@/store/global'; - -export default () => { - const [openPanel] = useGlobalStore((s) => [s.openPanel]); - const { t } = useTranslation('panel'); - return ( - { - openPanel('dance'); - }} - title={t('dance.musicAndDance')} - /> - ); -}; diff --git a/src/features/PlayList/Item/index.tsx b/src/features/PlayList/Item/index.tsx deleted file mode 100644 index 257e4851..00000000 --- a/src/features/PlayList/Item/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { ActionIcon, Avatar, Icon } from '@lobehub/ui'; -import { useHover } from 'ahooks'; -import { Typography } from 'antd'; -import { Pause, Play, Trash2 } from 'lucide-react'; -import { memo, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; - -import ListItem from '@/components/ListItem'; -import { useDanceStore } from '@/store/dance'; -import { playListSelectors } from '@/store/dance/selectors/playlist'; - -import { useStyles } from './style'; - -const { Text } = Typography; - -interface PlayItemProps { - playItemId: string; -} - -const PlayItem = (props: PlayItemProps) => { - const { playItemId } = props; - const { styles } = useStyles(); - const { t } = useTranslation('common'); - - const { playItem, removePlayItem, currentPlayId, isPlaying, setIsPlaying, getDanceItemByPlayId } = - useDanceStore((s) => ({ - currentPlayId: s.currentPlayId, - isPlaying: s.isPlaying, - playItem: s.playItem, - playlist: s.playlist, - removePlayItem: s.removePlayItem, - setIsPlaying: s.setIsPlaying, - getDanceItemByPlayId: playListSelectors.getDanceItemByPlayId(s), - })); - - const isCurrentPlay = currentPlayId ? currentPlayId === playItemId : false; - const item = getDanceItemByPlayId(playItemId); - const hoverRef = useRef(null); - const isHovered = useHover(hoverRef); - - return ( - removePlayItem(playItemId)} - size="small" - />, - ]} - showAction={isHovered} - className={styles.listItem} - onDoubleClick={() => { - if (isCurrentPlay && isPlaying) { - setIsPlaying(false); - } else { - playItem(playItemId); - } - }} - avatar={ -
- - {isHovered || isCurrentPlay ? ( -
{ - if (isCurrentPlay && isPlaying) { - setIsPlaying(false); - } else { - playItem(playItemId); - } - }} - > - -
- ) : null} -
- } - title={item?.name} - description={ - - {item?.createAt} - - } - active={isCurrentPlay || isHovered} - /> - ); -}; - -export default memo(PlayItem); diff --git a/src/features/PlayList/Item/style.ts b/src/features/PlayList/Item/style.ts deleted file mode 100644 index 32db54e2..00000000 --- a/src/features/PlayList/Item/style.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { createStyles } from 'antd-style'; - -const useStyles = createStyles(({ css, token }) => ({ - mask: css` - cursor: pointer; - - position: absolute; - top: 0; - left: 0; - - width: 100%; - height: 100%; - - background-color: ${token.colorBgMask}; - `, - playIcon: css` - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - - font-size: 24px; - color: ${token.colorText}; - `, - listItem: css` - position: relative; - - height: 64px; - margin-block: 2px; - - font-size: ${token.fontSize}px; - - border-radius: ${token.borderRadius}px; - `, -})); - -export { useStyles }; diff --git a/src/features/PlayList/index.tsx b/src/features/PlayList/index.tsx deleted file mode 100644 index 318268c4..00000000 --- a/src/features/PlayList/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -/* eslint-disable @next/next/no-img-element */ -import { Empty, Space } from 'antd'; -import { createStyles } from 'antd-style'; -import classNames from 'classnames'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Center, Flexbox } from 'react-layout-kit'; - -import Header from '@/components/Header'; -import AudioPlayer from '@/features/AudioPlayer'; -import ClearPlayList from '@/features/PlayList/Actions/ClearPlayList'; -import Dance from '@/features/PlayList/Actions/Dance'; -import { useDanceStore } from '@/store/dance'; - -import PlayItem from './Item'; - -interface PlayListProps { - className?: string; - style?: React.CSSProperties; -} - -const useStyles = createStyles(({ css, token }) => ({ - container: css` - position: relative; - height: 100%; - background-color: rgba(255, 255, 255, 2%); - border-radius: ${token.borderRadius}px; - `, - list: css` - overflow-y: scroll; - width: 100%; - padding: 0 ${token.paddingSM}px; - `, - player: css` - width: 100%; - height: 64px; - padding: 0 ${token.paddingSM}px; - border-top: 1px solid ${token.colorBorder}; - `, -})); - -const PlayList = (props: PlayListProps) => { - const playlist = useDanceStore((s) => s.playlist); - const { t } = useTranslation(['panel', 'common']); - const { className, style } = props; - const { styles } = useStyles(); - - return ( - - -
- - - - } - /> - - {playlist.map((id) => { - return ; - })} - {playlist.length === 0 ? ( - - ) : null} - -
- -
- - ); -}; - -export default memo(PlayList); diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index 9327e9eb..6efffc7d 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -13,7 +13,7 @@ export default { selectModel: '请选择模型', selectInDanceList: '请从舞蹈列表中选取', inputStartChat: '请输入内容开始聊天', - playlist: '播放列表', + danceList: '舞蹈列表', history: '聊天记录', cancel: '取消', confirm: '确定', @@ -54,12 +54,11 @@ export default { sessionCreate: '创建聊天', danceMarket: '舞蹈市场', clearAll: '清空', - clearPlayList: '清空列表', - clearPlayListTip: '确认清除播放列表吗?', clearNow: '立即清除', clearContext: '清除上下文', clearTip: '操作无法撤销,清除后数据将无法恢复,请慎重操作', clearHistoryTip: '该操作不可逆,请谨慎操作', + clearSuccess: '清除成功', clearTitle: '确认清除所有会话消息?', clearHistoryTitle: '确定删除历史消息?', diff --git a/src/panels/AgentPanel/Agent/List/index.tsx b/src/panels/AgentPanel/Agent/List/index.tsx deleted file mode 100644 index 0976278a..00000000 --- a/src/panels/AgentPanel/Agent/List/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GradientButton } from '@lobehub/ui'; -import { useRouter } from 'next/navigation'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import GridList from '@/components/GridList'; -import { useAgentStore } from '@/store/agent'; - -interface AgentListProps { - className?: string; - style?: React.CSSProperties; -} - -const AgentList = (props: AgentListProps) => { - const { className, style } = props; - const router = useRouter(); - const { t } = useTranslation('common'); - - const [localAgentList, activateAgent, currentIdentifier] = useAgentStore((s) => [ - s.localAgentList, - s.activateAgent, - s.currentIdentifier, - ]); - - return ( - ({ - avatar: items.meta.avatar, - id: items.agentId, - name: items.meta.name, - }))} - onClick={(id) => { - activateAgent(id); - }} - isActivated={(id) => id === currentIdentifier} - empty={{ - actions: [ - { - router.push('/market'); - }} - > - + {t('actions.subscribeRole')} - , - ], - }} - /> - ); -}; - -export default memo(AgentList); diff --git a/src/panels/AgentPanel/Agent/index.tsx b/src/panels/AgentPanel/Agent/index.tsx deleted file mode 100644 index b5ad693d..00000000 --- a/src/panels/AgentPanel/Agent/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { createStyles } from 'antd-style'; -import classNames from 'classnames'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import TopBanner from '@/components/TopBanner'; - -import AgentList from './List'; - -const useStyles = createStyles(({ css }) => ({ - container: css` - position: relative; - - display: flex; - - width: 100%; - height: 100%; - min-height: 500px; - `, - content: css` - overflow-y: auto; - flex-grow: 1; - padding-right: 24px; - padding-left: 24px; - `, -})); - -interface AgentProps { - className?: string; - style?: React.CSSProperties; -} - -const Agent = (props: AgentProps) => { - const { styles } = useStyles(); - const { style, className } = props; - const { t } = useTranslation('chat'); - - return ( -
-
- - -
-
- ); -}; - -export default memo(Agent); diff --git a/src/panels/AgentPanel/index.tsx b/src/panels/AgentPanel/index.tsx deleted file mode 100644 index a4195ff6..00000000 --- a/src/panels/AgentPanel/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import PanelContainer from '@/panels/PanelContainer'; - -import Agent from './Agent'; -import { useStyles } from './style'; - -interface ControlPanelProps { - className?: string; - style?: React.CSSProperties; -} - -const ControlPanel = (props: ControlPanelProps) => { - const { style, className } = props; - const { styles } = useStyles(); - const { t } = useTranslation('chat'); - - return ( - -
- -
-
- ); -}; - -export default ControlPanel; diff --git a/src/panels/AgentPanel/style.ts b/src/panels/AgentPanel/style.ts deleted file mode 100644 index 3a1ed92b..00000000 --- a/src/panels/AgentPanel/style.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createStyles } from 'antd-style'; - -export const useStyles = createStyles(({ css }) => ({ - content: css` - display: flex; - flex-direction: row; - flex-grow: 1; - - width: 100%; - height: 100%; - `, -})); diff --git a/src/panels/DancePanel/Dance/Card/index.tsx b/src/panels/DancePanel/Dance/Card/index.tsx deleted file mode 100644 index 47b743e0..00000000 --- a/src/panels/DancePanel/Dance/Card/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { DraggablePanel } from '@lobehub/ui'; -import { Button, Popconfirm, message } from 'antd'; -import { createStyles } from 'antd-style'; -import React, { memo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; - -import DanceInfo from '@/components/DanceInfo'; -import { SIDEBAR_MAX_WIDTH, SIDEBAR_WIDTH } from '@/constants/token'; -import { danceListSelectors, useDanceStore } from '@/store/dance'; - -const useStyles = createStyles(({ css, token }) => ({ - content: css` - display: flex; - flex-direction: column; - height: 100% !important; - `, - header: css` - border-bottom: 1px solid ${token.colorBorder}; - `, -})); - -// eslint-disable-next-line react/display-name -const SideBar = memo(() => { - const { styles } = useStyles(); - const [tempId, setTempId] = useState(''); - const [ - showDanceSidebar, - activateDance, - deactivateDance, - addAndPlayItem, - addToPlayList, - removeDanceItem, - ] = useDanceStore((s) => [ - danceListSelectors.showSideBar(s), - s.activateDance, - s.deactivateDance, - s.addAndPlayItem, - s.addToPlayList, - s.removeDanceItem, - ]); - - const currentDance = useDanceStore((s) => danceListSelectors.currentDanceItem(s)); - const { t } = useTranslation(['panel', 'common']); - return ( - { - if (!show) { - setTempId(useDanceStore.getState().currentIdentifier); - deactivateDance(); - } else if (tempId) { - activateDance(tempId); - } - }} - placement={'right'} - > - { - if (currentDance) { - addAndPlayItem(currentDance.danceId); - } - }} - type={'primary'} - > - {t('dance.play')} - , - , - { - if (currentDance) { - removeDanceItem(currentDance.danceId); - } - }} - title={t('dance.cancelSubscribed') + '?'} - > - - , - ]} - dance={currentDance} - /> - - ); -}); - -export default SideBar; diff --git a/src/panels/DancePanel/Dance/Card/style.ts b/src/panels/DancePanel/Dance/Card/style.ts deleted file mode 100644 index 521d3447..00000000 --- a/src/panels/DancePanel/Dance/Card/style.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createStyles } from 'antd-style'; - -export const useStyles = createStyles(({ css, token }) => ({ - container: css` - position: relative; - padding: 16px; - border-bottom: 1px solid ${token.colorBorderSecondary}; - `, - date: css` - font-size: 12px; - color: ${token.colorTextDescription}; - `, - desc: css` - color: ${token.colorTextDescription}; - text-align: center; - `, - footer: css` - overflow-y: auto; - height: 300px; - padding: 8px; - white-space: break-spaces; - `, - - title: css` - overflow: hidden; - - width: 160px; - - font-size: 20px; - font-weight: 600; - text-align: center; - text-overflow: ellipsis; - white-space: nowrap; - `, -})); diff --git a/src/panels/DancePanel/Dance/List/index.tsx b/src/panels/DancePanel/Dance/List/index.tsx deleted file mode 100644 index 08a8a616..00000000 --- a/src/panels/DancePanel/Dance/List/index.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { GradientButton } from '@lobehub/ui'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -import GridList from '@/components/GridList'; -import { useDanceStore } from '@/store/dance'; - -interface DanceListProps { - className?: string; - setTab?: (tab: string) => void; - style?: React.CSSProperties; -} - -const DanceList = (props: DanceListProps) => { - const { className, style, setTab } = props; - const [danceList, activateDance, currentIdentifier] = useDanceStore((s) => [ - s.danceList, - s.activateDance, - s.currentIdentifier, - ]); - const { t } = useTranslation('common'); - - return ( - ({ - avatar: items.thumb, - id: items.danceId, - name: items.name, - }))} - onClick={(id) => { - activateDance(id); - }} - isActivated={(id) => id === currentIdentifier} - empty={{ - actions: [ - { - if (setTab) { - setTab('market'); - } - }} - > - + {t('actions.subscribeDance')} - , - ], - }} - /> - ); -}; - -export default DanceList; diff --git a/src/panels/DancePanel/Dance/index.tsx b/src/panels/DancePanel/Dance/index.tsx deleted file mode 100644 index 30f6ac76..00000000 --- a/src/panels/DancePanel/Dance/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import classNames from 'classnames'; -import React, { memo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import TopBanner from '@/components/TopBanner'; - -import DanceCard from './Card'; -import DanceList from './List'; -import { useStyles } from './style'; - -interface DanceProps { - className?: string; - setTab?: (tab: string) => void; - style?: React.CSSProperties; -} - -const Dance = (props: DanceProps) => { - const { style, className, setTab } = props; - const { styles } = useStyles(); - const { t } = useTranslation('chat'); - - return ( -
-
- - -
- -
- ); -}; - -export default memo(Dance); diff --git a/src/panels/DancePanel/Dance/style.ts b/src/panels/DancePanel/Dance/style.ts deleted file mode 100644 index 56c71cb2..00000000 --- a/src/panels/DancePanel/Dance/style.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createStyles } from 'antd-style'; - -export const useStyles = createStyles(({ css }) => ({ - container: css` - position: relative; - - display: flex; - flex-direction: row; - flex-grow: 1; - - width: 100%; - height: 100%; - `, - content: css` - overflow-y: auto; - flex-grow: 1; - padding-right: 24px; - padding-left: 24px; - `, - title: css` - z-index: 2; - margin-top: 24px; - font-size: 36px; - font-weight: 800; - `, -})); diff --git a/src/panels/DancePanel/Market/Card/SubscribeButton.tsx b/src/panels/DancePanel/Market/Card/SubscribeButton.tsx index 32ff2373..0da73331 100644 --- a/src/panels/DancePanel/Market/Card/SubscribeButton.tsx +++ b/src/panels/DancePanel/Market/Card/SubscribeButton.tsx @@ -1,10 +1,9 @@ -import { Button, Progress, message } from 'antd'; +import { Button, Progress } from 'antd'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; import { useDownloadDance } from '@/hooks/useDownloadDance'; -import { danceListSelectors, useDanceStore } from '@/store/dance'; import { Dance } from '@/types/dance'; interface SubscribeButtonProps { @@ -12,15 +11,8 @@ interface SubscribeButtonProps { } const SubscribeButton = (props: SubscribeButtonProps) => { - const [removeDanceItem, subscribed] = useDanceStore((s) => [ - s.removeDanceItem, - danceListSelectors.subscribed(s), - ]); - const { dance } = props; - const isSubscribed = subscribed(dance.danceId); - const { downloading, percent, fetchDanceData } = useDownloadDance(); const { t } = useTranslation('common'); @@ -29,31 +21,20 @@ const SubscribeButton = (props: SubscribeButtonProps) => { ); }; diff --git a/src/panels/DancePanel/Market/Card/UnSubscribeButton.tsx b/src/panels/DancePanel/Market/Card/UnSubscribeButton.tsx new file mode 100644 index 00000000..a8390fb7 --- /dev/null +++ b/src/panels/DancePanel/Market/Card/UnSubscribeButton.tsx @@ -0,0 +1,41 @@ +import { Button, Popconfirm, message } from 'antd'; +import React, { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useDanceStore } from '@/store/dance'; +import { Dance } from '@/types/dance'; + +interface UnSubscribeButtonProps { + dance: Dance; +} + +const UnSubscribe = memo((props: UnSubscribeButtonProps) => { + const { dance } = props; + const removeDanceItem = useDanceStore((s) => s.removeDanceItem); + const [loading, setLoading] = useState(false); + + const { t } = useTranslation(['common', 'panel']); + + return ( + { + if (dance) { + setLoading(true); + await removeDanceItem(dance.danceId); + message.success(t('actions.unsubscribeSuccess')); + setLoading(false); + } + }} + title={t('dance.cancelSubscribed', { ns: 'panel' }) + '?'} + > + + + ); +}); + +export default UnSubscribe; diff --git a/src/panels/DancePanel/Market/Card/index.tsx b/src/panels/DancePanel/Market/Card/index.tsx index de70dc3f..7c77d356 100644 --- a/src/panels/DancePanel/Market/Card/index.tsx +++ b/src/panels/DancePanel/Market/Card/index.tsx @@ -1,13 +1,12 @@ import { DraggablePanel } from '@lobehub/ui'; -import { Button, message } from 'antd'; import { createStyles } from 'antd-style'; import React, { memo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; import Author from '@/components/Author'; import DanceInfo from '@/components/DanceInfo'; import { SIDEBAR_MAX_WIDTH, SIDEBAR_WIDTH } from '@/constants/token'; import SubscribeButton from '@/panels/DancePanel/Market/Card/SubscribeButton'; +import UnSubscribeButton from '@/panels/DancePanel/Market/Card/UnSubscribeButton'; import { danceListSelectors, useDanceStore } from '@/store/dance'; import { marketStoreSelectors, useMarketStore } from '@/store/market'; @@ -34,45 +33,24 @@ const Header = () => { ], ); - const [subscribed, addAndPlayItem, addToPlayList] = useDanceStore((s) => [ - danceListSelectors.subscribed(s), - s.addAndPlayItem, - s.addToPlayList, - ]); - const { t } = useTranslation('panel'); + const [subscribed] = useDanceStore((s) => [danceListSelectors.subscribed(s)]); const actions = []; if (currentDanceItem) { const isSubscribed = subscribed(currentDanceItem.danceId); if (isSubscribed) { - actions.push([ - , - , - ]); + actions.push( + , + ); + } else { + actions.push( + , + ); } - - actions.push(); } return ( diff --git a/src/panels/DancePanel/index.tsx b/src/panels/DancePanel/index.tsx index 224241e5..887fbf3c 100644 --- a/src/panels/DancePanel/index.tsx +++ b/src/panels/DancePanel/index.tsx @@ -1,13 +1,10 @@ 'use client'; -import { AppstoreOutlined, BarsOutlined } from '@ant-design/icons'; -import { Segmented } from 'antd'; -import React, { useState } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import PanelContainer from '@/panels/PanelContainer'; -import Dance from './Dance'; import Market from './Market'; interface DancePanelProps { @@ -17,23 +14,16 @@ interface DancePanelProps { const DancePanel = (props: DancePanelProps) => { const { style, className } = props; - const [tab, setTab] = useState('dance'); const { t } = useTranslation(['common', 'panel']); - const options = [ - { value: 'dance', label: t('actions.subscribed'), icon: }, - { value: 'market', label: t('actions.market'), icon: }, - ]; - return ( } > - {tab === 'dance' ? : } + ); }; diff --git a/src/panels/index.tsx b/src/panels/index.tsx index 0a7852fd..ced66cf7 100644 --- a/src/panels/index.tsx +++ b/src/panels/index.tsx @@ -1,6 +1,5 @@ 'use client'; -export { default as AgentPanel } from './AgentPanel'; export { default as DancePanel } from './DancePanel'; export { default as MarketPanel } from './MarketPanel'; export { default as RolePanel } from './RolePanel'; diff --git a/src/store/dance/index.ts b/src/store/dance/index.ts index 2a5f8701..26a986fb 100644 --- a/src/store/dance/index.ts +++ b/src/store/dance/index.ts @@ -6,15 +6,13 @@ import { createWithEqualityFn } from 'zustand/traditional'; import storage from '@/utils/storage'; import { DanceListStore, createDanceStore } from './slices/dancelist'; -import { PlayListStore, createPlayListStore } from './slices/playlist'; -export type DanceStore = DanceListStore & PlayListStore; +export type DanceStore = DanceListStore; export const DANCE_STORAGE_KEY = 'vidol-chat-dance-storage'; const createStore: StateCreator = (...parameters) => ({ ...createDanceStore(...parameters), - ...createPlayListStore(...parameters), }); const persistOptions: PersistOptions = { diff --git a/src/store/dance/selectors/playlist.ts b/src/store/dance/selectors/playlist.ts deleted file mode 100644 index 38b22721..00000000 --- a/src/store/dance/selectors/playlist.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Dance } from '@/types/dance'; - -import { DanceStore } from '../index'; - -const currentPlay = (s: DanceStore): Dance | undefined => { - const { currentPlayId, danceList } = s; - const currentDance = danceList.find((item) => item.danceId === currentPlayId); - if (!currentDance) return undefined; - - return currentDance; -}; - -const getDanceItemByPlayId = (s: DanceStore) => (playId: string) => { - const { danceList } = s; - const currentDance = danceList.find((item) => item.danceId === playId); - if (!currentDance) return undefined; - - return currentDance; -}; - -export const playListSelectors = { - currentPlay, - getDanceItemByPlayId, -}; diff --git a/src/store/dance/slices/dancelist.ts b/src/store/dance/slices/dancelist.ts index c7d77ae6..48965a6b 100644 --- a/src/store/dance/slices/dancelist.ts +++ b/src/store/dance/slices/dancelist.ts @@ -12,13 +12,26 @@ export interface DanceListStore { activateDance: (identifier: string) => void; addDanceItem: (dance: Dance) => void; currentIdentifier: string; + /** + * current playing dance. + */ + currentPlayId: string; danceList: Dance[]; danceLoading: boolean; deactivateDance: () => void; fetchDanceIndex: () => void; isPlaying: boolean; + /** + * Play a dance. + * @param dance + */ + playItem: (danceId: string) => void; removeDanceItem: (danceId: string) => Promise; setIsPlaying: (isPlaying: boolean) => void; + /** + * Toggle play or pause. + */ + togglePlayPause: () => void; } export const createDanceStore: StateCreator< @@ -32,11 +45,17 @@ export const createDanceStore: StateCreator< set({ currentIdentifier: identifier }); }, currentIdentifier: '', + currentPlayId: '', danceList: [], + isPlaying: false, danceLoading: false, deactivateDance: () => { set({ currentIdentifier: undefined }); }, + togglePlayPause: () => { + if (!get().currentPlayId) return; + set({ isPlaying: !get().isPlaying }); + }, fetchDanceIndex: async () => { set({ danceLoading: true }); try { @@ -49,7 +68,6 @@ export const createDanceStore: StateCreator< set({ danceLoading: false }); } }, - isPlaying: false, setIsPlaying: (isPlaying) => { set({ isPlaying }); }, @@ -65,6 +83,9 @@ export const createDanceStore: StateCreator< }); set({ danceList: newList }); }, + playItem: (danceId) => { + set({ currentPlayId: danceId, isPlaying: true }); + }, removeDanceItem: async (danceId) => { const { danceList } = get(); const newList = produce(danceList, (draft) => { diff --git a/src/store/dance/slices/playlist.ts b/src/store/dance/slices/playlist.ts deleted file mode 100644 index 6db855d3..00000000 --- a/src/store/dance/slices/playlist.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { produce } from 'immer'; -import { StateCreator } from 'zustand/vanilla'; - -import { DanceStore } from '@/store/dance'; - -export interface PlayListStore { - /** - * Add a dance to the playlist and play it. - * @param dance - */ - addAndPlayItem: (danceId: string) => void; - - /** - * Add a dance to the playlist. - */ - addToPlayList: (danceId: string) => void; - /** - * Clear the playlist. - */ - clearPlayList: () => void; - /** - * current playing dance. - */ - currentPlayId: string; - /** - * next dance. - */ - nextDance: () => void; - /** - * Play a dance. - * @param dance - */ - playItem: (danceId: string) => void; - /** - * Playlist. - */ - playlist: string[]; - /** - * previous dance. - */ - prevDance: () => void; - /** - * Remove a dance from the playlist. - * @param dance - */ - removePlayItem: (danceId: string) => void; - /** - * Set the playlist. - * @param playlist - */ - setPlayList: (playlist: string[]) => void; - /** - * Toggle play or pause. - */ - togglePlayPause: () => void; -} - -export const createPlayListStore: StateCreator< - DanceStore, - [['zustand/devtools', never]], - [], - PlayListStore -> = (set, get) => { - return { - /** - * Add a dance to the playlist and play it. add to the first. - * @param danceId - */ - addAndPlayItem: (danceId) => { - const { playlist, playItem } = get(); - - const nextPlayList = produce(playlist, (draftState) => { - const index = draftState.indexOf(danceId); - if (index === -1) { - draftState.unshift(danceId); - } else { - draftState.splice(index, 1); - draftState.unshift(danceId); - } - }); - - set({ playlist: nextPlayList }); - - playItem(danceId); - }, - /** - * Add a dance to the playlist. add to the last. - * @param danceId - */ - addToPlayList: (danceId) => { - const { playlist } = get(); - - const nextPlayList = produce(playlist, (draftState) => { - const index = draftState.indexOf(danceId); - if (index === -1) { - draftState.push(danceId); - } - }); - - set({ playlist: nextPlayList }); - }, - clearPlayList: () => { - set({ currentPlayId: undefined, isPlaying: false, playlist: [] }); - }, - - currentPlayId: '', - isPlaying: false, - - nextDance: () => { - const { currentPlayId, playlist, playItem } = get(); - if (currentPlayId && playlist.length > 0) { - const currentPlayIndex = playlist.indexOf(currentPlayId); - if (currentPlayIndex < playlist.length - 1) { - playItem(playlist[currentPlayIndex + 1]); - } else { - playItem(playlist[0]); - } - } - }, - - playItem: (danceId) => { - set({ currentPlayId: danceId, isPlaying: true }); - }, - playlist: [], - prevDance: () => { - const { currentPlayId, playlist, playItem } = get(); - if (currentPlayId && playlist.length > 0) { - const currentPlayIndex = playlist.indexOf(currentPlayId); - if (currentPlayIndex > 0) { - playItem(playlist[currentPlayIndex - 1]); - } else { - const dance = playlist.at(-1); - if (dance) playItem(dance); - } - } - }, - removePlayItem: (danceId) => { - const { playlist } = get(); - const nextPlayList = produce(playlist, (draftState) => { - const currentPlayIndex = playlist.indexOf(danceId); - draftState.splice(currentPlayIndex, 1); - }); - - if (nextPlayList.length === 0) { - set({ currentPlayId: undefined, isPlaying: false, playlist: nextPlayList }); - } else { - set({ playlist: nextPlayList }); - } - }, - setPlayList: (playlist) => { - set({ playlist: playlist }); - }, - togglePlayPause: () => { - if (!get().currentPlayId) return; - set({ isPlaying: !get().isPlaying }); - }, - }; -}; From 054d94a365557f0d2e280543e8f3ab180302f5f5 Mon Sep 17 00:00:00 2001 From: rdmclin2 Date: Sun, 4 Aug 2024 00:19:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:=20feat:=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=87=8D=E7=BD=AE=E8=88=9E=E8=B9=88=E5=8A=A8=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Actions/Discord.tsx | 2 +- src/features/AgentViewer/ToolBar/index.tsx | 14 +++- src/features/DanceList/Item/index.tsx | 85 ++++++++++++++++++++++ src/features/DanceList/Item/style.ts | 42 +++++++++++ src/features/DanceList/index.tsx | 76 +++++++++++++++++++ src/features/audioPlayer/audioPlayer.ts | 32 ++++++++ src/features/vrmViewer/model.ts | 82 +++++++++++++-------- src/hooks/useDownloadDance.tsx | 6 +- src/hooks/useLoadAudio.tsx | 21 ++++-- src/locales/default/features.ts | 1 + src/store/dance/slices/dancelist.ts | 29 ++------ 11 files changed, 320 insertions(+), 70 deletions(-) create mode 100644 src/features/DanceList/Item/index.tsx create mode 100644 src/features/DanceList/Item/style.ts create mode 100644 src/features/DanceList/index.tsx create mode 100644 src/features/audioPlayer/audioPlayer.ts diff --git a/src/features/Actions/Discord.tsx b/src/features/Actions/Discord.tsx index 2467d6eb..a8626ec7 100644 --- a/src/features/Actions/Discord.tsx +++ b/src/features/Actions/Discord.tsx @@ -29,7 +29,7 @@ export default () => { icon={SiDiscord} key="discord" title={t('support')} - onClick={() => window.open('https://discord.gg/8Gq83ZCK9u', '_blank')} + onClick={() => window.open('https://discord.gg/AYFPHvv2jT', '_blank')} style={{ border: `1px solid ${theme.colorFillSecondary}` }} /> ); diff --git a/src/features/AgentViewer/ToolBar/index.tsx b/src/features/AgentViewer/ToolBar/index.tsx index f378ec9d..000ab881 100644 --- a/src/features/AgentViewer/ToolBar/index.tsx +++ b/src/features/AgentViewer/ToolBar/index.tsx @@ -1,6 +1,6 @@ import { ActionIconGroup } from '@lobehub/ui'; import dayjs from 'dayjs'; -import { Aperture, Axis3D, Grid3x3, Orbit, RotateCw, SwitchCamera } from 'lucide-react'; +import { Aperture, Axis3D, Grid3x3, Orbit, Power, SwitchCamera } from 'lucide-react'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -41,7 +41,12 @@ const ToolBar = (props: ToolBarProps) => { dropdownMenu={dropdownMenu} items={[ { - icon: RotateCw, + icon: Power, + key: 'power', + label: t('toolBar.resetToIdle'), + }, + { + icon: SwitchCamera, key: 'resetCamera', label: t('toolBar.resetCamera'), }, @@ -63,6 +68,10 @@ const ToolBar = (props: ToolBarProps) => { break; } + case 'power': { + viewer.model?.resetToIdle(); + break; + } case 'screenShot': { const canvas = document.querySelector('#canvas') as HTMLCanvasElement; const imageType = 'png'; @@ -103,6 +112,7 @@ const ToolBar = (props: ToolBarProps) => { }} style={style} type={'block'} + size={'normal'} /> ); }; diff --git a/src/features/DanceList/Item/index.tsx b/src/features/DanceList/Item/index.tsx new file mode 100644 index 00000000..e703e533 --- /dev/null +++ b/src/features/DanceList/Item/index.tsx @@ -0,0 +1,85 @@ +import { Avatar } from '@lobehub/ui'; +import { useHover } from 'ahooks'; +import { Progress, Typography } from 'antd'; +import React, { memo, useRef, useState } from 'react'; + +import ListItem from '@/components/ListItem'; +import Actions from '@/features/DanceList/Item/Actions'; +import { useLoadAudio } from '@/hooks/useLoadAudio'; +import { useLoadDance } from '@/hooks/useLoadDance'; +import { useDanceStore } from '@/store/dance'; +import { useGlobalStore } from '@/store/global'; +import { Dance } from '@/types/dance'; + +import { useStyles } from './style'; + +const { Text } = Typography; + +interface DanceItemProps { + danceItem: Dance; +} + +const DanceItem = (props: DanceItemProps) => { + const { danceItem } = props; + const [open, setOpen] = useState(false); + + const { styles } = useStyles(); + const [currentPlayId, setCurrentPlayId] = useDanceStore((s) => [ + s.currentPlayId, + s.setCurrentPlayId, + ]); + + const isCurrentPlay = currentPlayId ? currentPlayId === danceItem.danceId : false; + const hoverRef = useRef(null); + const isHovered = useHover(hoverRef); + + const { downloading: audioDownloading, percent: audioPercent, fetchAudioBuffer } = useLoadAudio(); + const { downloading: danceDownloading, percent: dancePercent, fetchDanceBuffer } = useLoadDance(); + const viewer = useGlobalStore((s) => s.viewer); + + const handlePlayPause = () => { + const audioPromise = fetchAudioBuffer(danceItem.danceId, danceItem.audio); + const dancePromise = fetchDanceBuffer(danceItem.danceId, danceItem.src); + Promise.all([dancePromise, audioPromise]).then((res) => { + if (!res) return; + const [danceBuffer, audioBuffer] = res; + viewer.model?.dance(danceBuffer, audioBuffer); + }); + setCurrentPlayId(danceItem.danceId); + }; + + return ( + + ) : null, + , + ]} + onClick={handlePlayPause} + className={styles.listItem} + avatar={ +
+ +
+ } + title={danceItem?.name} + description={ + + {danceItem?.author} + + } + active={isCurrentPlay || isHovered} + /> + ); +}; + +export default memo(DanceItem); diff --git a/src/features/DanceList/Item/style.ts b/src/features/DanceList/Item/style.ts new file mode 100644 index 00000000..29422950 --- /dev/null +++ b/src/features/DanceList/Item/style.ts @@ -0,0 +1,42 @@ +import { createStyles } from 'antd-style'; + +const useStyles = createStyles(({ css, token }) => ({ + mask: css` + cursor: pointer; + + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + + background-color: ${token.colorBgMask}; + `, + progress: css` + background-color: rgba(${token.colorBgLayout}, 0.8); + backdrop-filter: saturate(180%) blur(10px); + border-radius: 100%; + `, + playIcon: css` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + font-size: 24px; + color: ${token.colorText}; + `, + listItem: css` + position: relative; + + height: 64px; + margin-block: 2px; + + font-size: ${token.fontSize}px; + + border-radius: ${token.borderRadius}px; + `, +})); + +export { useStyles }; diff --git a/src/features/DanceList/index.tsx b/src/features/DanceList/index.tsx new file mode 100644 index 00000000..b2a05f5d --- /dev/null +++ b/src/features/DanceList/index.tsx @@ -0,0 +1,76 @@ +import { ActionIcon } from '@lobehub/ui'; +import { Empty } from 'antd'; +import { createStyles } from 'antd-style'; +import classNames from 'classnames'; +import { PlusCircle } from 'lucide-react'; +import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import Header from '@/components/Header'; +import { useDanceStore } from '@/store/dance'; +import { useGlobalStore } from '@/store/global'; + +import DanceItem from './Item'; + +interface PlayListProps { + className?: string; + style?: React.CSSProperties; +} + +const useStyles = createStyles(({ css, token }) => ({ + container: css` + position: relative; + height: 100%; + background-color: rgba(255, 255, 255, 2%); + border-radius: ${token.borderRadius}px; + `, + list: css` + overflow-y: scroll; + width: 100%; + padding: 0 ${token.paddingSM}px; + `, + player: css` + width: 100%; + height: 64px; + padding: 0 ${token.paddingSM}px; + border-top: 1px solid ${token.colorBorder}; + `, +})); + +const DanceList = (props: PlayListProps) => { + const danceList = useDanceStore((s) => s.danceList); + const [openPanel] = useGlobalStore((s) => [s.openPanel]); + + const { t } = useTranslation(['panel', 'common']); + const { className, style } = props; + const { styles } = useStyles(); + + return ( + + +
{ + openPanel('dance'); + }} + title={t('dance.musicAndDance')} + /> + } + /> + + {danceList.map((item) => { + return ; + })} + {danceList.length === 0 ? ( + + ) : null} + + + ); +}; + +export default memo(DanceList); diff --git a/src/features/audioPlayer/audioPlayer.ts b/src/features/audioPlayer/audioPlayer.ts new file mode 100644 index 00000000..7311c0d7 --- /dev/null +++ b/src/features/audioPlayer/audioPlayer.ts @@ -0,0 +1,32 @@ +export class AudioPlayer { + public readonly audio: AudioContext; + public bufferSource: AudioBufferSourceNode | undefined; + + public constructor(audio: AudioContext) { + this.audio = audio; + this.bufferSource = undefined; + } + + public async playFromArrayBuffer(buffer: ArrayBuffer, onEnded?: () => void) { + const audioBuffer = await this.audio.decodeAudioData(buffer); + + this.bufferSource = this.audio.createBufferSource(); + this.bufferSource.buffer = audioBuffer; + + this.bufferSource.connect(this.audio.destination); + this.bufferSource.start(); + if (onEnded) { + this.bufferSource.addEventListener('ended', onEnded); + } + } + + public stopPlay() { + if (this.bufferSource) this.bufferSource.stop(); + } + + public async playFromURL(url: string, onEnded?: () => void) { + const res = await fetch(url); + const buffer = await res.arrayBuffer(); + this.playFromArrayBuffer(buffer, onEnded); + } +} diff --git a/src/features/vrmViewer/model.ts b/src/features/vrmViewer/model.ts index a84e9011..70417ec0 100644 --- a/src/features/vrmViewer/model.ts +++ b/src/features/vrmViewer/model.ts @@ -3,6 +3,7 @@ import * as THREE from 'three'; import { AnimationAction, AnimationClip } from 'three'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'; +import { AudioPlayer } from '@/features/audioPlayer/audioPlayer'; import { convert } from '@/libs/VMDAnimation/vmd2vrmanim'; import { bindToVRM, toOffset } from '@/libs/VMDAnimation/vmd2vrmanim.binding'; import IKHandler from '@/libs/VMDAnimation/vrm-ik-handler'; @@ -26,14 +27,18 @@ export class Model { private _lookAtTargetParent: THREE.Object3D; private _lipSync?: LipSync; + private _audioPlayer?: AudioPlayer; private _action: AnimationAction | undefined; private _clip: AnimationClip | undefined; + private _audio: ArrayBuffer | undefined; constructor(lookAtTargetParent: THREE.Object3D) { this._lookAtTargetParent = lookAtTargetParent; this._lipSync = new LipSync(new AudioContext()); + this._audioPlayer = new AudioPlayer(new AudioContext()); this._action = undefined; this._clip = undefined; + this._audio = undefined; } public async loadVRM(url: string): Promise { @@ -72,30 +77,49 @@ export class Model { } } - /** - * VRMアニメーションを読み込む - * - * https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm_animation-1.0/README.ja.md - */ - public async loadAnimation(vrmAnimation: VRMAnimation): Promise { + public disposeAll() { const { vrm, mixer } = this; + if (!vrm || !mixer) { console.error('You have to load VRM first'); return; } - if (this._action) this._action.stop(); + + mixer.stopAllAction(); + this.ikHandler?.disableAll(); + if (this._action) { + this._action.stop(); + this._action = undefined; + } + + if (this._audio) { + this._audioPlayer?.stopPlay(); + this._audio = undefined; + } + if (this._clip) { mixer.uncacheAction(this._clip); mixer.uncacheClip(this._clip); this._clip = undefined; } + } - const clip = vrmAnimation.createAnimationClip(vrm); + /** + * VRMアニメーションを読み込む + * + * https://github.com/vrm-c/vrm-specification/blob/master/specification/VRMC_vrm_animation-1.0/README.ja.md + */ + public async loadAnimation(vrmAnimation: VRMAnimation): Promise { + const { vrm, mixer } = this; - const action = mixer.clipAction(clip); - action.play(); - this._action = action; - this._clip = clip; + if (vrm && mixer) { + this.disposeAll(); + const clip = vrmAnimation.createAnimationClip(vrm); + const action = mixer.clipAction(clip); + action.play(); + this._action = action; + this._clip = clip; + } } public async loadIdleAnimation() { @@ -107,13 +131,7 @@ export class Model { const { vrm, mixer } = this; if (vrm && mixer) { - mixer.stopAllAction(); - if (this._action) this._action.stop(); - if (this._clip) { - mixer.uncacheAction(this._clip); - mixer.uncacheClip(this._clip); - this._clip = undefined; - } + this.disposeAll(); // Load animation const clip = await loadMixamoAnimation(animationUrl, vrm); // Apply the loaded animation to mixer and play @@ -126,32 +144,32 @@ export class Model { /** * 播放舞蹈 - * @param buffer ArrayBuffer + * @param audio ArrayBuffer + * @param dance ArrayBuffer */ - public async dance(buffer: ArrayBuffer) { + public async dance(dance: ArrayBuffer, audio?: ArrayBuffer) { const { vrm, mixer } = this; if (vrm && mixer) { - mixer.stopAllAction(); - if (this._action) this._action.stop(); - if (this._clip) { - mixer.uncacheAction(this._clip); - mixer.uncacheClip(this._clip); - this._clip = undefined; - } - const animation = convert(buffer, toOffset(vrm)); + this.disposeAll(); + const animation = convert(dance, toOffset(vrm)); const clip = bindToVRM(animation, vrm); const action = mixer.clipAction(clip); action.play(); // play animation + if (audio) { + this._audioPlayer?.playFromArrayBuffer(audio); + this._audio = audio; + } + this._action = action; this._clip = clip; } } - public async stopDance() { + public async resetToIdle() { const { vrm, mixer } = this; if (vrm && mixer) { - mixer.stopAllAction(); - this.ikHandler?.disableAll(); + this.disposeAll(); + await this.loadIdleAnimation(); } } diff --git a/src/hooks/useDownloadDance.tsx b/src/hooks/useDownloadDance.tsx index 66127fb2..c4866136 100644 --- a/src/hooks/useDownloadDance.tsx +++ b/src/hooks/useDownloadDance.tsx @@ -33,7 +33,7 @@ export const useDownloadDance = () => { onProgress: (loaded, total) => { setAudioProgress((loaded / total) * 100); }, - }); + }).then((res) => res.arrayBuffer()); const dancePromise = fetchWithProgress(dance.src, { onProgress: (loaded, total) => { @@ -42,7 +42,7 @@ export const useDownloadDance = () => { }).then((res) => res.arrayBuffer()); try { - const [audioBlob, coverBase64, danceArrayBuffer] = await Promise.all([ + const [audioArrayBuffer, coverBase64, danceArrayBuffer] = await Promise.all([ audioPromise, coverPromise, dancePromise, @@ -51,7 +51,7 @@ export const useDownloadDance = () => { await setItem(danceKey, danceArrayBuffer); const audioKey = getAudioPathByDanceId(dance.danceId); - await setItem(audioKey, audioBlob); + await setItem(audioKey, audioArrayBuffer); addDanceItem({ ...dance, cover: coverBase64 }); message.success(dance.name + t('actions.downloadSuccess')); diff --git a/src/hooks/useLoadAudio.tsx b/src/hooks/useLoadAudio.tsx index b280aaf2..61f3996e 100644 --- a/src/hooks/useLoadAudio.tsx +++ b/src/hooks/useLoadAudio.tsx @@ -1,3 +1,4 @@ +import { isArrayBuffer } from 'lodash-es'; import { useState } from 'react'; import { fetchWithProgress } from '@/utils/fetch'; @@ -8,30 +9,34 @@ export const useLoadAudio = () => { const [downloading, setDownloading] = useState(false); const [percent, setPercent] = useState(0); - const fetchAudioUrl = async (danceId: string, audioUrl: string) => { + const fetchAudioBuffer = async (danceId: string, audioUrl: string) => { const localAudioPath = getAudioPathByDanceId(danceId); - let audioBlob = (await storage.getItem(localAudioPath)) as Blob; + let audioBuffer = (await storage.getItem(localAudioPath)) as ArrayBuffer; + // 存量转换 + if (audioBuffer && !isArrayBuffer(audioBuffer)) { + audioBuffer = await (audioBuffer as Blob).arrayBuffer(); + } try { - if (!audioBlob) { + if (!audioBuffer) { setDownloading(true); setPercent(0); - audioBlob = await fetchWithProgress(audioUrl, { + audioBuffer = await fetchWithProgress(audioUrl, { onProgress: (loaded, total) => { setPercent((loaded / total) * 100); }, - }); - await storage.setItem(localAudioPath, audioBlob); + }).then((res) => res.arrayBuffer()); + await storage.setItem(localAudioPath, audioBuffer); } } finally { setDownloading(false); } - return URL.createObjectURL(audioBlob); + return audioBuffer; }; return { downloading, percent, - fetchAudioUrl, + fetchAudioBuffer, }; }; diff --git a/src/locales/default/features.ts b/src/locales/default/features.ts index bd0078d8..8e15d665 100644 --- a/src/locales/default/features.ts +++ b/src/locales/default/features.ts @@ -49,6 +49,7 @@ export default { cameraControl: '镜头控制', floor: '切换地板', resetCamera: '重置镜头', + resetToIdle: '停止舞蹈动作', screenShot: '拍照', grid: '网格', axes: '坐标轴', diff --git a/src/store/dance/slices/dancelist.ts b/src/store/dance/slices/dancelist.ts index 48965a6b..022175ad 100644 --- a/src/store/dance/slices/dancelist.ts +++ b/src/store/dance/slices/dancelist.ts @@ -12,26 +12,13 @@ export interface DanceListStore { activateDance: (identifier: string) => void; addDanceItem: (dance: Dance) => void; currentIdentifier: string; - /** - * current playing dance. - */ currentPlayId: string; danceList: Dance[]; danceLoading: boolean; deactivateDance: () => void; fetchDanceIndex: () => void; - isPlaying: boolean; - /** - * Play a dance. - * @param dance - */ - playItem: (danceId: string) => void; removeDanceItem: (danceId: string) => Promise; - setIsPlaying: (isPlaying: boolean) => void; - /** - * Toggle play or pause. - */ - togglePlayPause: () => void; + setCurrentPlayId: (danceId: string) => void; } export const createDanceStore: StateCreator< @@ -47,15 +34,10 @@ export const createDanceStore: StateCreator< currentIdentifier: '', currentPlayId: '', danceList: [], - isPlaying: false, danceLoading: false, deactivateDance: () => { set({ currentIdentifier: undefined }); }, - togglePlayPause: () => { - if (!get().currentPlayId) return; - set({ isPlaying: !get().isPlaying }); - }, fetchDanceIndex: async () => { set({ danceLoading: true }); try { @@ -68,9 +50,7 @@ export const createDanceStore: StateCreator< set({ danceLoading: false }); } }, - setIsPlaying: (isPlaying) => { - set({ isPlaying }); - }, + addDanceItem: (dance) => { const { danceList } = get(); @@ -83,8 +63,9 @@ export const createDanceStore: StateCreator< }); set({ danceList: newList }); }, - playItem: (danceId) => { - set({ currentPlayId: danceId, isPlaying: true }); + + setCurrentPlayId: (danceId) => { + set({ currentPlayId: danceId }); }, removeDanceItem: async (danceId) => { const { danceList } = get();