myYouTube/frontend/src/components/DownloadButton.tsx
2026-09-16 21:40:32 +00:00

113 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { FeedVideoDto } from '../api/client'
import { deleteDownload, downloadVideo, getDownloadStatus } from '../api/client'
import Icon from './Icon'
const ACTIVE_STATUSES = ['queued', 'downloading', 'postprocessing']
const STATUS_LABELS: Record<string, string> = {
queued: 'В очереди...',
postprocessing: 'Обработка...',
}
interface Props {
video: FeedVideoDto
showDelete?: boolean
}
function DownloadButton({ video, showDelete = false }: Props) {
const queryClient = useQueryClient()
// Once we start tracking a download (it was already active on load, or the
// user just clicked "Скачать"), the query stays enabled for good --
// refetchInterval alone stops the ticking once the status is terminal.
// Disabling the query on completion used to make the button fall back to
// the *stale* `video` prop instead of the query's own fresh cache, which
// showed "Скачать" again until the page was manually reloaded.
const [tracking, setTracking] = useState(ACTIVE_STATUSES.includes(video.local.status))
const statusQuery = useQuery({
queryKey: ['download-status', video.youtube_video_id],
queryFn: () => getDownloadStatus(video.youtube_video_id),
enabled: tracking,
refetchInterval: (query) =>
query.state.data && ACTIVE_STATUSES.includes(query.state.data.status) ? 2000 : false,
})
const downloadMutation = useMutation({
mutationFn: () => downloadVideo(video.youtube_video_id),
onSuccess: (data) => {
queryClient.setQueryData(['download-status', video.youtube_video_id], data)
setTracking(true)
},
})
const deleteMutation = useMutation({
mutationFn: () => deleteDownload(video.youtube_video_id),
onSuccess: (data) => {
queryClient.setQueryData(['download-status', video.youtube_video_id], data)
},
})
const status = statusQuery.data?.status ?? video.local.status
const percent = statusQuery.data?.progress_percent ?? video.local.progress_percent
const prevStatus = useRef(status)
useEffect(() => {
if (prevStatus.current !== status) {
prevStatus.current = status
if (status === 'completed' || status === 'failed' || status === 'deleted') {
queryClient.invalidateQueries({ queryKey: ['feed'] })
queryClient.invalidateQueries({ queryKey: ['video', video.youtube_video_id] })
}
}
}, [status, queryClient, video.youtube_video_id])
if (status === 'completed') {
return (
<span className="download-inline-actions">
<span className="download-badge completed"><Icon name="check" size={15} /> На сервере</span>
{showDelete && <button className="button-quiet"
onClick={() => {
if (confirm('Удалить скачанную копию с сервера?')) {
deleteMutation.mutate()
}
}}
disabled={deleteMutation.isPending}
>Удалить копию</button>}
{deleteMutation.isError && (
<span className="download-badge error" title={(deleteMutation.error as Error).message}>
{(deleteMutation.error as Error).message}
</span>
)}
</span>
)
}
if (status === 'downloading') {
return <span className="download-badge downloading"><span className="download-progress" style={{ width: `${Math.min(100, Math.max(0, percent ?? 0))}%` }} />Скачивается {percent ?? 0}%</span>
}
if (status === 'queued' || status === 'postprocessing') {
return <span className="download-badge downloading">{STATUS_LABELS[status]}</span>
}
if (status === 'failed') {
return (
<span className="download-error">
<button className="button-secondary" onClick={() => downloadMutation.mutate()} disabled={downloadMutation.isPending}>
Повторить
</button>
{downloadMutation.isError && <span className="sr-only" role="alert">Не удалось повторить загрузку.</span>}
</span>
)
}
if (status === 'unknown') return <button className="button-secondary" onClick={() => downloadMutation.mutate()} disabled={downloadMutation.isPending}><Icon name="refresh" size={16} /> Загрузить снова</button>
return (
<span className="download-inline-actions"><button className="button-secondary" onClick={() => downloadMutation.mutate()} disabled={downloadMutation.isPending}><Icon name="download" size={16} />{downloadMutation.isPending ? 'Добавляем…' : 'Скачать'}</button>{downloadMutation.isError && <span className="download-action-error" role="alert">Не удалось добавить видео в очередь.</span>}</span>
)
}
export default DownloadButton