mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-08 20:59:47 +10:00
Update metadata
This commit is contained in:
@@ -3,10 +3,11 @@ import { artistApi, autoDownloadApi } from '@/api/client'
|
||||
import { Music, ArtistInfo, ArtistSongsResponse, OnlineSong } from '@/types'
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowLeft, Play, Loader2, Music2, RefreshCw, Download } from 'lucide-react'
|
||||
import { ArrowLeft, Play, Loader2, Music2, RefreshCw, Download, Edit } from 'lucide-react'
|
||||
import { formatDuration } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import EditMetadataDialog from '../music/EditMetadataDialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -30,6 +31,7 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
|
||||
const [selectedSong, setSelectedSong] = useState<OnlineSong | null>(null)
|
||||
const highlightSongId = searchParams.get('highlight') ? Number(searchParams.get('highlight')) : null
|
||||
const songRefs = useRef<{ [key: number]: HTMLDivElement | null }>({})
|
||||
const [editingMusic, setEditingMusic] = useState<Music | null>(null)
|
||||
|
||||
const { data: artistInfo } = useQuery({
|
||||
queryKey: ['artist-info', decodedArtistName],
|
||||
@@ -242,7 +244,20 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setEditingMusic(song)
|
||||
}}
|
||||
title="Edit metadata"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="text-sm text-muted-foreground w-16 text-right">
|
||||
{formatDuration(song.duration || 0)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -349,6 +364,13 @@ export default function ArtistDetailPage({ onPlayMusic }: ArtistDetailProps) {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Metadata Dialog */}
|
||||
<EditMetadataDialog
|
||||
music={editingMusic}
|
||||
open={!!editingMusic}
|
||||
onClose={() => setEditingMusic(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { musicApi } from '@/api/client'
|
||||
import { Music } from '@/types'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface EditMetadataDialogProps {
|
||||
music: Music | null
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function EditMetadataDialog({ music, open, onClose }: EditMetadataDialogProps) {
|
||||
const [title, setTitle] = useState('')
|
||||
const [artist, setArtist] = useState('')
|
||||
const [album, setAlbum] = useState('')
|
||||
const [lyrics, setLyrics] = useState('')
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
useEffect(() => {
|
||||
if (music) {
|
||||
setTitle(music.title || '')
|
||||
setArtist(music.artist || '')
|
||||
setAlbum(music.album || '')
|
||||
setLyrics(music.lyrics || '')
|
||||
}
|
||||
}, [music])
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (data: { title: string; artist: string; album: string; lyrics: string }) => {
|
||||
if (!music) return
|
||||
await musicApi.update(music.id, data)
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Metadata updated successfully')
|
||||
queryClient.invalidateQueries({ queryKey: ['music'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['artist-all-songs'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['playlists'] })
|
||||
onClose()
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to update metadata')
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
updateMutation.mutate({
|
||||
title: title.trim(),
|
||||
artist: artist.trim(),
|
||||
album: album.trim(),
|
||||
lyrics: lyrics.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Metadata</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update song metadata. These changes will be preserved even when rescanning music files.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Song Title *</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter song title"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="artist">Artist</Label>
|
||||
<Input
|
||||
id="artist"
|
||||
value={artist}
|
||||
onChange={(e) => setArtist(e.target.value)}
|
||||
placeholder="Enter artist name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="album">Album</Label>
|
||||
<Input
|
||||
id="album"
|
||||
value={album}
|
||||
onChange={(e) => setAlbum(e.target.value)}
|
||||
placeholder="Enter album name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="lyrics">Lyrics</Label>
|
||||
<Textarea
|
||||
id="lyrics"
|
||||
value={lyrics}
|
||||
onChange={(e) => setLyrics(e.target.value)}
|
||||
placeholder="Enter lyrics (optional)"
|
||||
rows={6}
|
||||
className="resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={updateMutation.isPending}>
|
||||
{updateMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user