/** * Source management component for PodTUI * Add, remove, and configure podcast sources */ import { createSignal, For } from "solid-js"; import { useFeedStore } from "@/stores/feed"; import { useTheme } from "@/context/ThemeContext"; import { SourceType } from "@/types/source"; import type { PodcastSource } from "@/types/source"; import { SelectableBox, SelectableText } from "@/components/Selectable"; interface SourceManagerProps { focused?: boolean; onClose?: () => void; } type FocusArea = "list" | "add" | "url" | "country" | "explicit" | "language"; export function SourceManager(props: SourceManagerProps) { const feedStore = useFeedStore(); const { theme } = useTheme(); const [selectedIndex, setSelectedIndex] = createSignal(0); const [focusArea, setFocusArea] = createSignal("list"); const [newSourceUrl, setNewSourceUrl] = createSignal(""); const [newSourceName, setNewSourceName] = createSignal(""); const [error, setError] = createSignal(null); const sources = () => feedStore.sources(); const handleKeyPress = (key: { name: string; shift?: boolean }) => { if (key.name === "escape") { if (focusArea() !== "list") { setFocusArea("list"); setError(null); } else if (props.onClose) { props.onClose(); } return; } if (key.name === "tab") { const areas: FocusArea[] = [ "list", "country", "language", "explicit", "add", "url", ]; const idx = areas.indexOf(focusArea()); const nextIdx = key.shift ? (idx - 1 + areas.length) % areas.length : (idx + 1) % areas.length; setFocusArea(areas[nextIdx]); return; } if (focusArea() === "list") { 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(sources().length - 1, i + 1)); } else if ( key.name === "return" || key.name === "space" ) { const source = sources()[selectedIndex()]; if (source) { feedStore.toggleSource(source.id); } } else if (key.name === "d" || key.name === "delete") { const source = sources()[selectedIndex()]; if (source) { const removed = feedStore.removeSource(source.id); if (!removed) { setError("Cannot remove default sources"); } } } else if (key.name === "a") { setFocusArea("add"); } } if (focusArea() === "country") { if ( key.name === "enter" || key.name === "return" || key.name === "space" ) { const source = sources()[selectedIndex()]; if (source && source.type === SourceType.API) { const next = source.country === "US" ? "GB" : "US"; feedStore.updateSource(source.id, { country: next }); } } } if (focusArea() === "explicit") { if ( key.name === "return" || key.name === "space" ) { const source = sources()[selectedIndex()]; if (source && source.type === SourceType.API) { feedStore.updateSource(source.id, { allowExplicit: !source.allowExplicit, }); } } } if (focusArea() === "language") { if ( key.name === "return" || key.name === "space" ) { const source = sources()[selectedIndex()]; if (source && source.type === SourceType.API) { const next = source.language === "ja_jp" ? "en_us" : "ja_jp"; feedStore.updateSource(source.id, { language: next }); } } } }; const handleAddSource = () => { const url = newSourceUrl().trim(); const name = newSourceName().trim() || `Custom Source`; if (!url) { setError("URL is required"); return; } try { new URL(url); } catch { setError("Invalid URL format"); return; } feedStore.addSource({ name, type: "rss" as SourceType, baseUrl: url, enabled: true, description: `Custom RSS feed: ${url}`, }); setNewSourceUrl(""); setNewSourceName(""); setFocusArea("list"); setError(null); }; const getSourceIcon = (source: PodcastSource) => { if (source.type === SourceType.API) return "[API]"; if (source.type === SourceType.RSS) return "[RSS]"; return "[?]"; }; const selectedSource = () => sources()[selectedIndex()]; const isApiSource = () => selectedSource()?.type === SourceType.API; const sourceCountry = () => selectedSource()?.country || "US"; const sourceExplicit = () => selectedSource()?.allowExplicit !== false; const sourceLanguage = () => selectedSource()?.language || "en_us"; return ( Podcast Sources [Esc] Close Manage where to search for podcasts {/* Source list */} Sources: {(source, index) => ( focusArea() === "list" && index() === selectedIndex()} flexDirection="row" gap={1} padding={0} onMouseDown={() => { setSelectedIndex(index()); setFocusArea("list"); feedStore.toggleSource(source.id); }} > focusArea() === "list" && index() === selectedIndex()} primary > {focusArea() === "list" && index() === selectedIndex() ? ">" : " "} focusArea() === "list" && index() === selectedIndex()} primary > {source.name} )} Space/Enter to toggle, d to delete, a to add {/* API settings */} false} primary={isApiSource()}> {isApiSource() ? "API Settings" : "API Settings (select an API source)"} false} primary={focusArea() === "country"}> Country: {sourceCountry()} false} primary={focusArea() === "language"}> Language:{" "} {sourceLanguage() === "ja_jp" ? "Japanese" : "English"} false} primary={focusArea() === "explicit"}> Explicit: {sourceExplicit() ? "Yes" : "No"} false} tertiary> Enter/Space to toggle focused setting {/* Add new source form */} false} primary={focusArea() === "add" || focusArea() === "url"}> Add New Source: false} tertiary>Name: false} tertiary>URL: { setNewSourceUrl(v); setError(null); }} placeholder="https://example.com/feed.rss" focused={props.focused && focusArea() === "url"} width={35} /> false} primary>[+] Add Source {/* Error message */} {error() && false} tertiary>{error()}} false} tertiary>Tab to switch sections, Esc to close ); }