diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx index 215945c..d189bf5 100644 --- a/src/frontend/src/pages/ChannelDetail.tsx +++ b/src/frontend/src/pages/ChannelDetail.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { useParams, useNavigate, Link } from 'react-router-dom'; import { ArrowLeft, @@ -7,6 +7,7 @@ import { CheckCircle, ChevronDown, ChevronRight, + ChevronUp, Download, ExternalLink, Film, @@ -17,7 +18,6 @@ import { Music, RefreshCw, Save, - Search, Trash2, } from 'lucide-react'; import { useChannel, useUpdateChannel, useDeleteChannel, useScanChannel, useSetMonitoringMode, useScanStatus } from '../api/hooks/useChannels'; @@ -151,6 +151,24 @@ export function ChannelDetail() { const [checkIntervalSaved, setCheckIntervalSaved] = useState(false); const { toast } = useToast(); + // ── Collapsible header ── + const headerRef = useRef(null); + const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(false); + + useEffect(() => { + const el = headerRef.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + // Collapse when the header is mostly out of view + setIsHeaderCollapsed(!entry.isIntersecting); + }, + { threshold: 0, rootMargin: '-60px 0px 0px 0px' }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + // Sync local check interval from channel data useEffect(() => { if (channel?.checkInterval != null) { @@ -774,248 +792,347 @@ export function ChannelDetail() { Back to Channels - {/* Channel header */} + {/* Compact sticky bar — visible when full header scrolls out of view */}
- {/* Avatar */} + {/* Identity — compact */} {`${channel.name} + + {channel.name} + + - {/* Info */} -
-
-

+ + {/* Key actions — compact */} + + + + + + +
+ + {/* Scroll-to-top to reveal full header */} + +
+ + {/* Full channel header — observed for collapse trigger */} +
+ {/* Identity row */} +
+ {`${channel.name} +
+
+

+ {channel.name} +

+ +
+ - {channel.name} -

- + {channel.url} + + +
+
+ + {/* Control groups */} +
+ {/* Monitoring group */} +
+ + Monitoring + +
+ +
+ setLocalCheckInterval(e.target.value === '' ? '' : Number(e.target.value))} + aria-label="Check interval in minutes" + title="Check interval (minutes)" + style={{ + width: 56, + padding: 'var(--space-2)', + borderRadius: 'var(--radius-md)', + border: '1px solid var(--border)', + backgroundColor: 'var(--bg-main)', + color: 'var(--text-primary)', + fontSize: 'var(--font-size-sm)', + }} + /> + min + +
+
- - - {channel.url} + {/* Format group */} +
+ + Format - - - - {/* Actions row */} -
- {/* Monitoring mode dropdown */} - - - {/* Format profile selector */} +
- {/* Per-channel check interval */} -
- setLocalCheckInterval(e.target.value === '' ? '' : Number(e.target.value))} - aria-label="Check interval in minutes" - title="Check interval (minutes)" - style={{ - width: 64, - padding: 'var(--space-2) var(--space-2)', - borderRadius: 'var(--radius-md)', - border: '1px solid var(--border)', - backgroundColor: 'var(--bg-main)', - color: 'var(--text-primary)', - fontSize: 'var(--font-size-sm)', - }} - /> - min + {/* Actions group */} +
+ + Actions + +
-
- - {/* Refresh & Scan button */} - - - {/* Collect Monitored button */} - - - {/* Refresh Playlists button (YouTube only) */} - {isYouTube ? ( - - ) : null} + + {isYouTube ? ( + + ) : null} +
+
- {/* Delete button */} + {/* Spacer + Delete */} +