diff --git a/src/frontend/src/api/hooks/useAdhocDownload.ts b/src/frontend/src/api/hooks/useAdhocDownload.ts new file mode 100644 index 0000000..e4ef821 --- /dev/null +++ b/src/frontend/src/api/hooks/useAdhocDownload.ts @@ -0,0 +1,52 @@ +import { useMutation } from '@tanstack/react-query'; +import { apiClient } from '../client'; +import type { ContentType } from '@shared/types/index'; + +// ── Types ── + +export interface UrlPreviewResponse { + title: string; + thumbnail: string | null; + duration: number | null; + platform: string; + channelName: string | null; + contentType: ContentType; + platformContentId: string; + url: string; +} + +interface ConfirmRequest { + url: string; + title: string; + platform: string; + platformContentId: string; + contentType: string; + channelName?: string; + duration?: number | null; + thumbnailUrl?: string | null; + formatProfileId?: number; +} + +interface ConfirmResponse { + contentItemId: number; + queueItemId: number; + status: string; +} + +// ── Mutations ── + +/** Resolve URL metadata via yt-dlp (preview step). */ +export function useUrlPreview() { + return useMutation({ + mutationFn: (url: string) => + apiClient.post('/api/v1/download/url/preview', { url }), + }); +} + +/** Confirm ad-hoc download — creates content item and enqueues. */ +export function useUrlConfirm() { + return useMutation({ + mutationFn: (data: ConfirmRequest) => + apiClient.post('/api/v1/download/url/confirm', data), + }); +} diff --git a/src/frontend/src/components/AddUrlModal.tsx b/src/frontend/src/components/AddUrlModal.tsx new file mode 100644 index 0000000..8dd56b3 --- /dev/null +++ b/src/frontend/src/components/AddUrlModal.tsx @@ -0,0 +1,425 @@ +import { useState, useCallback } from 'react'; +import { Modal } from './Modal'; +import { useUrlPreview, useUrlConfirm } from '../api/hooks/useAdhocDownload'; +import { useFormatProfiles } from '../api/hooks/useFormatProfiles'; +import { useToast } from './Toast'; +import { Loader, Download, Clock, Film, Music, Radio } from 'lucide-react'; +import type { UrlPreviewResponse } from '../api/hooks/useAdhocDownload'; + +// ── Helpers ── + +function formatDuration(seconds: number | null): string { + if (seconds === null || seconds <= 0) return '—'; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; + return `${m}:${String(s).padStart(2, '0')}`; +} + +const PLATFORM_LABELS: Record = { + youtube: 'YouTube', + soundcloud: 'SoundCloud', + generic: 'Generic', +}; + +function ContentTypeIcon({ type }: { type: string }) { + switch (type) { + case 'video': + return