feat: Wired ContentListItem into ChannelDetail with three-button segmen…
- "src/frontend/src/pages/ChannelDetail.tsx" GSD-Task: S03/T02
This commit is contained in:
parent
592c8b1317
commit
9fc15a3ed0
1 changed files with 113 additions and 24 deletions
|
|
@ -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)
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue