Spaces:
Running
Running
fixing some things before video
Browse files- app/components/VoiceApp.tsx +102 -36
app/components/VoiceApp.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
| 8 |
BookOpen,
|
| 9 |
Play,
|
| 10 |
Stop,
|
|
|
|
| 11 |
DownloadSimple,
|
| 12 |
ArrowClockwise,
|
| 13 |
SpinnerGap
|
|
@@ -37,6 +38,9 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 37 |
const [voiceContents, setVoiceContents] = useState<VoiceContent[]>([])
|
| 38 |
const [currentlyPlaying, setCurrentlyPlaying] = useState<string | null>(null)
|
| 39 |
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null)
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
// Load saved content from server and localStorage
|
| 42 |
useEffect(() => {
|
|
@@ -84,27 +88,58 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 84 |
await loadContent()
|
| 85 |
}
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
const handlePlay = (content: VoiceContent) => {
|
| 88 |
if (!content.audioUrl) return
|
| 89 |
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
if (audioElement) {
|
| 92 |
audioElement.pause()
|
| 93 |
audioElement.currentTime = 0
|
| 94 |
}
|
| 95 |
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
}
|
| 110 |
|
|
@@ -114,18 +149,28 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 114 |
audioElement.currentTime = 0
|
| 115 |
setAudioElement(null)
|
| 116 |
setCurrentlyPlaying(null)
|
|
|
|
|
|
|
| 117 |
}
|
| 118 |
}
|
| 119 |
|
| 120 |
-
const handleDownload = (content: VoiceContent) => {
|
| 121 |
if (!content.audioUrl) return
|
| 122 |
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
|
| 131 |
const handleRefresh = () => {
|
|
@@ -206,8 +251,8 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 206 |
<div className="flex items-start justify-between mb-3">
|
| 207 |
<div className="flex items-center gap-3">
|
| 208 |
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${content.type === 'song'
|
| 209 |
-
|
| 210 |
-
|
| 211 |
}`}>
|
| 212 |
{content.type === 'song' ? (
|
| 213 |
<MusicNote size={20} weight="fill" />
|
|
@@ -258,25 +303,46 @@ export function VoiceApp({ onClose, onMinimize, onMaximize, onFocus, zIndex }: V
|
|
| 258 |
)}
|
| 259 |
|
| 260 |
{content.audioUrl && (
|
| 261 |
-
<
|
| 262 |
-
onClick={() => handlePlay(content)}
|
| 263 |
-
className={`w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium text-sm transition-all ${currentlyPlaying === content.id
|
| 264 |
-
? 'bg-red-50 text-red-600 border border-red-100 hover:bg-red-100'
|
| 265 |
-
: 'bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300'
|
| 266 |
-
}`}
|
| 267 |
-
>
|
| 268 |
{currentlyPlaying === content.id ? (
|
| 269 |
-
|
| 270 |
-
<
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
) : (
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
| 275 |
<Play size={16} weight="fill" />
|
| 276 |
Play Audio
|
| 277 |
-
|
| 278 |
)}
|
| 279 |
-
</
|
| 280 |
)}
|
| 281 |
</div>
|
| 282 |
)}
|
|
|
|
| 8 |
BookOpen,
|
| 9 |
Play,
|
| 10 |
Stop,
|
| 11 |
+
Pause,
|
| 12 |
DownloadSimple,
|
| 13 |
ArrowClockwise,
|
| 14 |
SpinnerGap
|
|
|
|
| 38 |
const [voiceContents, setVoiceContents] = useState<VoiceContent[]>([])
|
| 39 |
const [currentlyPlaying, setCurrentlyPlaying] = useState<string | null>(null)
|
| 40 |
const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(null)
|
| 41 |
+
const [isPlaying, setIsPlaying] = useState(false)
|
| 42 |
+
const [currentTime, setCurrentTime] = useState(0)
|
| 43 |
+
const [duration, setDuration] = useState(0)
|
| 44 |
|
| 45 |
// Load saved content from server and localStorage
|
| 46 |
useEffect(() => {
|
|
|
|
| 88 |
await loadContent()
|
| 89 |
}
|
| 90 |
|
| 91 |
+
const formatTime = (time: number) => {
|
| 92 |
+
const minutes = Math.floor(time / 60)
|
| 93 |
+
const seconds = Math.floor(time % 60)
|
| 94 |
+
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
const handlePlay = (content: VoiceContent) => {
|
| 98 |
if (!content.audioUrl) return
|
| 99 |
|
| 100 |
+
if (currentlyPlaying === content.id && audioElement) {
|
| 101 |
+
if (isPlaying) {
|
| 102 |
+
audioElement.pause()
|
| 103 |
+
setIsPlaying(false)
|
| 104 |
+
} else {
|
| 105 |
+
audioElement.play()
|
| 106 |
+
setIsPlaying(true)
|
| 107 |
+
}
|
| 108 |
+
return
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Stop previous
|
| 112 |
if (audioElement) {
|
| 113 |
audioElement.pause()
|
| 114 |
audioElement.currentTime = 0
|
| 115 |
}
|
| 116 |
|
| 117 |
+
const audio = new Audio(content.audioUrl)
|
| 118 |
+
|
| 119 |
+
audio.addEventListener('loadedmetadata', () => {
|
| 120 |
+
setDuration(audio.duration)
|
| 121 |
+
})
|
| 122 |
+
|
| 123 |
+
audio.addEventListener('timeupdate', () => {
|
| 124 |
+
setCurrentTime(audio.currentTime)
|
| 125 |
+
})
|
| 126 |
+
|
| 127 |
+
audio.addEventListener('ended', () => {
|
| 128 |
+
setIsPlaying(false)
|
| 129 |
+
setCurrentTime(0)
|
| 130 |
+
})
|
| 131 |
+
|
| 132 |
+
audio.play()
|
| 133 |
+
setAudioElement(audio)
|
| 134 |
+
setCurrentlyPlaying(content.id)
|
| 135 |
+
setIsPlaying(true)
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 139 |
+
const time = parseFloat(e.target.value)
|
| 140 |
+
setCurrentTime(time)
|
| 141 |
+
if (audioElement) {
|
| 142 |
+
audioElement.currentTime = time
|
| 143 |
}
|
| 144 |
}
|
| 145 |
|
|
|
|
| 149 |
audioElement.currentTime = 0
|
| 150 |
setAudioElement(null)
|
| 151 |
setCurrentlyPlaying(null)
|
| 152 |
+
setIsPlaying(false)
|
| 153 |
+
setCurrentTime(0)
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
| 157 |
+
const handleDownload = async (content: VoiceContent) => {
|
| 158 |
if (!content.audioUrl) return
|
| 159 |
|
| 160 |
+
try {
|
| 161 |
+
const response = await fetch(content.audioUrl)
|
| 162 |
+
const blob = await response.blob()
|
| 163 |
+
const url = window.URL.createObjectURL(blob)
|
| 164 |
+
const link = document.createElement('a')
|
| 165 |
+
link.href = url
|
| 166 |
+
link.download = `${content.title.replace(/\s+/g, '_')}.mp3`
|
| 167 |
+
document.body.appendChild(link)
|
| 168 |
+
link.click()
|
| 169 |
+
document.body.removeChild(link)
|
| 170 |
+
window.URL.revokeObjectURL(url)
|
| 171 |
+
} catch (error) {
|
| 172 |
+
console.error('Download failed:', error)
|
| 173 |
+
}
|
| 174 |
}
|
| 175 |
|
| 176 |
const handleRefresh = () => {
|
|
|
|
| 251 |
<div className="flex items-start justify-between mb-3">
|
| 252 |
<div className="flex items-center gap-3">
|
| 253 |
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${content.type === 'song'
|
| 254 |
+
? 'bg-purple-100 text-purple-600'
|
| 255 |
+
: 'bg-blue-100 text-blue-600'
|
| 256 |
}`}>
|
| 257 |
{content.type === 'song' ? (
|
| 258 |
<MusicNote size={20} weight="fill" />
|
|
|
|
| 303 |
)}
|
| 304 |
|
| 305 |
{content.audioUrl && (
|
| 306 |
+
<div className="mt-3">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
{currentlyPlaying === content.id ? (
|
| 308 |
+
<div className="bg-white rounded-lg border border-gray-200 p-3 space-y-2">
|
| 309 |
+
<div className="flex items-center gap-3">
|
| 310 |
+
<button
|
| 311 |
+
onClick={() => handlePlay(content)}
|
| 312 |
+
className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-900 text-white hover:bg-gray-800 transition-colors"
|
| 313 |
+
>
|
| 314 |
+
{isPlaying ? (
|
| 315 |
+
<Pause size={14} weight="fill" />
|
| 316 |
+
) : (
|
| 317 |
+
<Play size={14} weight="fill" />
|
| 318 |
+
)}
|
| 319 |
+
</button>
|
| 320 |
+
<div className="flex-1">
|
| 321 |
+
<input
|
| 322 |
+
type="range"
|
| 323 |
+
min="0"
|
| 324 |
+
max={duration || 100}
|
| 325 |
+
value={currentTime}
|
| 326 |
+
onChange={handleSeek}
|
| 327 |
+
className="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:bg-gray-900 [&::-webkit-slider-thumb]:rounded-full"
|
| 328 |
+
/>
|
| 329 |
+
<div className="flex justify-between text-[10px] text-gray-500 mt-1 font-medium">
|
| 330 |
+
<span>{formatTime(currentTime)}</span>
|
| 331 |
+
<span>{formatTime(duration)}</span>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
</div>
|
| 335 |
+
</div>
|
| 336 |
) : (
|
| 337 |
+
<button
|
| 338 |
+
onClick={() => handlePlay(content)}
|
| 339 |
+
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-lg font-medium text-sm bg-[#F5F5F7] text-gray-700 border border-gray-200 hover:bg-gray-200 hover:border-gray-300 transition-all"
|
| 340 |
+
>
|
| 341 |
<Play size={16} weight="fill" />
|
| 342 |
Play Audio
|
| 343 |
+
</button>
|
| 344 |
)}
|
| 345 |
+
</div>
|
| 346 |
)}
|
| 347 |
</div>
|
| 348 |
)}
|