/** * Feed list component for PodTUI * Scrollable list of feeds with keyboard navigation and mouse support */ import { createSignal, For, Show } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import { FeedItem } from "./FeedItem"; import { useFeedStore } from "@/stores/feed"; import { FeedVisibility, FeedSortField } from "@/types/feed"; import type { Feed } from "@/types/feed"; interface FeedListProps { focused?: boolean; compact?: boolean; showEpisodeCount?: boolean; showLastUpdated?: boolean; onSelectFeed?: (feed: Feed) => void; onOpenFeed?: (feed: Feed) => void; onFocusChange?: (focused: boolean) => void; } export function FeedList(props: FeedListProps) { const feedStore = useFeedStore(); const [selectedIndex, setSelectedIndex] = createSignal(0); const filteredFeeds = () => feedStore.getFilteredFeeds(); const handleKeyPress = (key: { name: string }) => { if (key.name === "escape") { props.onFocusChange?.(false); return; } const feeds = filteredFeeds(); if (key.name === "up" || key.name === "k") { setSelectedIndex((i) => Math.max(0, i - 1)); } else if (key.name === "down" || key.name === "j") { setSelectedIndex((i) => Math.min(feeds.length - 1, i + 1)); } else if (key.name === "return" || key.name === "enter") { const feed = feeds[selectedIndex()]; if (feed && props.onOpenFeed) { props.onOpenFeed(feed); } } else if (key.name === "home" || key.name === "g") { setSelectedIndex(0); } else if (key.name === "end") { setSelectedIndex(feeds.length - 1); } else if (key.name === "pageup") { setSelectedIndex((i) => Math.max(0, i - 5)); } else if (key.name === "pagedown") { setSelectedIndex((i) => Math.min(feeds.length - 1, i + 5)); } else if (key.name === "p") { // Toggle pin on selected feed const feed = feeds[selectedIndex()]; if (feed) { feedStore.togglePinned(feed.id); } } else if (key.name === "f") { // Cycle visibility filter cycleVisibilityFilter(); } else if (key.name === "s") { // Cycle sort cycleSortField(); } // Notify selection change const selectedFeed = feeds[selectedIndex()]; if (selectedFeed && props.onSelectFeed) { props.onSelectFeed(selectedFeed); } }; useKeyboard((key) => { if (!props.focused) return; handleKeyPress(key); }); const cycleVisibilityFilter = () => { const current = feedStore.filter().visibility; let next: FeedVisibility | "all"; if (current === "all") next = FeedVisibility.PUBLIC; else if (current === FeedVisibility.PUBLIC) next = FeedVisibility.PRIVATE; else next = "all"; feedStore.setFilter({ ...feedStore.filter(), visibility: next }); }; const cycleSortField = () => { const sortOptions: FeedSortField[] = [ FeedSortField.UPDATED, FeedSortField.TITLE, FeedSortField.EPISODE_COUNT, FeedSortField.LATEST_EPISODE, ]; const current = feedStore.filter().sortBy as FeedSortField; const idx = sortOptions.indexOf(current); const next = sortOptions[(idx + 1) % sortOptions.length]; feedStore.setFilter({ ...feedStore.filter(), sortBy: next }); }; const visibilityLabel = () => { const vis = feedStore.filter().visibility; if (vis === "all") return "All"; if (vis === "public") return "Public"; return "Private"; }; const sortLabel = () => { const sort = feedStore.filter().sortBy; switch (sort) { case "title": return "Title"; case "episodeCount": return "Episodes"; case "latestEpisode": return "Latest"; default: return "Updated"; } }; const handleFeedClick = (feed: Feed, index: number) => { setSelectedIndex(index); if (props.onSelectFeed) { props.onSelectFeed(feed); } }; const handleFeedDoubleClick = (feed: Feed) => { if (props.onOpenFeed) { props.onOpenFeed(feed); } }; return ( {/* Header with filter controls */} My Feeds ({filteredFeeds().length} feeds) [f] {visibilityLabel()} [s] {sortLabel()} {/* Feed list in scrollbox */} 0} fallback={ No feeds found. Add podcasts from the Discover or Search tabs. } > {(feed, index) => ( handleFeedClick(feed, index())}> )} {/* Navigation help */} Enter open | Esc up | j/k navigate | p pin | f filter | s sort ); }