forked from 77media/video-flow
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Upload, Library, Play, Pause, RefreshCw, Music2, Volume2 } from 'lucide-react';
|
|
import { cn } from '@/public/lib/utils';
|
|
import { GlassIconButton } from './glass-icon-button';
|
|
import { ReplaceMusicModal } from './replace-music-modal';
|
|
|
|
interface Music {
|
|
url: string;
|
|
script: string;
|
|
name?: string;
|
|
duration?: string;
|
|
totalDuration?: string;
|
|
isLooped?: boolean;
|
|
}
|
|
|
|
interface MusicTabContentProps {
|
|
taskSketch: any[];
|
|
currentSketchIndex: number;
|
|
onSketchSelect: (index: number) => void;
|
|
music?: Music;
|
|
}
|
|
|
|
export function MusicTabContent({
|
|
taskSketch,
|
|
currentSketchIndex,
|
|
onSketchSelect,
|
|
music
|
|
}: MusicTabContentProps) {
|
|
const [isReplaceModalOpen, setIsReplaceModalOpen] = useState(false);
|
|
const [activeMethod, setActiveMethod] = useState('upload');
|
|
const [isPlaying, setIsPlaying] = useState(false);
|
|
const [progress, setProgress] = useState(0);
|
|
const [volume, setVolume] = useState(75);
|
|
const [isLooped, setIsLooped] = useState(music?.isLooped ?? true);
|
|
const [fadeIn, setFadeIn] = useState('0s');
|
|
const [fadeOut, setFadeOut] = useState('3s');
|
|
const [trimFrom, setTrimFrom] = useState('00 : 00');
|
|
const [trimTo, setTrimTo] = useState(music?.duration?.split(' : ').slice(0, 2).join(' : ') || '01 : 35');
|
|
const audioRef = useRef<HTMLAudioElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (music) {
|
|
setIsLooped(music.isLooped ?? true);
|
|
if (music.duration) {
|
|
const durationParts = music.duration.split(' : ');
|
|
if (durationParts.length >= 2) {
|
|
setTrimTo(`${durationParts[0]} : ${durationParts[1]}`);
|
|
}
|
|
}
|
|
}
|
|
}, [music]);
|
|
|
|
if (!music || !music.url) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
|
<Music2 className="w-16 h-16 mb-4" />
|
|
<p>No music data</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const handleTimeUpdate = () => {
|
|
if (audioRef.current) {
|
|
const progress = (audioRef.current.currentTime / audioRef.current.duration) * 100;
|
|
setProgress(progress);
|
|
}
|
|
};
|
|
|
|
const togglePlay = () => {
|
|
if (audioRef.current) {
|
|
if (isPlaying) {
|
|
audioRef.current.pause();
|
|
} else {
|
|
audioRef.current.play();
|
|
}
|
|
setIsPlaying(!isPlaying);
|
|
}
|
|
};
|
|
|
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (audioRef.current) {
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
const x = e.clientX - rect.left;
|
|
const percentage = (x / rect.width) * 100;
|
|
const time = (percentage / 100) * audioRef.current.duration;
|
|
audioRef.current.currentTime = time;
|
|
setProgress(percentage);
|
|
}
|
|
};
|
|
|
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newVolume = parseInt(e.target.value);
|
|
setVolume(newVolume);
|
|
if (audioRef.current) {
|
|
audioRef.current.volume = newVolume / 100;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<motion.div
|
|
className="space-y-4"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
>
|
|
<motion.div
|
|
className="group relative p-4 w-[280px] rounded-lg overflow-hidden border border-blue-500 bg-blue-500/10"
|
|
whileHover={{ scale: 1.01 }}
|
|
whileTap={{ scale: 0.99 }}
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-shrink-0">
|
|
<Music2 className="w-8 h-8 text-white/70" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium truncate">{music.name || music.script || 'Background music'}</h3>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="p-4 rounded-lg bg-white/5"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.1 }}
|
|
>
|
|
<h3 className="text-sm font-medium mb-2">Replace music</h3>
|
|
<div className="flex gap-4">
|
|
<motion.button
|
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
|
onClick={() => setIsReplaceModalOpen(true)}
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
<Upload className="w-6 h-6" />
|
|
<span>Upload music</span>
|
|
</motion.button>
|
|
|
|
<motion.button
|
|
className="flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border border-white/10 hover:border-white/20 transition-colors"
|
|
onClick={() => setIsReplaceModalOpen(true)}
|
|
whileHover={{ scale: 1.02 }}
|
|
whileTap={{ scale: 0.98 }}
|
|
>
|
|
<Library className="w-6 h-6" />
|
|
<span>Music library</span>
|
|
</motion.button>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="grid grid-cols-2 gap-6"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.2 }}
|
|
>
|
|
<div className="space-y-6">
|
|
<motion.div
|
|
className="space-y-2"
|
|
whileHover={{ scale: 1.01 }}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-sm text-white/70">Loop music</label>
|
|
<motion.div
|
|
className={cn(
|
|
"w-12 h-6 rounded-full p-1 cursor-pointer",
|
|
isLooped ? "bg-blue-500" : "bg-white/10"
|
|
)}
|
|
onClick={() => setIsLooped(!isLooped)}
|
|
layout
|
|
>
|
|
<motion.div
|
|
className="w-4 h-4 bg-white rounded-full"
|
|
layout
|
|
animate={{
|
|
x: isLooped ? "100%" : "0%"
|
|
}}
|
|
transition={{ type: "spring", stiffness: 300, damping: 25 }}
|
|
/>
|
|
</motion.div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="space-y-2"
|
|
whileHover={{ scale: 1.01 }}
|
|
>
|
|
<label className="text-sm text-white/70">Trim</label>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-white/50">from</span>
|
|
<input
|
|
type="text"
|
|
value={trimFrom}
|
|
onChange={(e) => setTrimFrom(e.target.value)}
|
|
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
|
focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-white/50">to</span>
|
|
<input
|
|
type="text"
|
|
value={trimTo}
|
|
onChange={(e) => setTrimTo(e.target.value)}
|
|
className="w-20 px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
|
focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="space-y-4"
|
|
whileHover={{ scale: 1.01 }}
|
|
>
|
|
<label className="text-sm text-white/70">Fade in & out (max 10s)</label>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<span className="text-xs text-white/50">Fade in:</span>
|
|
<input
|
|
type="text"
|
|
value={fadeIn}
|
|
onChange={(e) => setFadeIn(e.target.value)}
|
|
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
|
focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<span className="text-xs text-white/50">Fade out:</span>
|
|
<input
|
|
type="text"
|
|
value={fadeOut}
|
|
onChange={(e) => setFadeOut(e.target.value)}
|
|
className="w-full px-2 py-1 bg-white/5 border border-white/10 rounded text-center
|
|
focus:outline-none focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
<motion.div
|
|
className="space-y-2"
|
|
whileHover={{ scale: 1.01 }}
|
|
>
|
|
<label className="text-sm text-white/70">Music volume</label>
|
|
<div className="flex items-center gap-3">
|
|
<Volume2 className="w-4 h-4 text-white/50" />
|
|
<div className="flex-1 h-1 bg-white/10 rounded-full relative">
|
|
<motion.div
|
|
className="absolute h-full bg-blue-500 rounded-full"
|
|
style={{ width: `${volume}%` }}
|
|
layout
|
|
/>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="100"
|
|
value={volume}
|
|
onChange={handleVolumeChange}
|
|
className="absolute w-full h-full opacity-0 cursor-pointer"
|
|
/>
|
|
</div>
|
|
<span className="text-sm text-white/50 w-12 text-right">{volume}%</span>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
<motion.div
|
|
className="p-6 rounded-lg bg-white/5 flex flex-col items-center justify-center gap-6"
|
|
whileHover={{ scale: 1.01 }}
|
|
>
|
|
<div className="w-24 h-24 rounded-full bg-white/10 flex items-center justify-center relative">
|
|
<motion.div
|
|
className="absolute w-full h-full rounded-full border-2 border-blue-500 border-t-transparent"
|
|
animate={{ rotate: 360 }}
|
|
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
|
style={{ opacity: isPlaying ? 1 : 0 }}
|
|
/>
|
|
<motion.button
|
|
className="w-16 h-16 rounded-full bg-blue-500/20 hover:bg-blue-500/30
|
|
flex items-center justify-center transition-colors"
|
|
onClick={togglePlay}
|
|
whileHover={{ scale: 1.1 }}
|
|
whileTap={{ scale: 0.9 }}
|
|
>
|
|
{isPlaying ? (
|
|
<Pause className="w-8 h-8 text-blue-500" />
|
|
) : (
|
|
<Play className="w-8 h-8 text-blue-500" />
|
|
)}
|
|
</motion.button>
|
|
</div>
|
|
|
|
<div className="w-full space-y-3">
|
|
<div
|
|
className="w-full h-1 bg-white/10 rounded-full cursor-pointer overflow-hidden"
|
|
onClick={handleProgressClick}
|
|
>
|
|
<motion.div
|
|
className="h-full bg-blue-500 rounded-full"
|
|
style={{ width: `${progress}%` }}
|
|
layout
|
|
/>
|
|
</div>
|
|
<div className="text-sm text-white/50 text-center">
|
|
{audioRef.current ? (
|
|
`${Math.floor(audioRef.current.currentTime)}s / ${Math.floor(audioRef.current.duration)}s`
|
|
) : '0:00 / 0:00'}
|
|
</div>
|
|
</div>
|
|
|
|
<audio
|
|
ref={audioRef}
|
|
src={music.url}
|
|
onTimeUpdate={handleTimeUpdate}
|
|
onEnded={() => setIsPlaying(false)}
|
|
loop={isLooped}
|
|
style={{ display: 'none' }}
|
|
/>
|
|
</motion.div>
|
|
</motion.div>
|
|
|
|
<ReplaceMusicModal
|
|
isOpen={isReplaceModalOpen}
|
|
activeReplaceMethod={activeMethod}
|
|
onClose={() => setIsReplaceModalOpen(false)}
|
|
onMusicSelect={(music) => {
|
|
console.log('Selected music:', music);
|
|
setIsReplaceModalOpen(false);
|
|
// TODO: 处理音乐选择逻辑
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|