|
@@ -1,275 +1,124 @@
|
|
|
-import EmptyData from "@/components/empty-data";
|
|
|
-import CardListItem from "@/components/list/card-list-item/index";
|
|
|
-import CardList from "@/components/list/card-list/index";
|
|
|
-import { IVoicePlayerBar } from "@/components/voice-player-bar/index";
|
|
|
-import WemetaRadio from "@/components/wemeta-radio/index";
|
|
|
-import { CloneVoiceStatus } from "@/consts/enum";
|
|
|
-import IconWave from "@/images/icon-wave-20.png";
|
|
|
-import { voiceCloneConfirm } from "@/service/character";
|
|
|
-import { useAppStore } from "@/store/appStore";
|
|
|
-import { useCharacterStore } from "@/store/characterStore";
|
|
|
-import { ICharacter, TEntityVoiceCloneRecord } from "@/types";
|
|
|
-import { formatDateFull, getCloneVoiceIdentifier } from "@/utils/index";
|
|
|
-import { Image, View } from "@tarojs/components";
|
|
|
-import { useEffect, useRef, useState } from "react";
|
|
|
-import PopupContainer from "./components/popup-container";
|
|
|
-import PopupRecorder, { ECloneStatus } from "./components/popup-recorder/index";
|
|
|
-import PopupTryout from "./components/popup-tryout/index";
|
|
|
+import WemetaTabs from "@/components/wemeta-tabs/index";
|
|
|
+import VoicePlayerBar, { IVoicePlayerBar } from "@/components/voice-player-bar";
|
|
|
+import SystemVoice from "./components/system-voice";
|
|
|
+import MyVoice from "./components/my-voice/index";
|
|
|
+import { TServiceAudioModel } from "@/types";
|
|
|
+import { View, ScrollView } from "@tarojs/components";
|
|
|
+import { useRouter } from "@tarojs/taro";
|
|
|
+import React, { useRef, useState } from "react";
|
|
|
+import NavBarNormal from "@/components/nav-bar-normal/index";
|
|
|
import style from "./index.module.less";
|
|
|
-
|
|
|
-interface Props {
|
|
|
- value?: ICharacter;
|
|
|
- setValue?: (value: ICharacter) => void;
|
|
|
- profileId: string;
|
|
|
- onPlay?: (voice: any) => void;
|
|
|
-}
|
|
|
-
|
|
|
-export default ({ profileId, onPlay }: Props) => {
|
|
|
- const playerRef = useRef<IVoicePlayerBar>(null);
|
|
|
- const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
- const [show, setShow] = useState(false);
|
|
|
- const [popupType, setPopupType] = useState<"clone" | "try" | "reclone">(
|
|
|
- "clone",
|
|
|
+import PageCustom from "@/components/page-custom/index";
|
|
|
+
|
|
|
+interface Props {}
|
|
|
+type ExtendedTServiceAudioModel = TServiceAudioModel & { checked: boolean };
|
|
|
+const VoiceTabs: React.FC<Props> = ({}) => {
|
|
|
+const playerRef = useRef<IVoicePlayerBar>(null);
|
|
|
+const [sysVoice, setSysVoice] = useState<ExtendedTServiceAudioModel | null>(
|
|
|
+ null
|
|
|
);
|
|
|
|
|
|
const [voiceName, setVoiceName] = useState("");
|
|
|
- const [voiceIndex, setVoiceIndex] = useState(-1);
|
|
|
-
|
|
|
- const { saveCharacter, fetchCharacter, fetchVoiceCloneHistory } =
|
|
|
- useCharacterStore();
|
|
|
- const character = useCharacterStore((state) => state.character);
|
|
|
- const voiceList = useCharacterStore((state) => state.voiceList);
|
|
|
- const appConfig = useAppStore((state) => state.appConfig);
|
|
|
-
|
|
|
- const createVoiceNameText = (voiceName: string) => {
|
|
|
- const i = voiceList.findIndex((item) => item.voiceName === voiceName);
|
|
|
- console.log(voiceName, i);
|
|
|
- if (i === -1) return "";
|
|
|
- return `克隆声音 0${i + 1}`;
|
|
|
- };
|
|
|
- // 克隆列表操作
|
|
|
- const renderRightColumn = (item?: TEntityVoiceCloneRecord) => {
|
|
|
- if (item?.status == CloneVoiceStatus.CloneVoiceStatusExpired) {
|
|
|
- return <WemetaRadio disabled></WemetaRadio>;
|
|
|
- }
|
|
|
-
|
|
|
- if (item?.status == CloneVoiceStatus.CloneVoiceStatusSuccess) {
|
|
|
- const notSystemVoice = (character?.voice !== character?.defaultSystemVoice)
|
|
|
- const isSameVoice = (item.voiceName === character?.voice)
|
|
|
- return (
|
|
|
- <WemetaRadio
|
|
|
- checked={ isSameVoice && notSystemVoice}
|
|
|
- ></WemetaRadio>
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- if (item?.status == CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
|
|
|
- return (
|
|
|
- <View className="text-14 bg-primary text-black leading-22 px-12 py-6 rounded-28 active:pressed-button">
|
|
|
- 试听
|
|
|
+ const [voiceAlias, setVoiceAlias] = useState("");
|
|
|
+ const [voiceIdx, setVoiceIdx] = useState(-1);
|
|
|
+ const router = useRouter();
|
|
|
+ const profileId = router.params.profileId || "";
|
|
|
+
|
|
|
+ const tabList = [
|
|
|
+ {
|
|
|
+ label: "我的",
|
|
|
+ key: "1",
|
|
|
+ children: (
|
|
|
+ <View className={style.tabContent}>
|
|
|
+ <MyVoice profileId={profileId}></MyVoice>
|
|
|
</View>
|
|
|
- );
|
|
|
- }
|
|
|
- return <></>;
|
|
|
- };
|
|
|
-
|
|
|
- // 克隆状态
|
|
|
- const renderCloneStatus = (item: TEntityVoiceCloneRecord) => {
|
|
|
- if (item.status === CloneVoiceStatus.CloneVoiceStatusSuccess) {
|
|
|
- return (
|
|
|
- <View className={`text-12 leading-20 text-gray-45`}>
|
|
|
- {item.createdAt && formatDateFull(new Date(item.createdAt))}
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "全部",
|
|
|
+ key: "2",
|
|
|
+ children: (
|
|
|
+ <View className={style.tabContent}>
|
|
|
+ <ScrollView
|
|
|
+ scrollY
|
|
|
+ id="scrollView"
|
|
|
+ style={{
|
|
|
+ flex: 1,
|
|
|
+ height: "100%", // 高度自适应
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <View className="px-16 py-12">
|
|
|
+ <SystemVoice
|
|
|
+ profileId={profileId}
|
|
|
+ onPlay={(item) => {
|
|
|
+ handlePlayAction(item, "system");
|
|
|
+ }}
|
|
|
+ ></SystemVoice>
|
|
|
+ </View>
|
|
|
+ </ScrollView>
|
|
|
</View>
|
|
|
- );
|
|
|
- }
|
|
|
- if (item.status === CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
|
|
|
- return <View className={`text-12 leading-20 text-orange`}>试听确认</View>;
|
|
|
- }
|
|
|
-
|
|
|
- if (item.status == CloneVoiceStatus.CloneVoiceStatusExpired) {
|
|
|
- return <View className={`text-12 leading-20 text-orange`}>已过期</View>;
|
|
|
- }
|
|
|
- return <View>当前时间</View>;
|
|
|
- };
|
|
|
-
|
|
|
- const renderCloneList = () => {
|
|
|
- if (!voiceList.length) {
|
|
|
- return <EmptyData></EmptyData>;
|
|
|
- }
|
|
|
- return (
|
|
|
- <CardList>
|
|
|
- {voiceList.map((item, _index) => {
|
|
|
- return (
|
|
|
- <CardListItem
|
|
|
- rightRenderer={() => {
|
|
|
- return renderRightColumn(item);
|
|
|
- }}
|
|
|
- onClick={() => handleSelect(item, _index)}
|
|
|
- >
|
|
|
- <View className="flex items-center gap-16">
|
|
|
- <View className={style.listIcon}>
|
|
|
- <Image src={IconWave} className={style.iconImage}></Image>
|
|
|
- </View>
|
|
|
- <View className="flex flex-col gap-4">
|
|
|
- <View className={style.ListItemText}>
|
|
|
- {/* deprecated getCloneVoiceIdentifier */}
|
|
|
- {item.voiceAlias ?? getCloneVoiceIdentifier(_index + 1)}
|
|
|
- </View>
|
|
|
- {renderCloneStatus(item)}
|
|
|
- </View>
|
|
|
- </View>
|
|
|
- </CardListItem>
|
|
|
- );
|
|
|
- })}
|
|
|
- </CardList>
|
|
|
- );
|
|
|
- };
|
|
|
-
|
|
|
- const handleSelect = (item: TEntityVoiceCloneRecord, index: number) => {
|
|
|
- setVoiceIndex(index);
|
|
|
-
|
|
|
- if (item.status == CloneVoiceStatus.CloneVoiceStatusSuccess) {
|
|
|
- if (item.voiceName) {
|
|
|
- setVoiceName(item.voiceName);
|
|
|
- onPlay &&
|
|
|
- onPlay({
|
|
|
- voiceName: item.voiceName,
|
|
|
- voiceAlias: item.voiceAlias,
|
|
|
- voiceIndex: index,
|
|
|
- });
|
|
|
- }
|
|
|
- saveCharacter({
|
|
|
- profileId: profileId,
|
|
|
- voice: item.voiceName,
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- if (item.status == CloneVoiceStatus.CloneVoiceStatusUnconfirmed) {
|
|
|
- // 未确认,弹出试听框
|
|
|
- setPopupType("try");
|
|
|
- setShow(true);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 克隆按钮状态
|
|
|
- const handleCloneStatus = (status: ECloneStatus) => {
|
|
|
- console.log(status);
|
|
|
- };
|
|
|
-
|
|
|
- const fetchVoiceList = async () => {
|
|
|
- if (profileId) {
|
|
|
- const r = await fetchVoiceCloneHistory(profileId);
|
|
|
- const result = r.find((item) => item.status === "pending");
|
|
|
- if (result) {
|
|
|
- intervalRef.current = setTimeout(() => fetchVoiceList(), 3000);
|
|
|
- } else {
|
|
|
- stopTimer();
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const handleSureAction = async () => {
|
|
|
- setShow(false);
|
|
|
- await voiceCloneConfirm(voiceList[voiceIndex].voiceName!);
|
|
|
- fetchVoiceList();
|
|
|
- };
|
|
|
-
|
|
|
- const handleRecloneAction = () => {
|
|
|
- setPopupType("reclone");
|
|
|
- setShow(true);
|
|
|
- };
|
|
|
-
|
|
|
- // 声音录制完成
|
|
|
- const onRecordEnd = (r: string) => {
|
|
|
- console.log("onRecordEnd:", r);
|
|
|
- fetchVoiceList();
|
|
|
- };
|
|
|
-
|
|
|
- const stopTimer = () => {
|
|
|
- if (intervalRef.current !== null) {
|
|
|
- clearTimeout(intervalRef.current);
|
|
|
- intervalRef.current = null;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const calcRemainCloneNum = () => {
|
|
|
- if (appConfig?.maxCloneNum) {
|
|
|
- const clonedNum = voiceList.filter(
|
|
|
- (item) =>
|
|
|
- item.status === CloneVoiceStatus.CloneVoiceStatusSuccess ||
|
|
|
- item.status === CloneVoiceStatus.CloneVoiceStatusUnconfirmed,
|
|
|
- ).length;
|
|
|
- const remainNum = appConfig.maxCloneNum - clonedNum;
|
|
|
- return remainNum < 0 ? 0 : remainNum;
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "女声",
|
|
|
+ key: "3",
|
|
|
+ children: (
|
|
|
+ <View className={style.tabContent}>
|
|
|
+ <View>我的</View>
|
|
|
+ </View>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ label: "男声",
|
|
|
+ key: "4",
|
|
|
+ children: (
|
|
|
+ <View className={style.tabContent}>
|
|
|
+ <View>我的</View>
|
|
|
+ </View>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ const handlePlayAction = (voiceItem: any, type: "system" | "cloned") => {
|
|
|
+ if (type == "system") {
|
|
|
+ setVoiceName("");
|
|
|
+ setVoiceAlias("");
|
|
|
+ setVoiceIdx(-1);
|
|
|
+
|
|
|
+ setSysVoice(voiceItem);
|
|
|
+ playerRef.current && playerRef.current.play(voiceItem.voice);
|
|
|
+ } else {
|
|
|
+ setSysVoice(null);
|
|
|
+ setVoiceName(voiceItem.voiceName);
|
|
|
+ setVoiceAlias(voiceItem.voiceAlias);
|
|
|
+ setVoiceIdx(voiceItem.voiceIndex);
|
|
|
+ playerRef.current && playerRef.current.play(voiceItem.voiceName);
|
|
|
}
|
|
|
- return voiceList.length;
|
|
|
};
|
|
|
|
|
|
- const initPage = async (profileId: string) => {
|
|
|
- await fetchCharacter(profileId);
|
|
|
- fetchVoiceCloneHistory(profileId);
|
|
|
- character?.voice && setVoiceName(character.voice);
|
|
|
- };
|
|
|
-
|
|
|
- useEffect(() => {
|
|
|
- profileId && initPage(profileId);
|
|
|
- }, [profileId]);
|
|
|
-
|
|
|
- // 清除定时器
|
|
|
- useEffect(() => {
|
|
|
- return () => {
|
|
|
- stopTimer();
|
|
|
- };
|
|
|
- }, []);
|
|
|
return (
|
|
|
- <View className={`flex flex-col`}>
|
|
|
- <View className="flex-1">
|
|
|
- {/* <VoicePlayerBar
|
|
|
- ref={playerRef}
|
|
|
- voiceName={voiceName}
|
|
|
- voiceNameText={createVoiceNameText(voiceName)}
|
|
|
- /> */}
|
|
|
-
|
|
|
- {renderCloneList()}
|
|
|
- </View>
|
|
|
-
|
|
|
- <PopupContainer show={show} setShow={setShow}>
|
|
|
- {(popupType == "clone" || popupType == "reclone") && (
|
|
|
- <PopupRecorder
|
|
|
- onRecordEnd={onRecordEnd}
|
|
|
- show={show}
|
|
|
- setShow={setShow}
|
|
|
- setCloneStatus={(status) => handleCloneStatus(status)}
|
|
|
- voiceName={
|
|
|
- popupType == "reclone" ? voiceList[voiceIndex].voiceName! : ""
|
|
|
- }
|
|
|
- ></PopupRecorder>
|
|
|
- )}
|
|
|
- {popupType == "try" && (
|
|
|
- <PopupTryout
|
|
|
- show={show}
|
|
|
- onSure={handleSureAction}
|
|
|
- onReclone={handleRecloneAction}
|
|
|
- voiceName={voiceList[voiceIndex].voiceName!}
|
|
|
- showName={getCloneVoiceIdentifier(voiceIndex + 1)}
|
|
|
- ></PopupTryout>
|
|
|
- )}
|
|
|
- </PopupContainer>
|
|
|
-
|
|
|
- <View
|
|
|
- className={style.addButton}
|
|
|
- onClick={() => {
|
|
|
- setPopupType("clone");
|
|
|
- setShow(true);
|
|
|
- }}
|
|
|
- >
|
|
|
- <View className="button-rounded-big font-medium">
|
|
|
- <View>添加克隆声音</View>
|
|
|
- <View className="font-normal text-12 leading-0">
|
|
|
- (剩{calcRemainCloneNum()}次)
|
|
|
- </View>
|
|
|
+ <PageCustom>
|
|
|
+ <NavBarNormal backText="声音"></NavBarNormal>
|
|
|
+ <View className={style.container}>
|
|
|
+ <View className={style.playContainer}>
|
|
|
+ <VoicePlayerBar
|
|
|
+ ref={playerRef}
|
|
|
+ voiceItem={sysVoice}
|
|
|
+ voiceName={voiceName}
|
|
|
+ voiceNameText={voiceIdx == -1 ? "" : voiceAlias}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ <View className={style.voiceTab}>
|
|
|
+ <WemetaTabs
|
|
|
+ full
|
|
|
+ list={tabList}
|
|
|
+ current="1"
|
|
|
+ tabStyle="outline"
|
|
|
+ ></WemetaTabs>
|
|
|
</View>
|
|
|
</View>
|
|
|
- </View>
|
|
|
+ </PageCustom>
|
|
|
);
|
|
|
};
|
|
|
+
|
|
|
+export default VoiceTabs;
|