/** * MyShowsPage - Two-panel file-explorer style view * Left panel: list of subscribed shows * Right panel: episodes for the selected show */ import { createSignal, For, Show, createMemo, createEffect } from "solid-js"; import { useFeedStore } from "@/stores/feed"; import { useDownloadStore } from "@/stores/download"; import { DownloadStatus } from "@/types/episode"; import { format } from "date-fns"; import { useTheme } from "@/context/ThemeContext"; import { useAudioNavStore, AudioSource } from "@/stores/audio-nav"; import { useNavigation } from "@/context/NavigationContext"; import { LoadingIndicator } from "@/components/LoadingIndicator"; enum MyShowsPaneType { SHOWS = 1, EPISODES = 2, } export const MyShowsPaneCount = 2; export function MyShowsPage() { const feedStore = useFeedStore(); const downloadStore = useDownloadStore(); const audioNav = useAudioNavStore(); const [isRefreshing, setIsRefreshing] = createSignal(false); const [showIndex, setShowIndex] = createSignal(0); const [episodeIndex, setEpisodeIndex] = createSignal(0); const { theme } = useTheme(); const mutedColor = () => theme.muted || theme.text; const nav = useNavigation(); /** Threshold: load more when within this many items of the end */ const LOAD_MORE_THRESHOLD = 5; const shows = () => feedStore.getFilteredFeeds(); const selectedShow = createMemo(() => { return shows()[0]; //TODO: Integrate with locally handled keyboard navigation }); const episodes = createMemo(() => { const show = selectedShow(); if (!show) return []; return [...show.episodes].sort( (a, b) => b.pubDate.getTime() - a.pubDate.getTime(), ); }); const formatDate = (date: Date): string => { return format(date, "MMM d, yyyy"); }; const formatDuration = (seconds: number): string => { const mins = Math.floor(seconds / 60); const hrs = Math.floor(mins / 60); if (hrs > 0) return `${hrs}h ${mins % 60}m`; return `${mins}m`; }; /** Get download status label for an episode */ const downloadLabel = (episodeId: string): string => { const status = downloadStore.getDownloadStatus(episodeId); switch (status) { case DownloadStatus.QUEUED: return "[Q]"; case DownloadStatus.DOWNLOADING: { const pct = downloadStore.getDownloadProgress(episodeId); return `[${pct}%]`; } case DownloadStatus.COMPLETED: return "[DL]"; case DownloadStatus.FAILED: return "[ERR]"; default: return ""; } }; const handleRefresh = async () => { const show = selectedShow(); if (!show) return; setIsRefreshing(true); await feedStore.refreshFeed(show.id); setIsRefreshing(false); }; const handleUnsubscribe = () => { const show = selectedShow(); if (!show) return; feedStore.removeFeed(show.id); setShowIndex((i) => Math.max(0, i - 1)); setEpisodeIndex(0); }; /** Get download status color */ const downloadColor = (episodeId: string): string => { const status = downloadStore.getDownloadStatus(episodeId); switch (status) { case DownloadStatus.QUEUED: return theme.warning.toString(); case DownloadStatus.DOWNLOADING: return theme.primary.toString(); case DownloadStatus.COMPLETED: return theme.success.toString(); case DownloadStatus.FAILED: return theme.error.toString(); default: return mutedColor().toString(); } }; return ( Refreshing... 0} fallback={ No shows yet. Subscribe from Discover or Search. } > {(feed, index) => ( { setShowIndex(index()); setEpisodeIndex(0); audioNav.setSource( AudioSource.MY_SHOWS, selectedShow()?.podcast.id, ); }} > {index() === showIndex() ? ">" : " "} {feed.customName || feed.podcast.title} ({feed.episodes.length}) )} Select a show } > 0} fallback={ No episodes. Press [r] to refresh. } > {(episode, index) => ( setEpisodeIndex(index())} > {index() === episodeIndex() ? ">" : " "} {episode.episodeNumber ? `#${episode.episodeNumber} ` : ""} {episode.title} {formatDate(episode.pubDate)} {formatDuration(episode.duration)} {downloadLabel(episode.id)} )} Scroll down for more episodes ); }