Files
you-music/frontend/src/components/search/SearchPage.tsx
T
2025-10-30 22:35:02 +11:00

193 lines
6.9 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { searchApi, downloadApi } from '@/api/client'
import { Music } from '@/types'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Download, Search as SearchIcon, Play } from 'lucide-react'
import { toast } from 'sonner'
interface SearchPageProps {
onPlayMusic: (music: Music) => void
}
export default function SearchPage({ onPlayMusic }: SearchPageProps) {
const [query, setQuery] = useState('')
const [searchQuery, setSearchQuery] = useState('')
const queryClient = useQueryClient()
const { data: results, isLoading } = useQuery({
queryKey: ['search', searchQuery],
queryFn: async () => {
if (!searchQuery) return { youtube: [], bilibili: [] }
const response = await searchApi.search(searchQuery)
return response.data
},
enabled: !!searchQuery,
})
const downloadMutation = useMutation({
mutationFn: ({ url, title }: { url: string; title: string; thumbnail?: string; artist?: string }) =>
downloadApi.downloadMusic({ url, title }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['download-status'] })
toast.success('Download started! Check Download Center for progress.')
},
onError: () => {
toast.error('Download failed')
},
})
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
setSearchQuery(query)
}
const handleDownload = (url: string, title: string, thumbnail?: string, artist?: string) => {
downloadMutation.mutate({ url, title, thumbnail, artist })
}
const handlePlayAndDownload = async (url: string, title: string, thumbnail?: string, artist?: string) => {
// Start download
downloadMutation.mutate({ url, title, thumbnail, artist })
// Create temporary music object for streaming playback
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`
const tempMusic: Music = {
id: 0,
title,
artist: artist || null,
album: null,
duration: null,
file_path: streamUrl,
file_size: null,
source_url: url,
source_type: url.includes('youtube') ? 'youtube' : 'bilibili',
thumbnail: thumbnail || null,
lyrics: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
file_format: null,
file_location: null,
file_exists: true,
last_scanned_at: null,
}
onPlayMusic(tempMusic)
toast.success('Playing while downloading...')
}
return (
<div className="max-w-screen-xl mx-auto p-4">
<form onSubmit={handleSearch} className="mb-6">
<div className="flex gap-2">
<Input
type="text"
placeholder="Search for music..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1"
/>
<Button type="submit" disabled={isLoading}>
<SearchIcon className="h-5 w-5" />
</Button>
</div>
</form>
{isLoading && <div>Searching...</div>}
{results && (
<div className="space-y-6">
{results.youtube?.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">YouTube Results</h3>
<div className="grid grid-cols-1 gap-2">
{results.youtube.map((result: any, index: number) => (
<div
key={index}
className="flex items-center gap-4 p-3 rounded-lg hover:bg-accent"
>
{result.thumbnail && (
<img
src={result.thumbnail}
alt={result.title}
className="w-16 h-16 object-cover rounded"
/>
)}
<div className="flex-1">
<h4 className="font-medium">{result.title}</h4>
<p className="text-sm text-muted-foreground">{result.artist}</p>
</div>
<div className="flex gap-2">
<Button
size="icon"
variant="default"
onClick={() => handlePlayAndDownload(result.url, result.title, result.thumbnail, result.artist)}
disabled={downloadMutation.isPending}
>
<Play className="h-5 w-5" />
</Button>
<Button
size="icon"
variant="outline"
onClick={() => handleDownload(result.url, result.title, result.thumbnail, result.artist)}
disabled={downloadMutation.isPending}
>
<Download className="h-5 w-5" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
{results.bilibili?.length > 0 && (
<div>
<h3 className="text-lg font-semibold mb-3">Bilibili Results</h3>
<div className="grid grid-cols-1 gap-2">
{results.bilibili.map((result: any, index: number) => (
<div
key={index}
className="flex items-center gap-4 p-3 rounded-lg hover:bg-accent"
>
{result.thumbnail && (
<img
src={result.thumbnail}
alt={result.title}
className="w-16 h-16 object-cover rounded"
/>
)}
<div className="flex-1">
<h4 className="font-medium">{result.title}</h4>
<p className="text-sm text-muted-foreground">{result.artist}</p>
</div>
<div className="flex gap-2">
<Button
size="icon"
variant="default"
onClick={() => handlePlayAndDownload(result.url, result.title, result.thumbnail, result.artist)}
disabled={downloadMutation.isPending}
>
<Play className="h-5 w-5" />
</Button>
<Button
size="icon"
variant="outline"
onClick={() => handleDownload(result.url, result.title, result.thumbnail, result.artist)}
disabled={downloadMutation.isPending}
>
<Download className="h-5 w-5" />
</Button>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)
}