import { useEffect, useMemo, useRef, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { CheckSquare, Download, Film, Loader2, Square, Video, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../api/client'; import { useToast } from '../contexts/ToastContext'; import { formatFileSize } from '../utils/file'; import { Button } from './Button'; interface ArchiveMediaDownloadModalProps { archiveId: number; archiveName: string; printerName: string; onClose: () => void; } export function ArchiveMediaDownloadModal({ archiveId, archiveName, printerName, onClose, }: ArchiveMediaDownloadModalProps) { const { t } = useTranslation(); const { showToast } = useToast(); const [selectedPaths, setSelectedPaths] = useState>(new Set()); const [downloadStarting, setDownloadStarting] = useState(false); const [downloadProgress, setDownloadProgress] = useState<{ current: number; total: number } | null>(null); const initializedSelectionArchiveRef = useRef(null); const downloadAbortRef = useRef(null); const mediaQuery = useQuery({ queryKey: ['archive-printer-media', archiveId], queryFn: () => api.getArchivePrinterMedia(archiveId), staleTime: 60_000, }); const remoteFiles = useMemo(() => mediaQuery.data?.remote_files ?? [], [mediaQuery.data]); useEffect(() => { if (!mediaQuery.data || initializedSelectionArchiveRef.current === archiveId) return; initializedSelectionArchiveRef.current = archiveId; if (remoteFiles.length === 1) { setSelectedPaths(new Set([remoteFiles[0].path])); } }, [archiveId, mediaQuery.data, remoteFiles]); useEffect(() => { const availablePaths = new Set(remoteFiles.map(file => file.path)); setSelectedPaths(current => new Set([...current].filter(path => availablePaths.has(path)))); }, [remoteFiles]); useEffect(() => () => downloadAbortRef.current?.abort(), []); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); const togglePath = (path: string) => { setSelectedPaths((current) => { const next = new Set(current); if (next.has(path)) next.delete(path); else next.add(path); return next; }); }; const downloadLocalTimelapse = async () => { const sourceName = mediaQuery.data?.local_timelapse?.name ?? ''; const extension = sourceName.includes('.') ? `.${sourceName.split('.').pop()}` : ''; try { await api.downloadArchiveTimelapse(archiveId, `${archiveName}_timelapse${extension}`); } catch (error) { showToast(t('printerFiles.downloadFailed', { error: error instanceof Error ? error.message : t('printerFiles.unknownError'), }), 'error'); } }; const downloadSelected = async () => { if (!mediaQuery.data?.printer_id || selectedPaths.size === 0) return; const selectedFiles = remoteFiles.filter(file => selectedPaths.has(file.path)); if (selectedFiles.length === 0) return; const controller = new AbortController(); downloadAbortRef.current = controller; setDownloadStarting(true); try { const result = await api.downloadPrinterFilesAsZip( mediaQuery.data.printer_id, selectedFiles.map(file => file.path), Object.fromEntries(selectedFiles.map(file => [file.path, file.size])), `${archiveName.replace(/[^a-zA-Z0-9]/g, '_')}-printer-videos.zip`, true, controller.signal, (completed, total) => setDownloadProgress({ current: completed, total }), ); if (result.failed > 0) { showToast(t('printerFiles.zipPartial', { successful: result.successful, total: result.requested, }), 'warning'); } else { showToast(t('printerFiles.zipStarted', { count: result.successful })); } } catch (error) { showToast(t('printerFiles.downloadFailed', { error: error instanceof Error ? error.message : t('printerFiles.unknownError'), }), 'error'); } finally { if (downloadAbortRef.current === controller) downloadAbortRef.current = null; setDownloadProgress(null); setDownloadStarting(false); } }; const hasMedia = !!mediaQuery.data?.local_timelapse || remoteFiles.length > 0; const warningText = (warning: string) => { if (warning === 'printer_files_forbidden') return t('printers.permission.noFiles'); if (warning === 'printer_missing') return t('archives.media.printerMissing'); if (warning === 'timelapse_unavailable') return t('archives.media.timelapseUnavailable'); return t('archives.media.ipcamUnavailable'); }; const warnings = (mediaQuery.data?.warnings ?? []).map((warning) => (

{warningText(warning)}

)); return (
event.stopPropagation()} >

{archiveName} · {printerName}

{mediaQuery.isLoading ? (
{t('archives.media.searching')}
) : mediaQuery.isError ? (

{t('archives.media.searchFailed')}

) : !hasMedia ? (

{t('archives.media.none')}

{warnings}
) : (
{mediaQuery.data?.local_timelapse && (

{mediaQuery.data.local_timelapse.name}

{t('archives.media.attachedTimelapse')} · {formatFileSize(mediaQuery.data.local_timelapse.size)}

)} {remoteFiles.length > 0 && (

{t('archives.media.printerFiles')} ({remoteFiles.length})

{remoteFiles.map((file) => { const selected = selectedPaths.has(file.path); return ( ); })}
)} {warnings}
)}
{remoteFiles.length > 0 && (
)}
); }