diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx index 13d66e6..13af27f 100644 --- a/src/frontend/src/pages/ChannelDetail.tsx +++ b/src/frontend/src/pages/ChannelDetail.tsx @@ -177,7 +177,7 @@ export function ChannelDetail() { const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showFullDescription, setShowFullDescription] = useState(false); const [scanInProgress, setScanInProgress] = useState(false); - const [expandedPlaylists, setExpandedPlaylists] = useState>(new Set()); + const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [selectedIds, setSelectedIds] = useState>(new Set()); const [localCheckInterval, setLocalCheckInterval] = useState(''); const [checkIntervalSaved, setCheckIntervalSaved] = useState(false); @@ -341,13 +341,18 @@ export function ChannelDetail() { try { localStorage.setItem('tubearr-group-by', value); } catch { /* ignore */ } }, []); + // Reset expanded groups when groupBy changes + useEffect(() => { + setExpandedGroups(new Set()); + }, [groupBy]); + const handleSetViewMode = useCallback((mode: 'table' | 'card' | 'list') => { setViewMode(mode); try { localStorage.setItem('tubearr-content-view', mode); } catch { /* ignore */ } }, []); - const togglePlaylist = useCallback((id: number | 'uncategorized') => { - setExpandedPlaylists((prev) => { + const toggleGroup = useCallback((id: string | number) => { + setExpandedGroups((prev) => { const next = new Set(prev); if (next.has(id)) { next.delete(id); @@ -450,6 +455,68 @@ export function ChannelDetail() { return groups.length > 0 ? groups : null; }, [channel, playlistData, content]); + // ── Unified group-by logic ── + + const groupedContent = useMemo<{ id: string | number; title: string; items: ContentItem[] }[] | null>(() => { + if (groupBy === 'none') return null; + + if (groupBy === 'playlist') { + // Delegate to existing playlist grouping for YouTube; null for others + return playlistGroups; + } + + if (groupBy === 'year') { + const yearMap = new Map(); + for (const item of content) { + const year = item.publishedAt ? new Date(item.publishedAt).getFullYear().toString() : 'Unknown'; + const arr = yearMap.get(year); + if (arr) { + arr.push(item); + } else { + yearMap.set(year, [item]); + } + } + // Sort groups by year descending, 'Unknown' last + const groups = Array.from(yearMap.entries()) + .sort((a, b) => { + if (a[0] === 'Unknown') return 1; + if (b[0] === 'Unknown') return -1; + return Number(b[0]) - Number(a[0]); + }) + .map(([year, items]) => ({ id: year, title: year, items })); + return groups.length > 0 ? groups : null; + } + + if (groupBy === 'type') { + const shorts: ContentItem[] = []; + const longform: ContentItem[] = []; + const livestream: ContentItem[] = []; + const audio: ContentItem[] = []; + + for (const item of content) { + if (item.contentType === 'livestream') { + livestream.push(item); + } else if (item.contentType === 'audio') { + audio.push(item); + } else if (item.contentType === 'video' && item.duration != null && item.duration <= 60) { + shorts.push(item); + } else { + // video with duration > 60, null duration, or any other video + longform.push(item); + } + } + + const groups: { id: string; title: string; items: ContentItem[] }[] = []; + if (shorts.length > 0) groups.push({ id: 'short', title: 'Short', items: shorts }); + if (longform.length > 0) groups.push({ id: 'longform', title: 'Longform', items: longform }); + if (livestream.length > 0) groups.push({ id: 'livestream', title: 'Livestream', items: livestream }); + if (audio.length > 0) groups.push({ id: 'audio', title: 'Audio', items: audio }); + return groups.length > 0 ? groups : null; + } + + return null; + }, [groupBy, content, playlistGroups]); + // ── Content table columns ── const contentColumns = useMemo[]>( @@ -742,15 +809,15 @@ export function ChannelDetail() { [selectedIds, toggleSelect, toggleMonitored, downloadContent], ); - const renderPlaylistGroups = useCallback( - (groups: { id: number | 'uncategorized'; title: string; items: ContentItem[] }[]) => ( + const renderGroupedContent = useCallback( + (groups: { id: string | number; title: string; items: ContentItem[] }[]) => (
{groups.map((group) => { - const isExpanded = expandedPlaylists.has(group.id); + const isExpanded = expandedGroups.has(group.id); return (
), - [expandedPlaylists, togglePlaylist, renderTable, renderCardGrid, renderListView, viewMode], + [expandedGroups, toggleGroup, renderTable, renderCardGrid, renderListView, viewMode], ); // ── Loading / Error states ── @@ -844,7 +911,6 @@ export function ChannelDetail() { } const isYouTube = channel.platform === 'youtube'; - const hasPlaylistGroups = isYouTube && playlistGroups !== null; return (
@@ -1498,8 +1564,8 @@ export function ChannelDetail() { ) : null} {contentLoading ? ( - ) : hasPlaylistGroups ? ( - renderPlaylistGroups(playlistGroups!) + ) : groupedContent ? ( + renderGroupedContent(groupedContent) ) : viewMode === 'card' ? ( renderCardGrid(content) ) : viewMode === 'list' ? (