feat: Wired ContentListItem into ChannelDetail with three-button segmen…

- "src/frontend/src/pages/ChannelDetail.tsx"

GSD-Task: S03/T02
This commit is contained in:
jlightner 2026-04-03 06:42:34 +00:00
parent 592c8b1317
commit 9fc15a3ed0

View file

@ -12,6 +12,7 @@ import {
ExternalLink,
Film,
Grid3X3,
LayoutList,
List,
ListMusic,
Loader,
@ -33,6 +34,7 @@ import { QualityLabel } from '../components/QualityLabel';
import { DownloadProgressBar } from '../components/DownloadProgressBar';
import { SkeletonChannelHeader, SkeletonTable } from '../components/Skeleton';
import { ContentCard } from '../components/ContentCard';
import { ContentListItem } from '../components/ContentListItem';
import { Pagination } from '../components/Pagination';
import { Modal } from '../components/Modal';
import { useToast } from '../components/Toast';
@ -121,8 +123,12 @@ export function ChannelDetail() {
const [contentTypeFilter, setContentTypeFilter] = useState('');
const [sortKey, setSortKey] = useState<string | null>(null);
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
const [viewMode, setViewMode] = useState<'table' | 'card'>(() => {
try { return (localStorage.getItem('tubearr-content-view') as 'table' | 'card') || 'table'; }
const [viewMode, setViewMode] = useState<'table' | 'card' | 'list'>(() => {
try {
const stored = localStorage.getItem('tubearr-content-view');
if (stored === 'table' || stored === 'card' || stored === 'list') return stored;
return 'table';
}
catch { return 'table'; }
});
@ -312,12 +318,9 @@ export function ChannelDetail() {
setContentPage(1);
}, []);
const handleViewToggle = useCallback(() => {
setViewMode((prev) => {
const next = prev === 'table' ? 'card' : 'table';
try { localStorage.setItem('tubearr-content-view', next); } catch { /* ignore */ }
return next;
});
const handleSetViewMode = useCallback((mode: 'table' | 'card' | 'list') => {
setViewMode(mode);
try { localStorage.setItem('tubearr-content-view', mode); } catch { /* ignore */ }
}, []);
const togglePlaylist = useCallback((id: number | 'uncategorized') => {
@ -685,6 +688,37 @@ export function ChannelDetail() {
[selectedIds, toggleSelect, toggleMonitored, downloadContent],
);
const renderListView = useCallback(
(items: ContentItem[]) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--space-2)',
padding: 'var(--space-4)',
}}
>
{items.length === 0 ? (
<div style={{ padding: 'var(--space-8)', textAlign: 'center', color: 'var(--text-muted)' }}>
No content found for this channel.
</div>
) : (
items.map((item) => (
<ContentListItem
key={item.id}
item={item}
selected={selectedIds.has(item.id)}
onSelect={toggleSelect}
onToggleMonitored={(id, monitored) => toggleMonitored.mutate({ contentId: id, monitored })}
onDownload={(id) => downloadContent.mutate(id)}
/>
))
)}
</div>
),
[selectedIds, toggleSelect, toggleMonitored, downloadContent],
);
const renderPlaylistGroups = useCallback(
(groups: { id: number | 'uncategorized'; title: string; items: ContentItem[] }[]) => (
<div>
@ -726,13 +760,17 @@ export function ChannelDetail() {
{group.items.length}
</span>
</button>
{isExpanded ? renderTable(group.items) : null}
{isExpanded ? (
viewMode === 'card' ? renderCardGrid(group.items) :
viewMode === 'list' ? renderListView(group.items) :
renderTable(group.items)
) : null}
</div>
);
})}
</div>
),
[expandedPlaylists, togglePlaylist, renderTable],
[expandedPlaylists, togglePlaylist, renderTable, renderCardGrid, renderListView, viewMode],
);
// ── Loading / Error states ──
@ -1324,27 +1362,76 @@ export function ChannelDetail() {
<option value="audio">Audio</option>
<option value="livestream">Livestream</option>
</select>
{/* View toggle */}
<button
onClick={handleViewToggle}
title={viewMode === 'table' ? 'Switch to card view' : 'Switch to table view'}
aria-label={viewMode === 'table' ? 'Switch to card view' : 'Switch to table view'}
{/* View mode segmented control */}
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
borderRadius: 'var(--radius-md)',
backgroundColor: 'var(--bg-input)',
border: '1px solid var(--border-light)',
color: 'var(--text-secondary)',
transition: 'all var(--transition-fast)',
overflow: 'hidden',
flexShrink: 0,
}}
>
{viewMode === 'table' ? <Grid3X3 size={16} /> : <List size={16} />}
</button>
<button
onClick={() => handleSetViewMode('table')}
title="Table view"
aria-label="Table view"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
backgroundColor: viewMode === 'table' ? 'var(--accent)' : 'var(--bg-input)',
color: viewMode === 'table' ? '#fff' : 'var(--text-secondary)',
border: 'none',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
<List size={16} />
</button>
<button
onClick={() => handleSetViewMode('card')}
title="Card view"
aria-label="Card view"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
backgroundColor: viewMode === 'card' ? 'var(--accent)' : 'var(--bg-input)',
color: viewMode === 'card' ? '#fff' : 'var(--text-secondary)',
border: 'none',
borderLeft: '1px solid var(--border)',
borderRight: '1px solid var(--border)',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
<Grid3X3 size={16} />
</button>
<button
onClick={() => handleSetViewMode('list')}
title="List view"
aria-label="List view"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: 34,
height: 34,
backgroundColor: viewMode === 'list' ? 'var(--accent)' : 'var(--bg-input)',
color: viewMode === 'list' ? '#fff' : 'var(--text-secondary)',
border: 'none',
cursor: 'pointer',
transition: 'all var(--transition-fast)',
}}
>
<LayoutList size={16} />
</button>
</div>
</div>
{contentError ? (
<div
@ -1383,6 +1470,8 @@ export function ChannelDetail() {
renderPlaylistGroups(playlistGroups!)
) : viewMode === 'card' ? (
renderCardGrid(content)
) : viewMode === 'list' ? (
renderListView(content)
) : (
renderTable(content)
)}