From ab7ab3634bd12a95c7789778f8d17c3af97f6b46 Mon Sep 17 00:00:00 2001 From: jlightner Date: Fri, 3 Apr 2026 06:34:20 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Reordered=20Add=20Channel=20modal=20fie?= =?UTF-8?q?lds=20to=20URL=20=E2=86=92=20Monitoring=20Mode=20=E2=86=92=20Fo?= =?UTF-8?q?r=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "src/frontend/src/components/AddChannelModal.tsx" - "src/frontend/src/api/hooks/useChannels.ts" GSD-Task: S02/T01 --- src/frontend/src/api/hooks/useChannels.ts | 2 - .../src/components/AddChannelModal.tsx | 226 +++++++----------- src/frontend/src/components/Skeleton.tsx | 23 +- src/frontend/src/pages/ChannelDetail.tsx | 185 ++++++++++---- 4 files changed, 237 insertions(+), 199 deletions(-) diff --git a/src/frontend/src/api/hooks/useChannels.ts b/src/frontend/src/api/hooks/useChannels.ts index 51de309..4d8582c 100644 --- a/src/frontend/src/api/hooks/useChannels.ts +++ b/src/frontend/src/api/hooks/useChannels.ts @@ -39,8 +39,6 @@ interface CreateChannelInput { monitoringEnabled?: boolean; monitoringMode?: string; formatProfileId?: number; - grabAll?: boolean; - grabAllOrder?: 'newest' | 'oldest'; } /** Create a new channel by URL (resolves metadata via backend). */ diff --git a/src/frontend/src/components/AddChannelModal.tsx b/src/frontend/src/components/AddChannelModal.tsx index 7ffc163..61324be 100644 --- a/src/frontend/src/components/AddChannelModal.tsx +++ b/src/frontend/src/components/AddChannelModal.tsx @@ -51,8 +51,6 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { const [checkInterval, setCheckInterval] = useState(''); const [formatProfileId, setFormatProfileId] = useState(undefined); const [monitoringMode, setMonitoringMode] = useState('all'); - const [grabAll, setGrabAll] = useState(false); - const [grabAllOrder, setGrabAllOrder] = useState<'newest' | 'oldest'>('newest'); const createChannel = useCreateChannel(); const { data: platformSettingsList } = usePlatformSettings(); @@ -82,16 +80,6 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { if (settings.defaultMonitoringMode) { setMonitoringMode(settings.defaultMonitoringMode); } - - // Pre-fill grab-all defaults for YouTube - if (detectedPlatform === 'youtube') { - if (settings.grabAllEnabled) { - setGrabAll(true); - } - if (settings.grabAllOrder) { - setGrabAllOrder(settings.grabAllOrder); - } - } }, [detectedPlatform, platformSettingsList]); // eslint-disable-line react-hooks/exhaustive-deps const handleSubmit = (e: React.FormEvent) => { @@ -104,8 +92,6 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { checkInterval: checkInterval ? parseInt(checkInterval, 10) : undefined, monitoringMode, formatProfileId: formatProfileId ?? undefined, - grabAll: detectedPlatform === 'youtube' ? grabAll : undefined, - grabAllOrder: detectedPlatform === 'youtube' && grabAll ? grabAllOrder : undefined, }, { onSuccess: (newChannel) => { @@ -128,8 +114,6 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { setCheckInterval(''); setFormatProfileId(undefined); setMonitoringMode('all'); - setGrabAll(false); - setGrabAllOrder('newest'); createChannel.reset(); }; @@ -142,7 +126,29 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { return ( -
+ + {/* Loading overlay */} + {createChannel.isPending && ( +
+ + + Resolving channel… + +
+ )} {/* URL input */}
)} - {/* Monitoring Mode — shown when platform detected */} - {detectedPlatform && ( -
- - -
- )} - - {/* Grab All — YouTube only */} - {detectedPlatform === 'youtube' && ( - <> -
- setGrabAll(e.target.checked)} - disabled={createChannel.isPending} - style={{ width: 'auto' }} - /> - -
- - {/* Download order — shown when grab-all enabled */} - {grabAll && ( -
- - -

- Back-catalog items will be enqueued at low priority. -

-
- )} - - )} + {/* Check interval (optional) */} +
+ + setCheckInterval(e.target.value)} + placeholder="360 (default: 6 hours)" + disabled={createChannel.isPending} + style={{ width: '100%' }} + /> +
{/* Error display */} {createChannel.isError && ( @@ -362,9 +297,12 @@ export function AddChannelModal({ open, onClose }: AddChannelModalProps) { color: 'var(--danger)', }} > - {createChannel.error instanceof Error - ? createChannel.error.message - : 'Failed to add channel'} + {createChannel.error instanceof Error && + createChannel.error.message.toLowerCase().includes('already exists') + ? 'This channel has already been added.' + : createChannel.error instanceof Error + ? createChannel.error.message + : 'Failed to add channel'} )} diff --git a/src/frontend/src/components/Skeleton.tsx b/src/frontend/src/components/Skeleton.tsx index d1cad8a..4ac565d 100644 --- a/src/frontend/src/components/Skeleton.tsx +++ b/src/frontend/src/components/Skeleton.tsx @@ -62,20 +62,27 @@ export function SkeletonChannelHeader() { return (
- -
- - -
+ {/* Banner placeholder */} + + {/* Identity + controls */} +
+
+ +
+ + +
+
+ + +
diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx index b177535..e601e74 100644 --- a/src/frontend/src/pages/ChannelDetail.tsx +++ b/src/frontend/src/pages/ChannelDetail.tsx @@ -19,6 +19,7 @@ import { RefreshCw, Save, Trash2, + Users, } from 'lucide-react'; import { useChannel, useUpdateChannel, useDeleteChannel, useScanChannel, useSetMonitoringMode, useScanStatus } from '../api/hooks/useChannels'; import { useChannelContentPaginated, useDownloadContent, useToggleMonitored, useBulkMonitored, useCollectMonitored, type ChannelContentFilters } from '../api/hooks/useContent'; @@ -86,6 +87,14 @@ function formatRelativeTime(isoString: string | null): string { return `${years}y ago`; } +function formatSubscriberCount(count: number | null): string | null { + if (count == null) return null; + if (count < 1000) return `${count}`; + if (count < 1_000_000) return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}K`; + if (count < 1_000_000_000) return `${(count / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`; + return `${(count / 1_000_000_000).toFixed(1).replace(/\.0$/, '')}B`; +} + const MONITORING_MODE_OPTIONS: { value: MonitoringMode; label: string }[] = [ { value: 'all', label: 'All Content' }, { value: 'future', label: 'Future Only' }, @@ -144,6 +153,7 @@ export function ChannelDetail() { // ── Local state ── const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [showFullDescription, setShowFullDescription] = useState(false); const [scanInProgress, setScanInProgress] = useState(false); const [expandedPlaylists, setExpandedPlaylists] = useState>(new Set()); const [selectedIds, setSelectedIds] = useState>(new Set()); @@ -833,6 +843,11 @@ export function ChannelDetail() { {channel.name} + {channel.subscriberCount != null && ( + + {formatSubscriberCount(channel.subscriberCount)} subscribers + + )}
@@ -907,62 +922,141 @@ export function ChannelDetail() {
- {/* Identity row */} -
- {`${channel.name} -
-
-

- {channel.name} -

- -
- - {channel.url} - - -
-
+ ) : ( +
+ )} - {/* Control groups */} +
+ {/* Identity row — avatar overlaps banner */} +
+ {`${channel.name} +
+
+

+ {channel.name} +

+ + {channel.subscriberCount != null && ( + + + {formatSubscriberCount(channel.subscriberCount)} subscribers + + )} +
+ + {channel.url} + + +
+
+ + {/* Description — collapsible */} + {channel.description && ( +
+

150 + ? { overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' as const } + : {} + ), + }} + > + {channel.description} +

+ {channel.description.length > 150 && ( + + )} +
+ )}
+
{/* Content table / playlist groups */}