2026-09-16 19:37:33 +00:00
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
2026-09-16 18:56:17 +00:00
|
|
|
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
|
|
|
import type { FeedVideoDto } from '../api/client'
|
2026-09-16 19:47:35 +00:00
|
|
|
|
import { deleteDownload, downloadVideo, getDownloadStatus } from '../api/client'
|
2026-09-16 21:40:32 +00:00
|
|
|
|
import Icon from './Icon'
|
2026-09-16 18:56:17 +00:00
|
|
|
|
|
|
|
|
|
|
const ACTIVE_STATUSES = ['queued', 'downloading', 'postprocessing']
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
|
|
|
|
queued: 'В очереди...',
|
|
|
|
|
|
postprocessing: 'Обработка...',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
video: FeedVideoDto
|
2026-09-16 21:40:32 +00:00
|
|
|
|
showDelete?: boolean
|
2026-09-16 18:56:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 21:40:32 +00:00
|
|
|
|
function DownloadButton({ video, showDelete = false }: Props) {
|
2026-09-16 18:56:17 +00:00
|
|
|
|
const queryClient = useQueryClient()
|
2026-09-16 19:37:33 +00:00
|
|
|
|
// 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))
|
2026-09-16 18:56:17 +00:00
|
|
|
|
|
|
|
|
|
|
const statusQuery = useQuery({
|
|
|
|
|
|
queryKey: ['download-status', video.youtube_video_id],
|
|
|
|
|
|
queryFn: () => getDownloadStatus(video.youtube_video_id),
|
2026-09-16 19:37:33 +00:00
|
|
|
|
enabled: tracking,
|
|
|
|
|
|
refetchInterval: (query) =>
|
|
|
|
|
|
query.state.data && ACTIVE_STATUSES.includes(query.state.data.status) ? 2000 : false,
|
2026-09-16 18:56:17 +00:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const downloadMutation = useMutation({
|
|
|
|
|
|
mutationFn: () => downloadVideo(video.youtube_video_id),
|
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
|
queryClient.setQueryData(['download-status', video.youtube_video_id], data)
|
2026-09-16 19:37:33 +00:00
|
|
|
|
setTracking(true)
|
2026-09-16 18:56:17 +00:00
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-16 19:47:35 +00:00
|
|
|
|
const deleteMutation = useMutation({
|
|
|
|
|
|
mutationFn: () => deleteDownload(video.youtube_video_id),
|
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
|
queryClient.setQueryData(['download-status', video.youtube_video_id], data)
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-09-16 19:37:33 +00:00
|
|
|
|
const status = statusQuery.data?.status ?? video.local.status
|
|
|
|
|
|
const percent = statusQuery.data?.progress_percent ?? video.local.progress_percent
|
2026-09-16 18:56:17 +00:00
|
|
|
|
|
2026-09-16 19:37:33 +00:00
|
|
|
|
const prevStatus = useRef(status)
|
2026-09-16 18:56:17 +00:00
|
|
|
|
useEffect(() => {
|
2026-09-16 19:37:33 +00:00
|
|
|
|
if (prevStatus.current !== status) {
|
|
|
|
|
|
prevStatus.current = status
|
2026-09-16 19:47:35 +00:00
|
|
|
|
if (status === 'completed' || status === 'failed' || status === 'deleted') {
|
2026-09-16 18:56:17 +00:00
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['feed'] })
|
2026-09-16 19:37:33 +00:00
|
|
|
|
queryClient.invalidateQueries({ queryKey: ['video', video.youtube_video_id] })
|
2026-09-16 18:56:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-16 19:37:33 +00:00
|
|
|
|
}, [status, queryClient, video.youtube_video_id])
|
2026-09-16 18:56:17 +00:00
|
|
|
|
|
|
|
|
|
|
if (status === 'completed') {
|
2026-09-16 19:47:35 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<span className="download-inline-actions">
|
2026-09-16 21:40:32 +00:00
|
|
|
|
<span className="download-badge completed"><Icon name="check" size={15} /> На сервере</span>
|
|
|
|
|
|
{showDelete && <button className="button-quiet"
|
2026-09-16 19:47:35 +00:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (confirm('Удалить скачанную копию с сервера?')) {
|
|
|
|
|
|
deleteMutation.mutate()
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={deleteMutation.isPending}
|
2026-09-16 21:40:32 +00:00
|
|
|
|
>Удалить копию</button>}
|
2026-09-16 20:01:41 +00:00
|
|
|
|
{deleteMutation.isError && (
|
|
|
|
|
|
<span className="download-badge error" title={(deleteMutation.error as Error).message}>
|
|
|
|
|
|
{(deleteMutation.error as Error).message}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2026-09-16 19:47:35 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
)
|
2026-09-16 18:56:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status === 'downloading') {
|
2026-09-16 21:40:32 +00:00
|
|
|
|
return <span className="download-badge downloading"><span className="download-progress" style={{ width: `${Math.min(100, Math.max(0, percent ?? 0))}%` }} />Скачивается {percent ?? 0}%</span>
|
2026-09-16 18:56:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status === 'queued' || status === 'postprocessing') {
|
2026-09-16 21:40:32 +00:00
|
|
|
|
return <span className="download-badge downloading">{STATUS_LABELS[status]}</span>
|
2026-09-16 18:56:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (status === 'failed') {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="download-error">
|
2026-09-16 21:40:32 +00:00
|
|
|
|
<button className="button-secondary" onClick={() => downloadMutation.mutate()} disabled={downloadMutation.isPending}>
|
2026-09-16 18:56:17 +00:00
|
|
|
|
Повторить
|
|
|
|
|
|
</button>
|
2026-09-16 21:40:32 +00:00
|
|
|
|
{downloadMutation.isError && <span className="sr-only" role="alert">Не удалось повторить загрузку.</span>}
|
2026-09-16 18:56:17 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-16 21:40:32 +00:00
|
|
|
|
if (status === 'unknown') return <button className="button-secondary" onClick={() => downloadMutation.mutate()} disabled={downloadMutation.isPending}><Icon name="refresh" size={16} /> Загрузить снова</button>
|
|
|
|
|
|
|
2026-09-16 18:56:17 +00:00
|
|
|
|
return (
|
2026-09-16 21:40:32 +00:00
|
|
|
|
<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>
|
2026-09-16 18:56:17 +00:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default DownloadButton
|