diff --git a/src/frontend/src/components/SortGroupBar.tsx b/src/frontend/src/components/SortGroupBar.tsx
new file mode 100644
index 0000000..e452ee8
--- /dev/null
+++ b/src/frontend/src/components/SortGroupBar.tsx
@@ -0,0 +1,155 @@
+import { ArrowDown, ArrowUp } from 'lucide-react';
+
+export type SortKey = 'publishedAt' | 'title' | 'duration' | 'fileSize' | 'status';
+export type GroupByKey = 'none' | 'playlist' | 'year' | 'type';
+
+interface SortButton {
+ key: SortKey;
+ label: string;
+}
+
+const SORT_BUTTONS: SortButton[] = [
+ { key: 'publishedAt', label: 'Date' },
+ { key: 'title', label: 'Title' },
+ { key: 'duration', label: 'Duration' },
+ { key: 'fileSize', label: 'Size' },
+ { key: 'status', label: 'Status' },
+];
+
+const GROUP_BY_OPTIONS: { value: GroupByKey; label: string; youtubeOnly?: boolean }[] = [
+ { value: 'none', label: 'No Grouping' },
+ { value: 'playlist', label: 'Playlist', youtubeOnly: true },
+ { value: 'year', label: 'Year' },
+ { value: 'type', label: 'Type' },
+];
+
+interface SortGroupBarProps {
+ sortKey: string | null;
+ sortDirection: 'asc' | 'desc';
+ onSort: (key: string, direction: 'asc' | 'desc') => void;
+ groupBy: GroupByKey;
+ onGroupByChange: (groupBy: GroupByKey) => void;
+ isYouTube: boolean;
+}
+
+export function SortGroupBar({
+ sortKey,
+ sortDirection,
+ onSort,
+ groupBy,
+ onGroupByChange,
+ isYouTube,
+}: SortGroupBarProps) {
+ const handleSortClick = (key: SortKey) => {
+ if (sortKey === key) {
+ // Toggle direction
+ onSort(key, sortDirection === 'asc' ? 'desc' : 'asc');
+ } else {
+ // New sort key — default to descending
+ onSort(key, 'desc');
+ }
+ };
+
+ return (
+
+ {/* Sort label */}
+
+ Sort
+
+
+ {/* Sort buttons */}
+ {SORT_BUTTONS.map((btn) => {
+ const isActive = sortKey === btn.key;
+ return (
+
+ );
+ })}
+
+ {/* Spacer */}
+
+
+ {/* Group by */}
+
+ Group
+
+
+
+ );
+}
diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx
index ccb0c2e..13d66e6 100644
--- a/src/frontend/src/pages/ChannelDetail.tsx
+++ b/src/frontend/src/pages/ChannelDetail.tsx
@@ -36,6 +36,7 @@ import { SkeletonChannelHeader, SkeletonTable } from '../components/Skeleton';
import { ContentCard } from '../components/ContentCard';
import { ContentListItem } from '../components/ContentListItem';
import { Pagination } from '../components/Pagination';
+import { SortGroupBar, type GroupByKey } from '../components/SortGroupBar';
import { Modal } from '../components/Modal';
import { useToast } from '../components/Toast';
import { useDownloadProgress } from '../contexts/DownloadProgressContext';
@@ -121,8 +122,23 @@ export function ChannelDetail() {
const [contentSearch, setContentSearch] = useState('');
const [contentStatusFilter, setContentStatusFilter] = useState('');
const [contentTypeFilter, setContentTypeFilter] = useState('');
- const [sortKey, setSortKey] = useState(null);
- const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
+ const [sortKey, setSortKey] = useState(() => {
+ try { return localStorage.getItem('tubearr-sort-key') || null; } catch { return null; }
+ });
+ const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(() => {
+ try {
+ const stored = localStorage.getItem('tubearr-sort-dir');
+ if (stored === 'asc' || stored === 'desc') return stored;
+ return 'asc';
+ } catch { return 'asc'; }
+ });
+ const [groupBy, setGroupBy] = useState(() => {
+ try {
+ const stored = localStorage.getItem('tubearr-group-by');
+ if (stored === 'none' || stored === 'playlist' || stored === 'year' || stored === 'type') return stored;
+ return 'none';
+ } catch { return 'none'; }
+ });
const [viewMode, setViewMode] = useState<'table' | 'card' | 'list'>(() => {
try {
const stored = localStorage.getItem('tubearr-content-view');
@@ -316,6 +332,13 @@ export function ChannelDetail() {
setSortKey(key);
setSortDirection(direction);
setContentPage(1);
+ try { localStorage.setItem('tubearr-sort-key', key); } catch { /* ignore */ }
+ try { localStorage.setItem('tubearr-sort-dir', direction); } catch { /* ignore */ }
+ }, []);
+
+ const handleGroupByChange = useCallback((value: GroupByKey) => {
+ setGroupBy(value);
+ try { localStorage.setItem('tubearr-group-by', value); } catch { /* ignore */ }
}, []);
const handleSetViewMode = useCallback((mode: 'table' | 'card' | 'list') => {
@@ -1433,6 +1456,15 @@ export function ChannelDetail() {
+ {/* Sort & Group controls */}
+
{contentError ? (