Update metadata

This commit is contained in:
2025-11-02 23:11:05 +11:00
parent a624183d3c
commit 3d9a7ce9af
3 changed files with 184 additions and 2 deletions
@@ -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>
)
}
+24
View File
@@ -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 }