final feature set

This commit is contained in:
2026-02-05 22:55:24 -05:00
parent 6b00871c32
commit 168e6d5a61
115 changed files with 2401 additions and 4468 deletions

View File

@@ -1,4 +1,5 @@
import { createSignal, ErrorBoundary } from "solid-js";
import { useSelectionHandler } from "@opentui/solid";
import { Layout } from "./components/Layout";
import { Navigation } from "./components/Navigation";
import { TabNavigation } from "./components/TabNavigation";
@@ -18,6 +19,8 @@ import { useAppStore } from "./stores/app";
import { useAudio } from "./hooks/useAudio";
import { FeedVisibility } from "./types/feed";
import { useAppKeyboard } from "./hooks/useAppKeyboard";
import { Clipboard } from "./utils/clipboard";
import { emit } from "./utils/event-bus";
import type { TabId } from "./components/Tab";
import type { AuthScreen } from "./types/auth";
import type { Episode } from "./types/episode";
@@ -80,6 +83,21 @@ export function App() {
},
});
// Copy selected text to clipboard when selection ends (mouse release)
useSelectionHandler((selection: any) => {
if (!selection) return
const text = selection.getSelectedText?.()
if (!text || text.trim().length === 0) return
Clipboard.copy(text).then(() => {
emit("toast.show", {
message: "Copied to clipboard",
variant: "info",
duration: 1500,
})
}).catch(() => {})
})
const getPanels = () => {
const tab = activeTab();

View File

@@ -1,5 +1,7 @@
import type { Podcast } from "../types/podcast"
import type { Episode, EpisodeType } from "../types/episode"
import { detectContentType, ContentType } from "../utils/rss-content-detector"
import { htmlToText } from "../utils/html-to-text"
const getTagValue = (xml: string, tag: string): string => {
const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"))
@@ -22,6 +24,20 @@ const decodeEntities = (value: string) =>
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
/**
* Clean a description field: detect HTML vs plain text, and convert
* HTML to readable plain text. Plain text just gets entity decoding.
*/
const cleanDescription = (raw: string): string => {
if (!raw) return ""
const decoded = decodeEntities(raw)
const type = detectContentType(decoded)
if (type === ContentType.HTML) {
return htmlToText(decoded)
}
return decoded
}
/**
* Parse an itunes:duration value which can be:
* - "HH:MM:SS"
@@ -61,14 +77,14 @@ const parseEpisodeType = (raw: string): EpisodeType | undefined => {
export const parseRSSFeed = (xml: string, feedUrl: string): Podcast & { episodes: Episode[] } => {
const channel = xml.match(/<channel[\s\S]*?<\/channel>/i)?.[0] ?? xml
const title = decodeEntities(getTagValue(channel, "title")) || "Untitled Podcast"
const description = decodeEntities(getTagValue(channel, "description"))
const description = cleanDescription(getTagValue(channel, "description"))
const author = decodeEntities(getTagValue(channel, "itunes:author"))
const lastUpdated = new Date()
const items = channel.match(/<item[\s\S]*?<\/item>/gi) ?? []
const episodes = items.map((item, index) => {
const epTitle = decodeEntities(getTagValue(item, "title")) || `Episode ${index + 1}`
const epDescription = decodeEntities(getTagValue(item, "description"))
const epDescription = cleanDescription(getTagValue(item, "description"))
const pubDate = new Date(getTagValue(item, "pubDate") || Date.now())
// Audio URL + file size + MIME type from <enclosure>

View File

@@ -3,8 +3,11 @@ import { DEFAULT_THEME, THEME_JSON } from "../constants/themes"
import type { AppSettings, AppState, ThemeColors, ThemeName, ThemeMode, UserPreferences } from "../types/settings"
import { resolveTheme } from "../utils/theme-resolver"
import type { ThemeJson } from "../types/theme-schema"
const STORAGE_KEY = "podtui_app_state"
import {
loadAppStateFromFile,
saveAppStateToFile,
migrateAppStateFromLocalStorage,
} from "../utils/app-persistence"
const defaultSettings: AppSettings = {
theme: "system",
@@ -24,33 +27,21 @@ const defaultState: AppState = {
customTheme: DEFAULT_THEME,
}
const loadState = (): AppState => {
if (typeof localStorage === "undefined") return defaultState
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return defaultState
const parsed = JSON.parse(raw) as Partial<AppState>
return {
settings: { ...defaultSettings, ...parsed.settings },
preferences: { ...defaultPreferences, ...parsed.preferences },
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
}
} catch {
return defaultState
}
}
const saveState = (state: AppState) => {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} catch {
// ignore storage errors
}
}
export function createAppStore() {
const [state, setState] = createSignal<AppState>(loadState())
// Start with defaults; async load will update once ready
const [state, setState] = createSignal<AppState>(defaultState)
// Fire-and-forget async initialisation
const init = async () => {
await migrateAppStateFromLocalStorage()
const loaded = await loadAppStateFromFile()
setState(loaded)
}
init()
const saveState = (next: AppState) => {
saveAppStateToFile(next).catch(() => {})
}
const updateState = (next: AppState) => {
setState(next)

View File

@@ -11,6 +11,14 @@ import type { Episode, EpisodeStatus } from "../types/episode"
import type { PodcastSource, SourceType } from "../types/source"
import { DEFAULT_SOURCES } from "../types/source"
import { parseRSSFeed } from "../api/rss-parser"
import {
loadFeedsFromFile,
saveFeedsToFile,
loadSourcesFromFile,
saveSourcesToFile,
migrateFeedsFromLocalStorage,
migrateSourcesFromLocalStorage,
} from "../utils/feeds-persistence"
/** Max episodes to fetch on refresh */
const MAX_EPISODES_REFRESH = 50
@@ -18,85 +26,30 @@ const MAX_EPISODES_REFRESH = 50
/** Max episodes to fetch on initial subscribe */
const MAX_EPISODES_SUBSCRIBE = 20
/** Storage keys */
const STORAGE_KEYS = {
feeds: "podtui_feeds",
sources: "podtui_sources",
}
/** Load feeds from localStorage */
function loadFeeds(): Feed[] {
if (typeof localStorage === "undefined") {
return []
}
try {
const stored = localStorage.getItem(STORAGE_KEYS.feeds)
if (stored) {
const parsed = JSON.parse(stored)
// Convert date strings
return parsed.map((feed: Feed) => ({
...feed,
lastUpdated: new Date(feed.lastUpdated),
podcast: {
...feed.podcast,
lastUpdated: new Date(feed.podcast.lastUpdated),
},
episodes: feed.episodes.map((ep: Episode) => ({
...ep,
pubDate: new Date(ep.pubDate),
})),
}))
}
} catch {
// Ignore errors
}
return []
}
/** Save feeds to localStorage */
/** Save feeds to file (async, fire-and-forget) */
function saveFeeds(feeds: Feed[]): void {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEYS.feeds, JSON.stringify(feeds))
} catch {
// Ignore errors
}
saveFeedsToFile(feeds).catch(() => {})
}
/** Load sources from localStorage */
function loadSources(): PodcastSource[] {
if (typeof localStorage === "undefined") {
return [...DEFAULT_SOURCES]
}
try {
const stored = localStorage.getItem(STORAGE_KEYS.sources)
if (stored) {
return JSON.parse(stored)
}
} catch {
// Ignore errors
}
return [...DEFAULT_SOURCES]
}
/** Save sources to localStorage */
/** Save sources to file (async, fire-and-forget) */
function saveSources(sources: PodcastSource[]): void {
if (typeof localStorage === "undefined") return
try {
localStorage.setItem(STORAGE_KEYS.sources, JSON.stringify(sources))
} catch {
// Ignore errors
}
saveSourcesToFile(sources).catch(() => {})
}
/** Create feed store */
export function createFeedStore() {
const [feeds, setFeeds] = createSignal<Feed[]>(loadFeeds())
const [sources, setSources] = createSignal<PodcastSource[]>(loadSources())
const [feeds, setFeeds] = createSignal<Feed[]>([])
const [sources, setSources] = createSignal<PodcastSource[]>([...DEFAULT_SOURCES])
// Async initialization: migrate from localStorage, then load from file
;(async () => {
await migrateFeedsFromLocalStorage()
await migrateSourcesFromLocalStorage()
const loadedFeeds = await loadFeedsFromFile()
if (loadedFeeds.length > 0) setFeeds(loadedFeeds)
const loadedSources = await loadSourcesFromFile<PodcastSource>()
if (loadedSources && loadedSources.length > 0) setSources(loadedSources)
})()
const [filter, setFilter] = createSignal<FeedFilter>({
visibility: "all",
sortBy: "updated" as FeedSortField,

View File

@@ -1,14 +1,17 @@
/**
* Episode progress store for PodTUI
*
* Persists per-episode playback progress to localStorage.
* Persists per-episode playback progress to a JSON file in XDG_CONFIG_HOME.
* Tracks position, duration, completion, and last-played timestamp.
*/
import { createSignal } from "solid-js"
import type { Progress } from "../types/episode"
const STORAGE_KEY = "podtui_progress"
import {
loadProgressFromFile,
saveProgressToFile,
migrateProgressFromLocalStorage,
} from "../utils/app-persistence"
/** Threshold (fraction 0-1) at which an episode is considered completed */
const COMPLETION_THRESHOLD = 0.95
@@ -16,15 +19,19 @@ const COMPLETION_THRESHOLD = 0.95
/** Minimum seconds of progress before persisting */
const MIN_POSITION_TO_SAVE = 5
// --- localStorage helpers ---
// --- Singleton store ---
function loadProgress(): Record<string, Progress> {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return {}
const parsed = JSON.parse(raw) as Record<string, unknown>
const [progressMap, setProgressMap] = createSignal<Record<string, Progress>>({})
/** Persist current progress map to file (fire-and-forget) */
function persist(): void {
saveProgressToFile(progressMap()).catch(() => {})
}
/** Parse raw progress entries from file, reviving Date objects */
function parseProgressEntries(raw: Record<string, unknown>): Record<string, Progress> {
const result: Record<string, Progress> = {}
for (const [key, value] of Object.entries(parsed)) {
for (const [key, value] of Object.entries(raw)) {
const p = value as Record<string, unknown>
result[key] = {
episodeId: p.episodeId as string,
@@ -35,28 +42,18 @@ function loadProgress(): Record<string, Progress> {
}
}
return result
} catch {
return {}
}
}
function saveProgress(data: Record<string, Progress>): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
} catch {
// Quota exceeded or unavailable — silently ignore
}
/** Async initialisation — migrate from localStorage then load from file */
async function initProgress(): Promise<void> {
await migrateProgressFromLocalStorage()
const raw = await loadProgressFromFile()
const parsed = parseProgressEntries(raw as Record<string, unknown>)
setProgressMap(parsed)
}
// --- Singleton store ---
const [progressMap, setProgressMap] = createSignal<Record<string, Progress>>(
loadProgress(),
)
function persist(): void {
saveProgress(progressMap())
}
// Fire-and-forget init
initProgress()
function createProgressStore() {
return {

View File

@@ -0,0 +1,163 @@
/**
* App state persistence via JSON file in XDG_CONFIG_HOME
*
* Reads and writes app settings, preferences, and custom theme to a JSON file
* instead of localStorage. Provides migration from localStorage on first run.
*/
import { ensureConfigDir, getConfigFilePath } from "./config-dir"
import { backupConfigFile } from "./config-backup"
import type { AppState, AppSettings, UserPreferences, ThemeColors } from "../types/settings"
import { DEFAULT_THEME } from "../constants/themes"
const APP_STATE_FILE = "app-state.json"
const PROGRESS_FILE = "progress.json"
const LEGACY_APP_STATE_KEY = "podtui_app_state"
const LEGACY_PROGRESS_KEY = "podtui_progress"
// --- Defaults ---
const defaultSettings: AppSettings = {
theme: "system",
fontSize: 14,
playbackSpeed: 1,
downloadPath: "",
}
const defaultPreferences: UserPreferences = {
showExplicit: false,
autoDownload: false,
}
const defaultState: AppState = {
settings: defaultSettings,
preferences: defaultPreferences,
customTheme: DEFAULT_THEME,
}
// --- App State ---
/** Load app state from JSON file */
export async function loadAppStateFromFile(): Promise<AppState> {
try {
const filePath = getConfigFilePath(APP_STATE_FILE)
const file = Bun.file(filePath)
if (!(await file.exists())) return defaultState
const raw = await file.json()
if (!raw || typeof raw !== "object") return defaultState
const parsed = raw as Partial<AppState>
return {
settings: { ...defaultSettings, ...parsed.settings },
preferences: { ...defaultPreferences, ...parsed.preferences },
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
}
} catch {
return defaultState
}
}
/** Save app state to JSON file */
export async function saveAppStateToFile(state: AppState): Promise<void> {
try {
await ensureConfigDir()
await backupConfigFile(APP_STATE_FILE)
const filePath = getConfigFilePath(APP_STATE_FILE)
await Bun.write(filePath, JSON.stringify(state, null, 2))
} catch {
// Silently ignore write errors
}
}
/**
* Migrate app state from localStorage to file.
* Only runs once — if the state file already exists, it's a no-op.
*/
export async function migrateAppStateFromLocalStorage(): Promise<boolean> {
try {
const filePath = getConfigFilePath(APP_STATE_FILE)
const file = Bun.file(filePath)
if (await file.exists()) return false
if (typeof localStorage === "undefined") return false
const raw = localStorage.getItem(LEGACY_APP_STATE_KEY)
if (!raw) return false
const parsed = JSON.parse(raw) as Partial<AppState>
const state: AppState = {
settings: { ...defaultSettings, ...parsed.settings },
preferences: { ...defaultPreferences, ...parsed.preferences },
customTheme: { ...DEFAULT_THEME, ...parsed.customTheme },
}
await saveAppStateToFile(state)
return true
} catch {
return false
}
}
// --- Progress ---
interface ProgressEntry {
episodeId: string
position: number
duration: number
timestamp: string | Date
playbackSpeed?: number
}
/** Load progress map from JSON file */
export async function loadProgressFromFile(): Promise<Record<string, ProgressEntry>> {
try {
const filePath = getConfigFilePath(PROGRESS_FILE)
const file = Bun.file(filePath)
if (!(await file.exists())) return {}
const raw = await file.json()
if (!raw || typeof raw !== "object") return {}
return raw as Record<string, ProgressEntry>
} catch {
return {}
}
}
/** Save progress map to JSON file */
export async function saveProgressToFile(data: Record<string, unknown>): Promise<void> {
try {
await ensureConfigDir()
await backupConfigFile(PROGRESS_FILE)
const filePath = getConfigFilePath(PROGRESS_FILE)
await Bun.write(filePath, JSON.stringify(data, null, 2))
} catch {
// Silently ignore write errors
}
}
/**
* Migrate progress from localStorage to file.
* Only runs once — if the progress file already exists, it's a no-op.
*/
export async function migrateProgressFromLocalStorage(): Promise<boolean> {
try {
const filePath = getConfigFilePath(PROGRESS_FILE)
const file = Bun.file(filePath)
if (await file.exists()) return false
if (typeof localStorage === "undefined") return false
const raw = localStorage.getItem(LEGACY_PROGRESS_KEY)
if (!raw) return false
const parsed = JSON.parse(raw)
if (!parsed || typeof parsed !== "object") return false
await saveProgressToFile(parsed as Record<string, unknown>)
return true
} catch {
return false
}
}

View File

@@ -0,0 +1,96 @@
/**
* Config file backup utility for PodTUI
*
* Creates timestamped backups of config files before updates.
* Keeps the most recent N backups and cleans up older ones.
*/
import { readdir, unlink } from "fs/promises"
import path from "path"
import { getConfigDir, ensureConfigDir } from "./config-dir"
/** Maximum number of backup files to keep per config file */
const MAX_BACKUPS = 5
/**
* Generate a timestamped backup filename.
* Example: feeds.json -> feeds.json.2026-02-05T120000.backup
*/
function backupFilename(originalName: string): string {
const ts = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15)
return `${originalName}.${ts}.backup`
}
/**
* Create a backup of a config file before overwriting it.
* No-op if the source file does not exist.
*/
export async function backupConfigFile(filename: string): Promise<boolean> {
try {
await ensureConfigDir()
const dir = getConfigDir()
const srcPath = path.join(dir, filename)
const srcFile = Bun.file(srcPath)
if (!(await srcFile.exists())) return false
const content = await srcFile.text()
if (!content || content.trim().length === 0) return false
const backupName = backupFilename(filename)
const backupPath = path.join(dir, backupName)
await Bun.write(backupPath, content)
// Clean up old backups
await pruneBackups(filename)
return true
} catch {
return false
}
}
/**
* Keep only the most recent MAX_BACKUPS backup files for a given config file.
*/
async function pruneBackups(filename: string): Promise<void> {
try {
const dir = getConfigDir()
const entries = await readdir(dir)
// Match pattern: filename.*.backup
const prefix = `${filename}.`
const suffix = ".backup"
const backups = entries
.filter((e) => e.startsWith(prefix) && e.endsWith(suffix))
.sort() // Lexicographic sort works because timestamps are ISO-like
if (backups.length <= MAX_BACKUPS) return
const toRemove = backups.slice(0, backups.length - MAX_BACKUPS)
for (const name of toRemove) {
await unlink(path.join(dir, name)).catch(() => {})
}
} catch {
// Silently ignore cleanup errors
}
}
/**
* List existing backup files for a given config file, newest first.
*/
export async function listBackups(filename: string): Promise<string[]> {
try {
const dir = getConfigDir()
const entries = await readdir(dir)
const prefix = `${filename}.`
const suffix = ".backup"
return entries
.filter((e) => e.startsWith(prefix) && e.endsWith(suffix))
.sort()
.reverse()
} catch {
return []
}
}

44
src/utils/config-dir.ts Normal file
View File

@@ -0,0 +1,44 @@
/**
* XDG_CONFIG_HOME directory setup for PodTUI
*
* Handles config directory detection and creation following the XDG Base
* Directory Specification. Falls back to ~/.config when XDG_CONFIG_HOME
* is not set.
*/
import { mkdir } from "fs/promises"
import path from "path"
/** Application config directory name */
const APP_DIR_NAME = "podtui"
/** Resolve the XDG_CONFIG_HOME directory, defaulting to ~/.config */
export function getXdgConfigHome(): string {
const xdg = process.env.XDG_CONFIG_HOME
if (xdg) return xdg
const home = process.env.HOME ?? process.env.USERPROFILE ?? ""
if (!home) throw new Error("Cannot determine home directory")
return path.join(home, ".config")
}
/** Get the application-specific config directory path */
export function getConfigDir(): string {
return path.join(getXdgConfigHome(), APP_DIR_NAME)
}
/** Get the path for a specific config file */
export function getConfigFilePath(filename: string): string {
return path.join(getConfigDir(), filename)
}
/**
* Ensure the application config directory exists.
* Creates it recursively if needed.
*/
export async function ensureConfigDir(): Promise<string> {
const dir = getConfigDir()
await mkdir(dir, { recursive: true })
return dir
}

View File

@@ -0,0 +1,166 @@
/**
* Config file validation and migration for PodTUI
*
* Validates JSON structure of config files, handles corrupted files
* gracefully (falling back to defaults), and provides a single
* entry-point to migrate all localStorage data to XDG config files.
*/
import { getConfigFilePath } from "./config-dir"
import {
migrateAppStateFromLocalStorage,
migrateProgressFromLocalStorage,
} from "./app-persistence"
import {
migrateFeedsFromLocalStorage,
migrateSourcesFromLocalStorage,
} from "./feeds-persistence"
// --- Validation helpers ---
/** Check that a value is a non-null object */
function isObject(v: unknown): v is Record<string, unknown> {
return v !== null && typeof v === "object" && !Array.isArray(v)
}
/** Validate AppState JSON structure */
export function validateAppState(data: unknown): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!isObject(data)) {
return { valid: false, errors: ["app-state.json is not an object"] }
}
// settings
if (data.settings !== undefined) {
if (!isObject(data.settings)) {
errors.push("settings must be an object")
} else {
const s = data.settings as Record<string, unknown>
if (s.theme !== undefined && typeof s.theme !== "string") errors.push("settings.theme must be a string")
if (s.fontSize !== undefined && typeof s.fontSize !== "number") errors.push("settings.fontSize must be a number")
if (s.playbackSpeed !== undefined && typeof s.playbackSpeed !== "number") errors.push("settings.playbackSpeed must be a number")
if (s.downloadPath !== undefined && typeof s.downloadPath !== "string") errors.push("settings.downloadPath must be a string")
}
}
// preferences
if (data.preferences !== undefined) {
if (!isObject(data.preferences)) {
errors.push("preferences must be an object")
} else {
const p = data.preferences as Record<string, unknown>
if (p.showExplicit !== undefined && typeof p.showExplicit !== "boolean") errors.push("preferences.showExplicit must be a boolean")
if (p.autoDownload !== undefined && typeof p.autoDownload !== "boolean") errors.push("preferences.autoDownload must be a boolean")
}
}
// customTheme
if (data.customTheme !== undefined && !isObject(data.customTheme)) {
errors.push("customTheme must be an object")
}
return { valid: errors.length === 0, errors }
}
/** Validate feeds JSON structure */
export function validateFeeds(data: unknown): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!Array.isArray(data)) {
return { valid: false, errors: ["feeds.json is not an array"] }
}
for (let i = 0; i < data.length; i++) {
const feed = data[i]
if (!isObject(feed)) {
errors.push(`feeds[${i}] is not an object`)
continue
}
if (typeof feed.id !== "string") errors.push(`feeds[${i}].id must be a string`)
if (!isObject(feed.podcast)) errors.push(`feeds[${i}].podcast must be an object`)
if (!Array.isArray(feed.episodes)) errors.push(`feeds[${i}].episodes must be an array`)
}
return { valid: errors.length === 0, errors }
}
/** Validate progress JSON structure */
export function validateProgress(data: unknown): { valid: boolean; errors: string[] } {
const errors: string[] = []
if (!isObject(data)) {
return { valid: false, errors: ["progress.json is not an object"] }
}
for (const [key, value] of Object.entries(data)) {
if (!isObject(value)) {
errors.push(`progress["${key}"] is not an object`)
continue
}
const p = value as Record<string, unknown>
if (typeof p.episodeId !== "string") errors.push(`progress["${key}"].episodeId must be a string`)
if (typeof p.position !== "number") errors.push(`progress["${key}"].position must be a number`)
if (typeof p.duration !== "number") errors.push(`progress["${key}"].duration must be a number`)
}
return { valid: errors.length === 0, errors }
}
// --- Safe config file reading ---
/**
* Safely read and validate a config file.
* Returns the parsed data if valid, or null if the file is missing/corrupt.
*/
export async function safeReadConfigFile<T>(
filename: string,
validator: (data: unknown) => { valid: boolean; errors: string[] },
): Promise<{ data: T | null; errors: string[] }> {
try {
const filePath = getConfigFilePath(filename)
const file = Bun.file(filePath)
if (!(await file.exists())) {
return { data: null, errors: [] }
}
const text = await file.text()
let parsed: unknown
try {
parsed = JSON.parse(text)
} catch {
return { data: null, errors: [`${filename}: invalid JSON`] }
}
const result = validator(parsed)
if (!result.valid) {
return { data: null, errors: result.errors }
}
return { data: parsed as T, errors: [] }
} catch (err) {
return { data: null, errors: [`${filename}: ${String(err)}`] }
}
}
// --- Unified migration ---
/**
* Run all localStorage -> file migrations.
* Safe to call multiple times; each migration is a no-op if the target
* file already exists.
*
* Returns a summary of what was migrated.
*/
export async function migrateAllFromLocalStorage(): Promise<{
appState: boolean
progress: boolean
feeds: boolean
sources: boolean
}> {
const [appState, progress, feeds, sources] = await Promise.all([
migrateAppStateFromLocalStorage(),
migrateProgressFromLocalStorage(),
migrateFeedsFromLocalStorage(),
migrateSourcesFromLocalStorage(),
])
return { appState, progress, feeds, sources }
}

View File

@@ -107,6 +107,9 @@ export type AppEvents = {
"dialog.open": { dialogId: string }
"dialog.close": { dialogId?: string }
"command.execute": { command: string; args?: unknown }
"clipboard.copied": { text: string }
"selection.start": { x: number; y: number }
"selection.end": { text: string }
}
// Type-safe emit and on functions

View File

@@ -0,0 +1,132 @@
/**
* Feeds persistence via JSON file in XDG_CONFIG_HOME
*
* Reads and writes feeds to a JSON file instead of localStorage.
* Provides migration from localStorage on first run.
*/
import { ensureConfigDir, getConfigFilePath } from "./config-dir"
import { backupConfigFile } from "./config-backup"
import type { Feed } from "../types/feed"
const FEEDS_FILE = "feeds.json"
const SOURCES_FILE = "sources.json"
/** Deserialize date strings back to Date objects in feed data */
function reviveDates(feed: Feed): Feed {
return {
...feed,
lastUpdated: new Date(feed.lastUpdated),
podcast: {
...feed.podcast,
lastUpdated: new Date(feed.podcast.lastUpdated),
},
episodes: feed.episodes.map((ep) => ({
...ep,
pubDate: new Date(ep.pubDate),
})),
}
}
/** Load feeds from JSON file */
export async function loadFeedsFromFile(): Promise<Feed[]> {
try {
const filePath = getConfigFilePath(FEEDS_FILE)
const file = Bun.file(filePath)
if (!(await file.exists())) return []
const raw = await file.json()
if (!Array.isArray(raw)) return []
return raw.map(reviveDates)
} catch {
return []
}
}
/** Save feeds to JSON file */
export async function saveFeedsToFile(feeds: Feed[]): Promise<void> {
try {
await ensureConfigDir()
await backupConfigFile(FEEDS_FILE)
const filePath = getConfigFilePath(FEEDS_FILE)
await Bun.write(filePath, JSON.stringify(feeds, null, 2))
} catch {
// Silently ignore write errors
}
}
/** Load sources from JSON file */
export async function loadSourcesFromFile<T>(): Promise<T[] | null> {
try {
const filePath = getConfigFilePath(SOURCES_FILE)
const file = Bun.file(filePath)
if (!(await file.exists())) return null
const raw = await file.json()
if (!Array.isArray(raw)) return null
return raw as T[]
} catch {
return null
}
}
/** Save sources to JSON file */
export async function saveSourcesToFile<T>(sources: T[]): Promise<void> {
try {
await ensureConfigDir()
await backupConfigFile(SOURCES_FILE)
const filePath = getConfigFilePath(SOURCES_FILE)
await Bun.write(filePath, JSON.stringify(sources, null, 2))
} catch {
// Silently ignore write errors
}
}
/**
* Migrate feeds from localStorage to file.
* Only runs once — if the feeds file already exists, it's a no-op.
*/
export async function migrateFeedsFromLocalStorage(): Promise<boolean> {
try {
const filePath = getConfigFilePath(FEEDS_FILE)
const file = Bun.file(filePath)
if (await file.exists()) return false // Already migrated
if (typeof localStorage === "undefined") return false
const raw = localStorage.getItem("podtui_feeds")
if (!raw) return false
const feeds = JSON.parse(raw) as Feed[]
if (!Array.isArray(feeds) || feeds.length === 0) return false
await saveFeedsToFile(feeds)
return true
} catch {
return false
}
}
/**
* Migrate sources from localStorage to file.
*/
export async function migrateSourcesFromLocalStorage(): Promise<boolean> {
try {
const filePath = getConfigFilePath(SOURCES_FILE)
const file = Bun.file(filePath)
if (await file.exists()) return false
if (typeof localStorage === "undefined") return false
const raw = localStorage.getItem("podtui_sources")
if (!raw) return false
const sources = JSON.parse(raw)
if (!Array.isArray(sources) || sources.length === 0) return false
await saveSourcesToFile(sources)
return true
} catch {
return false
}
}

111
src/utils/html-to-text.ts Normal file
View File

@@ -0,0 +1,111 @@
/**
* HTML-to-text conversion for PodTUI
*
* Converts HTML content from RSS feed descriptions into clean plain text
* suitable for display in the terminal. Preserves paragraph structure,
* converts lists to bulleted text, and strips all tags.
*/
/**
* Convert HTML content to readable plain text.
*
* - Block elements (<p>, <div>, <br>, headings, <li>) become line breaks
* - <li> items get a bullet prefix
* - <a href="...">text</a> becomes "text (url)"
* - All other tags are stripped
* - HTML entities are decoded
* - Excessive whitespace is collapsed
*/
export function htmlToText(html: string): string {
if (!html) return ""
let text = html
// Strip CDATA wrappers
text = text.replace(/<!\[CDATA\[([\s\S]*?)]]>/gi, "$1")
// Replace <br> / <br/> with newline
text = text.replace(/<br\s*\/?>/gi, "\n")
// Replace <hr> with a separator line
text = text.replace(/<hr\s*\/?>/gi, "\n---\n")
// Block-level elements get newlines before/after
text = text.replace(/<\/?(p|div|blockquote|pre|h[1-6]|table|tr|section|article|header|footer)[\s>][^>]*>/gi, "\n")
// List items get bullet prefix
text = text.replace(/<li[^>]*>/gi, "\n - ")
text = text.replace(/<\/li>/gi, "")
// Strip list wrappers
text = text.replace(/<\/?(ul|ol|dl|dt|dd)[^>]*>/gi, "\n")
// Convert links: <a href="url">text</a> -> text (url)
text = text.replace(/<a\s[^>]*href=["']([^"']*)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, linkText) => {
const cleanText = stripTags(linkText).trim()
if (!cleanText) return href
// Don't duplicate if the link text IS the URL
if (cleanText === href || cleanText === href.replace(/^https?:\/\//, "")) return cleanText
return `${cleanText} (${href})`
})
// Strip all remaining tags
text = stripTags(text)
// Decode HTML entities
text = decodeHtmlEntities(text)
// Collapse multiple blank lines into at most two newlines
text = text.replace(/\n{3,}/g, "\n\n")
// Collapse runs of spaces/tabs (but not newlines) on each line
text = text
.split("\n")
.map((line) => line.replace(/[ \t]+/g, " ").trim())
.join("\n")
return text.trim()
}
/** Strip all HTML/XML tags from a string */
function stripTags(html: string): string {
return html.replace(/<[^>]*>/g, "")
}
/** Decode common HTML entities */
function decodeHtmlEntities(text: string): string {
return text
// Named entities
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&nbsp;/g, " ")
.replace(/&mdash;/g, "\u2014")
.replace(/&ndash;/g, "\u2013")
.replace(/&hellip;/g, "\u2026")
.replace(/&laquo;/g, "\u00AB")
.replace(/&raquo;/g, "\u00BB")
.replace(/&ldquo;/g, "\u201C")
.replace(/&rdquo;/g, "\u201D")
.replace(/&lsquo;/g, "\u2018")
.replace(/&rsquo;/g, "\u2019")
.replace(/&bull;/g, "\u2022")
.replace(/&copy;/g, "\u00A9")
.replace(/&reg;/g, "\u00AE")
.replace(/&trade;/g, "\u2122")
.replace(/&deg;/g, "\u00B0")
.replace(/&times;/g, "\u00D7")
// Numeric entities (decimal)
.replace(/&#(\d+);/g, (_, code) => {
const n = parseInt(code, 10)
return n > 0 && n < 0x10ffff ? String.fromCodePoint(n) : ""
})
// Numeric entities (hex)
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
const n = parseInt(hex, 16)
return n > 0 && n < 0x10ffff ? String.fromCodePoint(n) : ""
})
}

View File

@@ -0,0 +1,40 @@
/**
* RSS content type detection for PodTUI
*
* Determines whether RSS feed content (description, etc.) is HTML or plain
* text so the appropriate parsing path can be selected.
*/
export enum ContentType {
HTML = "html",
PLAIN_TEXT = "plain_text",
UNKNOWN = "unknown",
}
/** Common HTML tags found in RSS descriptions */
const HTML_TAG_RE = /<\s*\/?\s*(div|p|br|a|b|i|em|strong|ul|ol|li|span|h[1-6]|img|table|tr|td|blockquote|pre|code|hr)\b[^>]*\/?>/i
/** HTML entity patterns beyond the basic five (&amp; etc.) */
const HTML_ENTITY_RE = /&(nbsp|mdash|ndash|hellip|laquo|raquo|ldquo|rdquo|lsquo|rsquo|bull|#\d{2,5}|#x[0-9a-fA-F]{2,4});/
/** CDATA wrapper — content inside is almost always HTML */
const CDATA_RE = /^\s*<!\[CDATA\[/
/**
* Detect whether a string contains HTML markup or is plain text.
*/
export function detectContentType(content: string): ContentType {
if (!content || content.trim().length === 0) return ContentType.UNKNOWN
// CDATA-wrapped content is nearly always HTML
if (CDATA_RE.test(content)) return ContentType.HTML
// Check for standard HTML tags
if (HTML_TAG_RE.test(content)) return ContentType.HTML
// Check for extended HTML entities (basic &amp; / &lt; / etc. can appear in
// plain text too, so we only look for the less common ones)
if (HTML_ENTITY_RE.test(content)) return ContentType.HTML
return ContentType.PLAIN_TEXT
}

113
tasks/INDEX.md Normal file
View File

@@ -0,0 +1,113 @@
# PodTUI Task Index
This directory contains all task files for the PodTUI project feature implementation.
## Task Structure
Each feature has its own directory with:
- `README.md` - Feature overview and task list
- `{seq}-{task-description}.md` - Individual task files
## Feature Overview
### 1. Text Selection Copy to Clipboard
**Feature:** Text selection copy to clipboard
**Tasks:** 2 tasks
**Directory:** `tasks/text-selection-copy/`
### 2. HTML vs Plain Text RSS Parsing
**Feature:** Detect and handle both HTML and plain text content in RSS feeds
**Tasks:** 3 tasks
**Directory:** `tasks/rss-content-parsing/`
### 3. Merged Waveform Progress Bar
**Feature:** Create a real-time waveform visualization that expands from a progress bar during playback
**Tasks:** 4 tasks
**Directory:** `tasks/merged-waveform/`
### 4. Episode List Infinite Scroll
**Feature:** Implement scroll-to-bottom loading for episode lists with MAX_EPISODES_REFRESH limit
**Tasks:** 4 tasks
**Directory:** `tasks/episode-infinite-scroll/`
### 5. Episode Downloads
**Feature:** Add per-episode download and per-feed auto-download settings
**Tasks:** 6 tasks
**Directory:** `tasks/episode-downloads/`
### 6. Discover Categories Shortcuts Fix
**Feature:** Fix broken discover category filter functionality
**Tasks:** 3 tasks
**Directory:** `tasks/discover-categories-fix/`
### 7. Config Persistence to XDG_CONFIG_HOME
**Feature:** Move feeds and themes persistence from localStorage to XDG_CONFIG_HOME directory
**Tasks:** 5 tasks
**Directory:** `tasks/config-persistence/`
## Task Summary
**Total Features:** 7
**Total Tasks:** 27
**Critical Path:** Feature 7 (Config Persistence) - 5 tasks
## Task Dependencies
### Feature 1: Text Selection Copy to Clipboard
- 01 → 02
### Feature 2: HTML vs Plain Text RSS Parsing
- 03 → 04
- 03 → 05
### Feature 3: Merged Waveform Progress Bar
- 06 → 07
- 07 → 08
- 08 → 09
### Feature 4: Episode List Infinite Scroll
- 10 → 11
- 11 → 12
- 12 → 13
### Feature 5: Episode Downloads
- 14 → 15
- 15 → 16
- 16 → 17
- 17 → 18
- 18 → 19
### Feature 6: Discover Categories Shortcuts Fix
- 20 → 21
- 21 → 22
### Feature 7: Config Persistence to XDG_CONFIG_HOME
- 23 → 24
- 23 → 25
- 24 → 26
- 25 → 26
- 26 → 27
## Priority Overview
**P1 (Critical):**
- 23: Implement XDG_CONFIG_HOME directory setup
- 24: Refactor feeds persistence to JSON file
- 25: Refactor theme persistence to JSON file
- 26: Add config file validation and migration
**P2 (High):**
- All other tasks (01-22, 27)
**P3 (Medium):**
- 09: Optimize waveform rendering performance
- 13: Add loading indicator for pagination
- 19: Create download queue management
## Next Steps
1. Review all task files for accuracy
2. Confirm task dependencies
3. Start with P1 tasks (Feature 7)
4. Follow dependency order within each feature
5. Mark tasks complete as they're finished

View File

@@ -0,0 +1,50 @@
# 23. Implement XDG_CONFIG_HOME Directory Setup
meta:
id: config-persistence-23
feature: config-persistence
priority: P1
depends_on: []
tags: [configuration, file-system, directory-setup]
objective:
- Implement XDG_CONFIG_HOME directory detection and creation
- Create application-specific config directory
- Handle XDG_CONFIG_HOME environment variable
- Provide fallback to ~/.config if XDG_CONFIG_HOME not set
deliverables:
- Config directory detection utility
- Directory creation logic
- Environment variable handling
steps:
1. Create `src/utils/config-dir.ts`
2. Implement XDG_CONFIG_HOME detection
3. Create fallback to HOME/.config
4. Create application-specific directory (podcast-tui-app)
5. Add directory creation with error handling
tests:
- Unit: Test XDG_CONFIG_HOME detection
- Unit: Test config directory creation
- Manual: Verify directory exists at expected path
acceptance_criteria:
- Config directory is created at correct path
- XDG_CONFIG_HOME is respected if set
- Falls back to ~/.config if XDG_CONFIG_HOME not set
- Directory is created with correct permissions
validation:
- Run app and check config directory exists
- Test with XDG_CONFIG_HOME=/custom/path
- Test with XDG_CONFIG_HOME not set
- Verify directory is created in both cases
notes:
- XDG_CONFIG_HOME default: ~/.config
- App name from package.json: podcast-tui-app
- Use Bun.file() and file operations for directory creation
- Handle permission errors gracefully
- Use mkdir -p for recursive creation

View File

@@ -0,0 +1,51 @@
# 24. Refactor Feeds Persistence to JSON File
meta:
id: config-persistence-24
feature: config-persistence
priority: P1
depends_on: [config-persistence-23]
tags: [persistence, feeds, file-io]
objective:
- Move feeds persistence from localStorage to JSON file
- Load feeds from XDG_CONFIG_HOME directory
- Save feeds to JSON file
- Maintain backward compatibility
deliverables:
- Feeds JSON file I/O functions
- Updated feed store persistence
- Migration from localStorage
steps:
1. Create `src/utils/feeds-persistence.ts`
2. Implement loadFeedsFromFile() function
3. Implement saveFeedsToFile() function
4. Update feed store to use file-based persistence
5. Add migration from localStorage to file
tests:
- Unit: Test file I/O functions
- Integration: Test feed persistence with file
- Migration: Test migration from localStorage
acceptance_criteria:
- Feeds are loaded from JSON file
- Feeds are saved to JSON file
- Backward compatibility maintained
validation:
- Start app with no config file
- Subscribe to feeds
- Verify feeds saved to file
- Restart app and verify feeds loaded
- Test migration from localStorage
notes:
- File path: XDG_CONFIG_HOME/podcast-tui-app/feeds.json
- Use JSON.stringify/parse for serialization
- Handle file not found (empty initial load)
- Handle file write errors
- Add timestamp to file for versioning
- Maintain Feed type structure

View File

@@ -0,0 +1,52 @@
# 25. Refactor Theme Persistence to JSON File
meta:
id: config-persistence-25
feature: config-persistence
priority: P1
depends_on: [config-persistence-23]
tags: [persistence, themes, file-io]
objective:
- Move theme persistence from localStorage to JSON file
- Load custom themes from XDG_CONFIG_HOME directory
- Save custom themes to JSON file
- Maintain backward compatibility
deliverables:
- Themes JSON file I/O functions
- Updated theme persistence
- Migration from localStorage
steps:
1. Create `src/utils/themes-persistence.ts`
2. Implement loadThemesFromFile() function
3. Implement saveThemesToFile() function
4. Update theme store to use file-based persistence
5. Add migration from localStorage to file
tests:
- Unit: Test file I/O functions
- Integration: Test theme persistence with file
- Migration: Test migration from localStorage
acceptance_criteria:
- Custom themes are loaded from JSON file
- Custom themes are saved to JSON file
- Backward compatibility maintained
validation:
- Start app with no theme file
- Load custom theme
- Verify theme saved to file
- Restart app and verify theme loaded
- Test migration from localStorage
notes:
- File path: XDG_CONFIG_HOME/podcast-tui-app/themes.json
- Use JSON.stringify/parse for serialization
- Handle file not found (use default themes)
- Handle file write errors
- Add timestamp to file for versioning
- Maintain theme type structure
- Include all theme files in directory

View File

@@ -0,0 +1,51 @@
# 26. Add Config File Validation and Migration
meta:
id: config-persistence-26
feature: config-persistence
priority: P1
depends_on: [config-persistence-24, config-persistence-25]
tags: [validation, migration, data-integrity]
objective:
- Validate config file structure and data integrity
- Migrate data from localStorage to file
- Provide migration on first run
- Handle config file corruption
deliverables:
- Config file validation function
- Migration utility from localStorage
- Error handling for corrupted files
steps:
1. Create config file schema validation
2. Implement migration from localStorage to file
3. Add config file backup before migration
4. Handle corrupted JSON files
5. Test migration scenarios
tests:
- Unit: Test validation function
- Integration: Test migration from localStorage
- Error: Test corrupted file handling
acceptance_criteria:
- Config files are validated before use
- Migration from localStorage works seamlessly
- Corrupted files are handled gracefully
validation:
- Start app with localStorage data
- Verify migration to file
- Corrupt file and verify handling
- Test migration on app restart
notes:
- Validate Feed type structure
- Validate theme structure
- Create backup before migration
- Log migration events
- Provide error messages for corrupted files
- Add config file versioning
- Test with both new and old data formats

View File

@@ -0,0 +1,50 @@
# 27. Implement Config File Backup on Update
meta:
id: config-persistence-27
feature: config-persistence
priority: P2
depends_on: [config-persistence-26]
tags: [backup, data-safety, migration]
objective:
- Create backups of config files before updates
- Handle config file changes during app updates
- Provide rollback capability if needed
deliverables:
- Config backup utility
- Backup on config changes
- Config version history
steps:
1. Create config backup function
2. Implement backup on config save
3. Add config version history management
4. Test backup and restore scenarios
5. Add config file version display
tests:
- Unit: Test backup function
- Integration: Test backup on config save
- Manual: Test restore from backup
acceptance_criteria:
- Config files are backed up before updates
- Backup preserves data integrity
- Config version history is maintained
validation:
- Make config changes
- Verify backup created
- Restart app and check backup
- Test restore from backup
notes:
- Backup file naming: feeds.json.backup, themes.json.backup
- Keep last N backups (e.g., 5)
- Backup timestamp in filename
- Use atomic file operations
- Test with large config files
- Add config file size tracking
- Consider automatic cleanup of old backups

View File

@@ -0,0 +1,25 @@
# Config Persistence to XDG_CONFIG_HOME
Objective: Move feeds and themes persistence from localStorage to XDG_CONFIG_HOME directory
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 23 — Implement XDG_CONFIG_HOME directory setup → `23-config-directory-setup.md`
- [ ] 24 — Refactor feeds persistence to JSON file → `24-feeds-persistence-refactor.md`
- [ ] 25 — Refactor theme persistence to JSON file → `25-theme-persistence-refactor.md`
- [ ] 26 — Add config file validation and migration → `26-config-file-validation.md`
- [ ] 27 — Implement config file backup on update → `27-config-file-backup.md`
Dependencies
- 23 -> 24
- 23 -> 25
- 24 -> 26
- 25 -> 26
- 26 -> 27
Exit criteria
- Feeds are persisted to XDG_CONFIG_HOME/podcast-tui-app/feeds.json
- Themes are persisted to XDG_CONFIG_HOME/podcast-tui-app/themes.json
- Config file validation ensures data integrity
- Migration from localStorage works seamlessly

View File

@@ -0,0 +1,47 @@
# 20. Debug Category Filter Implementation
meta:
id: discover-categories-fix-20
feature: discover-categories-fix
priority: P2
depends_on: []
tags: [debugging, discover, categories]
objective:
- Identify why category filter is not working
- Analyze CategoryFilter component behavior
- Trace state flow from category selection to show filtering
deliverables:
- Debugged category filter logic
- Identified root cause of issue
- Test cases to verify fix
steps:
1. Review CategoryFilter component implementation
2. Review DiscoverPage category selection handler
3. Review discover store category filtering logic
4. Add console logging to trace state changes
5. Test with various category selections
tests:
- Debug: Test category selection in UI
- Debug: Verify state updates in console
- Manual: Select different categories and observe behavior
acceptance_criteria:
- Root cause of category filter issue identified
- State flow from category to shows is traced
- Specific code causing issue identified
validation:
- Run app and select categories
- Check console for state updates
- Verify which component is not responding correctly
notes:
- Check if categoryIndex signal is updated
- Verify discoverStore.setSelectedCategory() is called
- Check if filteredPodcasts() is recalculated
- Look for race conditions or state sync issues
- Add temporary logging to trace state changes

View File

@@ -0,0 +1,47 @@
# 21. Fix Category State Synchronization
meta:
id: discover-categories-fix-21
feature: discover-categories-fix
priority: P2
depends_on: [discover-categories-fix-20]
tags: [state-management, discover, categories]
objective:
- Ensure category state is properly synchronized across components
- Fix state updates not triggering re-renders
- Ensure category selection persists correctly
deliverables:
- Fixed state synchronization logic
- Updated category selection handlers
- Verified state propagation
steps:
1. Fix category state update handlers in DiscoverPage
2. Ensure discoverStore.setSelectedCategory() is called correctly
3. Fix signal updates to trigger component re-renders
4. Test state synchronization across component updates
5. Verify category state persists on navigation
tests:
- Unit: Test state update handlers
- Integration: Test category selection and state updates
- Manual: Navigate between tabs and verify category state
acceptance_criteria:
- Category state updates propagate correctly
- Component re-renders when category changes
- Category selection persists across navigation
validation:
- Select category and verify show list updates
- Switch tabs and back, verify category still selected
- Test category navigation with keyboard
notes:
- Check if signals are properly created and updated
- Verify discoverStore state is reactive
- Ensure CategoryFilter and TrendingShows receive updated data
- Test with multiple category selections
- Add state persistence if needed

View File

@@ -0,0 +1,47 @@
# 22. Fix Category Keyboard Navigation
meta:
id: discover-categories-fix-22
feature: discover-categories-fix
priority: P2
depends_on: [discover-categories-fix-21]
tags: [keyboard, navigation, discover]
objective:
- Fix keyboard navigation for categories
- Ensure category selection works with arrow keys
- Fix category index tracking during navigation
deliverables:
- Fixed keyboard navigation handlers
- Updated category index tracking
- Verified navigation works correctly
steps:
1. Review keyboard navigation in DiscoverPage
2. Fix category index signal updates
3. Ensure categoryIndex signal is updated on arrow key presses
4. Test category navigation with arrow keys
5. Fix category selection on Enter key
tests:
- Integration: Test category navigation with keyboard
- Manual: Navigate categories with arrow keys
- Edge case: Test category navigation from shows list
acceptance_criteria:
- Arrow keys navigate categories correctly
- Category index updates on navigation
- Enter key selects category and updates shows list
validation:
- Use arrow keys to navigate categories
- Verify category highlight moves correctly
- Press Enter to select category and verify show list updates
notes:
- Check if categoryIndex signal is bound correctly
- Ensure arrow keys update categoryIndex signal
- Verify categoryIndex is used in filteredPodcasts()
- Test category navigation from shows list back to categories
- Add keyboard hints in UI

View File

@@ -0,0 +1,19 @@
# Discover Categories Shortcuts Fix
Objective: Fix broken discover category filter functionality
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 20 — Debug category filter implementation → `20-category-filter-debug.md`
- [ ] 21 — Fix category state synchronization → `21-category-state-sync.md`
- [ ] 22 — Fix category keyboard navigation → `22-category-navigation-fix.md`
Dependencies
- 20 -> 21
- 21 -> 22
Exit criteria
- Category filter correctly updates show list
- Keyboard navigation works for categories
- Category selection persists during navigation

View File

@@ -0,0 +1,46 @@
# 14. Define Download Storage Structure
meta:
id: episode-downloads-14
feature: episode-downloads
priority: P2
depends_on: []
tags: [storage, types, data-model]
objective:
- Define data structures for downloaded episodes
- Create download state tracking
- Design download history and metadata storage
deliverables:
- DownloadedEpisode type definition
- Download state interface
- Storage schema for download metadata
steps:
1. Add DownloadedEpisode type to types/episode.ts
2. Define download state structure (status, progress, timestamp)
3. Create download metadata interface
4. Add download-related fields to Feed type
5. Design database-like storage structure
tests:
- Unit: Test type definitions
- Integration: Test storage schema
- Validation: Verify structure supports all download scenarios
acceptance_criteria:
- DownloadedEpisode type properly defines download metadata
- Download state interface tracks all necessary information
- Storage schema supports history and progress tracking
validation:
- Review type definitions for completeness
- Verify storage structure can hold all download data
- Test with mock download scenarios
notes:
- Add fields: status (downloading, completed, failed), progress (0-100), filePath, downloadedAt
- Include download speed and estimated time remaining
- Store download history with timestamps
- Consider adding resume capability

View File

@@ -0,0 +1,47 @@
# 15. Create Episode Download Utility
meta:
id: episode-downloads-15
feature: episode-downloads
priority: P2
depends_on: [episode-downloads-14]
tags: [downloads, utilities, file-io]
objective:
- Implement episode download functionality
- Download audio files from episode URLs
- Handle download errors and edge cases
deliverables:
- Download utility function
- File download handler
- Error handling for download failures
steps:
1. Create `src/utils/episode-downloader.ts`
2. Implement download function using Bun.file() or fetch
3. Add progress tracking during download
4. Handle download cancellation
5. Add error handling for network and file system errors
tests:
- Unit: Test download function with mock URLs
- Integration: Test with real audio file URLs
- Error handling: Test download failure scenarios
acceptance_criteria:
- Episodes can be downloaded successfully
- Download progress is tracked
- Errors are handled gracefully
validation:
- Download test episode from real podcast
- Verify file is saved correctly
- Check download progress tracking
notes:
- Use Bun's built-in file download capabilities
- Support resuming interrupted downloads
- Handle large files with streaming
- Add download speed tracking
- Consider download location in downloadPath setting

View File

@@ -0,0 +1,47 @@
# 16. Implement Download Progress Tracking
meta:
id: episode-downloads-16
feature: episode-downloads
priority: P2
depends_on: [episode-downloads-15]
tags: [progress, state-management, downloads]
objective:
- Track download progress for each episode
- Update download state in real-time
- Store download progress in persistent storage
deliverables:
- Download progress state in app store
- Progress update utility
- Integration with download utility
steps:
1. Add download state to app store
2. Update progress during download
3. Save progress to persistent storage
4. Handle download completion
5. Test progress tracking accuracy
tests:
- Unit: Test progress update logic
- Integration: Test progress tracking with download
- Persistence: Verify progress saved and restored
acceptance_criteria:
- Download progress is tracked accurately
- Progress updates in real-time
- Progress persists across app restarts
validation:
- Download a large file and watch progress
- Verify progress updates at intervals
- Restart app and verify progress restored
notes:
- Use existing progress store for episode playback
- Create separate download progress store
- Update progress every 1-2 seconds
- Handle download cancellation by resetting progress
- Store progress in XDG_CONFIG_HOME directory

View File

@@ -0,0 +1,47 @@
# 17. Add Download Status in Episode List
meta:
id: episode-downloads-17
feature: episode-downloads
priority: P2
depends_on: [episode-downloads-16]
tags: [ui, downloads, display]
objective:
- Display download status for episodes
- Add download button to episode list
- Show download progress visually
deliverables:
- Download status indicator component
- Download button in episode list
- Progress bar for downloading episodes
steps:
1. Add download status field to EpisodeListItem
2. Create download button in MyShowsPage episodes panel
3. Display download status (none, queued, downloading, completed, failed)
4. Add download progress bar for downloading episodes
5. Test download status display
tests:
- Integration: Test download status display
- Visual: Verify download button and progress bar
- UX: Test download status changes
acceptance_criteria:
- Download status is visible in episode list
- Download button is accessible
- Progress bar shows download progress
validation:
- View episode list with download button
- Start download and watch status change
- Verify progress bar updates
notes:
- Reuse existing episode list UI from MyShowsPage
- Add download icon button next to episode title
- Show status text: "DL", "DWN", "DONE", "ERR"
- Use existing progress bar component for download progress
- Position download button in episode header

View File

@@ -0,0 +1,48 @@
# 18. Implement Per-Feed Auto-Download Settings
meta:
id: episode-downloads-18
feature: episode-downloads
priority: P2
depends_on: [episode-downloads-17]
tags: [settings, automation, downloads]
objective:
- Add per-feed auto-download settings
- Configure number of episodes to auto-download per feed
- Enable/disable auto-download per feed
deliverables:
- Auto-download settings in feed store
- Settings UI for per-feed configuration
- Auto-download trigger logic
steps:
1. Add autoDownload field to Feed type
2. Add autoDownloadCount field to Feed type
3. Add settings UI in FeedPage or MyShowsPage
4. Implement auto-download trigger logic
5. Test auto-download functionality
tests:
- Unit: Test auto-download trigger logic
- Integration: Test with multiple feeds
- Edge case: Test with feeds having fewer episodes
acceptance_criteria:
- Auto-download settings are configurable per feed
- Settings are saved to persistent storage
- Auto-download works correctly when enabled
validation:
- Configure auto-download for a feed
- Subscribe to new episodes and verify auto-download
- Test with multiple feeds
notes:
- Add settings in FeedPage or MyShowsPage
- Default: autoDownload = false, autoDownloadCount = 0
- Only download newest episodes (by pubDate)
- Respect MAX_EPISODES_REFRESH limit
- Add settings in feed detail or feed list
- Consider adding "auto-download all new episodes" setting

View File

@@ -0,0 +1,48 @@
# 19. Create Download Queue Management
meta:
id: episode-downloads-19
feature: episode-downloads
priority: P3
depends_on: [episode-downloads-18]
tags: [queue, downloads, management]
objective:
- Manage download queue for multiple episodes
- Handle concurrent downloads
- Provide queue UI for managing downloads
deliverables:
- Download queue data structure
- Download queue manager
- Download queue UI
steps:
1. Create download queue data structure
2. Implement download queue manager (add, remove, process)
3. Handle concurrent downloads (limit to 1-2 at a time)
4. Create download queue UI component
5. Test queue management
tests:
- Unit: Test queue management logic
- Integration: Test with multiple downloads
- Edge case: Test queue with 50+ episodes
acceptance_criteria:
- Download queue manages multiple downloads
- Concurrent downloads are limited
- Queue UI shows download status
validation:
- Add 10 episodes to download queue
- Verify queue processes sequentially
- Check queue UI displays correctly
notes:
- Use queue data structure (array of episodes)
- Limit concurrent downloads to 2 for performance
- Add queue UI in Settings or separate tab
- Show queue in SettingsScreen or new Downloads tab
- Allow removing items from queue
- Add pause/resume for downloads

View File

@@ -0,0 +1,26 @@
# Episode Downloads
Objective: Add per-episode download and per-feed auto-download settings
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 14 — Define download storage structure → `14-download-storage-structure.md`
- [ ] 15 — Create episode download utility → `15-episode-download-utility.md`
- [ ] 16 — Implement download progress tracking → `16-download-progress-tracking.md`
- [ ] 17 — Add download status in episode list → `17-download-ui-component.md`
- [ ] 18 — Implement per-feed auto-download settings → `18-auto-download-settings.md`
- [ ] 19 — Create download queue management → `19-download-queue-management.md`
Dependencies
- 14 -> 15
- 15 -> 16
- 16 -> 17
- 17 -> 18
- 18 -> 19
Exit criteria
- Episodes can be downloaded individually
- Per-feed auto-download settings are configurable
- Download progress is tracked and displayed
- Download queue can be managed

View File

@@ -0,0 +1,46 @@
# 10. Add Scroll Event Listener to Episodes Panel
meta:
id: episode-infinite-scroll-10
feature: episode-infinite-scroll
priority: P2
depends_on: []
tags: [ui, events, scroll]
objective:
- Detect when user scrolls to bottom of episodes list
- Add scroll event listener to episodes panel
- Track scroll position and trigger pagination when needed
deliverables:
- Scroll event handler function
- Scroll position tracking
- Integration with episodes panel
steps:
1. Modify MyShowsPage to add scroll event listener
2. Detect scroll-to-bottom event (when scrollHeight - scrollTop <= clientHeight)
3. Track current scroll position
4. Add debouncing for scroll events
5. Test scroll detection accuracy
tests:
- Unit: Test scroll detection logic
- Integration: Test scroll events in episodes panel
- Manual: Scroll to bottom and verify detection
acceptance_criteria:
- Scroll-to-bottom is detected accurately
- Debouncing prevents excessive event firing
- Scroll position is tracked correctly
validation:
- Scroll through episodes list
- Verify bottom detection works
- Test with different terminal sizes
notes:
- Use scrollbox component's scroll event if available
- Debounce scroll events to 100ms
- Handle both manual scroll and programmatic scroll
- Consider virtual scrolling if episode count is large

View File

@@ -0,0 +1,46 @@
# 11. Implement Paginated Episode Fetching
meta:
id: episode-infinite-scroll-11
feature: episode-infinite-scroll
priority: P2
depends_on: [episode-infinite-scroll-10]
tags: [rss, pagination, data-fetching]
objective:
- Fetch episodes in chunks with MAX_EPISODES_REFRESH limit
- Merge new episodes with existing list
- Maintain episode ordering (newest first)
deliverables:
- Paginated episode fetch function
- Episode list merging logic
- Integration with feed store
steps:
1. Create paginated fetch function in feed store
2. Implement chunk-based episode fetching (50 episodes at a time)
3. Add logic to merge new episodes with existing list
4. Maintain reverse chronological order (newest first)
5. Deduplicate episodes by title or URL
tests:
- Unit: Test paginated fetch logic
- Integration: Test with real RSS feeds
- Edge case: Test with feeds having < 50 episodes
acceptance_criteria:
- Episodes fetched in chunks of MAX_EPISODES_REFRESH
- New episodes merged correctly with existing list
- Episode ordering maintained (newest first)
validation:
- Test with RSS feed having 100+ episodes
- Verify pagination works correctly
- Check episode ordering after merge
notes:
- Use existing `MAX_EPISODES_REFRESH = 50` constant
- Add episode deduplication logic
- Preserve episode metadata during merge
- Handle cases where feed has fewer episodes

View File

@@ -0,0 +1,46 @@
# 12. Manage Episode List Pagination State
meta:
id: episode-infinite-scroll-12
feature: episode-infinite-scroll
priority: P2
depends_on: [episode-infinite-scroll-11]
tags: [state-management, pagination]
objective:
- Track pagination state (current page, loaded count, has more episodes)
- Manage episode list state changes
- Handle pagination state across component renders
deliverables:
- Pagination state in feed store
- Episode list state management
- Integration with scroll events
steps:
1. Add pagination state to feed store (currentPage, loadedCount, hasMore)
2. Update episode list when new episodes are loaded
3. Manage loading state for pagination
4. Handle empty episode list case
5. Test pagination state transitions
tests:
- Unit: Test pagination state updates
- Integration: Test state transitions with scroll
- Edge case: Test with no episodes in feed
acceptance_criteria:
- Pagination state accurately tracks loaded episodes
- Episode list updates correctly with new episodes
- Loading state properly managed
validation:
- Load episodes and verify state updates
- Scroll to bottom and verify pagination triggers
- Test with feed having many episodes
notes:
- Use existing feed store from `src/stores/feed.ts`
- Add pagination state to Feed interface
- Consider loading indicator visibility
- Handle rapid scroll events gracefully

View File

@@ -0,0 +1,46 @@
# 13. Add Loading Indicator for Pagination
meta:
id: episode-infinite-scroll-13
feature: episode-infinite-scroll
priority: P3
depends_on: [episode-infinite-scroll-12]
tags: [ui, feedback, loading]
objective:
- Display loading indicator when fetching more episodes
- Show loading state in episodes panel
- Hide indicator when pagination complete
deliverables:
- Loading indicator component
- Loading state display logic
- Integration with pagination events
steps:
1. Add loading state to episodes panel state
2. Create loading indicator UI (spinner or text)
3. Display indicator when fetching episodes
4. Hide indicator when pagination complete
5. Test loading state visibility
tests:
- Integration: Test loading indicator during fetch
- Visual: Verify loading state doesn't block interaction
- UX: Test loading state disappears when done
acceptance_criteria:
- Loading indicator displays during fetch
- Indicator is visible but doesn't block scrolling
- Indicator disappears when pagination complete
validation:
- Scroll to bottom and watch loading indicator
- Verify indicator shows/hides correctly
- Test with slow RSS feeds
notes:
- Reuse existing loading indicator pattern from MyShowsPage
- Use spinner or "Loading..." text
- Position indicator at bottom of scrollbox
- Don't block user interaction while loading

View File

@@ -0,0 +1,21 @@
# Episode List Infinite Scroll
Objective: Implement scroll-to-bottom loading for episode lists with MAX_EPISODES_REFRESH limit
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 10 — Add scroll event listener to episodes panel → `10-episode-list-scroll-handler.md`
- [ ] 11 — Implement paginated episode fetching → `11-paginated-episode-loading.md`
- [ ] 12 — Manage episode list pagination state → `12-episode-list-state-management.md`
- [ ] 13 — Add loading indicator for pagination → `13-load-more-indicator.md`
Dependencies
- 10 -> 11
- 11 -> 12
- 12 -> 13
Exit criteria
- Episode list automatically loads more episodes when scrolling to bottom
- MAX_EPISODES_REFRESH is respected per fetch
- Loading state is properly displayed during pagination

View File

@@ -0,0 +1,46 @@
# 06. Implement Audio Waveform Analysis
meta:
id: merged-waveform-06
feature: merged-waveform
priority: P2
depends_on: []
tags: [audio, waveform, analysis]
objective:
- Analyze audio data to extract waveform information
- Create real-time waveform data from audio streams
- Generate waveform data points for visualization
deliverables:
- Audio analysis utility
- Waveform data extraction function
- Integration with audio backend
steps:
1. Research and select audio waveform analysis library (e.g., `audiowaveform`)
2. Create `src/utils/audio-waveform.ts`
3. Implement audio data extraction from backend
4. Generate waveform data points (amplitude values)
5. Add sample rate and duration normalization
tests:
- Unit: Test waveform generation from sample audio
- Integration: Test with real audio playback
- Performance: Measure waveform generation overhead
acceptance_criteria:
- Waveform data is generated from audio content
- Data points represent audio amplitude accurately
- Generation works with real-time audio streams
validation:
- Generate waveform from sample MP3 file
- Verify amplitude data matches audio peaks
- Test with different audio formats
notes:
- Consider using `ffmpeg` or `sox` for offline analysis
- For real-time: analyze audio chunks during playback
- Waveform resolution: 64-256 data points for TUI display
- Normalize amplitude to 0-1 range

View File

@@ -0,0 +1,46 @@
# 07. Create Merged Progress-Waveform Component
meta:
id: merged-waveform-07
feature: merged-waveform
priority: P2
depends_on: [merged-waveform-06]
tags: [ui, waveform, component]
objective:
- Design and implement a single component that shows progress bar and waveform
- Component starts as progress bar, expands to waveform when playing
- Provide smooth transitions between states
deliverables:
- MergedWaveform component
- State management for progress vs waveform display
- Visual styling for progress bar and waveform
steps:
1. Create `src/components/MergedWaveform.tsx`
2. Design component state machine (progress bar → waveform)
3. Implement progress bar visualization
4. Add waveform expansion animation
5. Style progress bar and waveform with theme colors
tests:
- Unit: Test component state transitions
- Integration: Test component in Player
- Visual: Verify smooth expansion animation
acceptance_criteria:
- Component displays progress bar when paused
- Component smoothly expands to waveform when playing
- Visual styles match theme and existing UI
validation:
- Test with paused and playing states
- Verify expansion is smooth and visually appealing
- Check theme color integration
notes:
- Use existing Waveform component as base
- Add CSS transitions for smooth expansion
- Keep component size manageable (fit in progress bar area)
- Consider responsive to terminal width changes

View File

@@ -0,0 +1,46 @@
# 08. Implement Real-Time Waveform Rendering During Playback
meta:
id: merged-waveform-08
feature: merged-waveform
priority: P2
depends_on: [merged-waveform-07]
tags: [audio, realtime, rendering]
objective:
- Update waveform in real-time during audio playback
- Highlight waveform based on current playback position
- Sync waveform with audio backend position updates
deliverables:
- Real-time waveform update logic
- Playback position highlighting
- Integration with audio backend position tracking
steps:
1. Subscribe to audio backend position updates
2. Update waveform data points based on playback position
3. Implement playback position highlighting
4. Add animation for progress indicator
5. Test synchronization with audio playback
tests:
- Integration: Test waveform sync with audio playback
- Performance: Measure real-time update overhead
- Visual: Verify progress highlighting matches audio position
acceptance_criteria:
- Waveform updates in real-time during playback
- Playback position is accurately highlighted
- No lag or desynchronization with audio
validation:
- Play audio and watch waveform update
- Verify progress bar matches audio position
- Test with different playback speeds
notes:
- Use existing audio position polling in `useAudio.ts`
- Update waveform every ~100ms for smooth visuals
- Consider reducing waveform resolution during playback for performance
- Ensure highlighting doesn't flicker

View File

@@ -0,0 +1,46 @@
# 09. Optimize Waveform Rendering Performance
meta:
id: merged-waveform-09
feature: merged-waveform
priority: P3
depends_on: [merged-waveform-08]
tags: [performance, optimization]
objective:
- Ensure waveform rendering doesn't cause performance issues
- Optimize for terminal TUI environment
- Minimize CPU and memory usage
deliverables:
- Performance optimizations
- Memory management for waveform data
- Performance monitoring and testing
steps:
1. Profile waveform rendering performance
2. Optimize data point generation and updates
3. Implement waveform data caching
4. Add performance monitoring
5. Test with long audio files
tests:
- Performance: Measure CPU usage during playback
- Performance: Measure memory usage over time
- Load test: Test with 30+ minute audio files
acceptance_criteria:
- Waveform rendering < 16ms per frame
- No memory leaks during extended playback
- Smooth playback even with waveform rendering
validation:
- Profile CPU usage during playback
- Monitor memory over 30-minute playback session
- Test with multiple simultaneous audio files
notes:
- Consider reducing waveform resolution during playback
- Cache waveform data to avoid regeneration
- Use efficient data structures for waveform points
- Test on slower terminals (e.g., tmux)

View File

@@ -0,0 +1,21 @@
# Merged Waveform Progress Bar
Objective: Create a real-time waveform visualization that expands from a progress bar during playback
Status legend: [ ] todo, [~] in-progress, [x] done
Tasks
- [ ] 06 — Implement audio waveform analysis → `06-waveform-audio-analysis.md`
- [ ] 07 — Create merged progress-waveform component → `07-merged-waveform-component.md`
- [ ] 08 — Implement real-time waveform rendering during playback → `08-realtime-waveform-rendering.md`
- [ ] 09 — Optimize waveform rendering performance → `09-waveform-performance-optimization.md`
Dependencies
- 06 -> 07
- 07 -> 08
- 08 -> 09
Exit criteria
- Waveform smoothly expands from progress bar during playback
- Waveform is highlighted based on current playback position
- No performance degradation during playback

View File

@@ -1,51 +0,0 @@
# 01. Initialize SolidJS OpenTUI Project with Bun
meta:
id: podcast-tui-app-01
feature: podcast-tui-app
priority: P0
depends_on: []
tags: [project-setup, solidjs, opentui, bun]
objective:
- Initialize a new SolidJS-based OpenTUI project using Bun
- Set up project structure with all necessary dependencies
- Configure TypeScript and development environment
deliverables:
- `/podcast-tui-app/` directory created
- `package.json` with all dependencies
- `tsconfig.json` configured for TypeScript
- `bunfig.toml` with Bun configuration
- `.gitignore` for Git version control
- `README.md` with project description
steps:
- Run `bunx create-tui@latest -t solid podcast-tui-app` to initialize the project
- Verify project structure is created correctly
- Install additional dependencies: `bun add @opentui/solid @opentui/core solid-js zustand`
- Install dev dependencies: `bun add -d @opentui/testing @opentui/react @opentui/solid`
- Configure TypeScript in `tsconfig.json` with SolidJS and OpenTUI settings
- Create `.gitignore` with Node and Bun specific files
- Initialize Git repository: `git init`
tests:
- Unit: Verify `create-tui` command works without errors
- Integration: Test that application can be started with `bun run src/index.tsx`
- Environment: Confirm all dependencies are installed correctly
acceptance_criteria:
- Project directory `podcast-tui-app/` exists
- `package.json` contains `@opentui/solid` and `@opentui/core` dependencies
- `bun run src/index.tsx` starts the application without errors
- TypeScript compilation works with `bun run build`
validation:
- Run `bun run build` to verify TypeScript compilation
- Run `bun run src/index.tsx` to start the application
- Run `bun pm ls` to verify all dependencies are installed
notes:
- OpenTUI is already installed in the system
- Use `-t solid` flag for SolidJS template
- All commands should be run from the project root

View File

@@ -1,55 +0,0 @@
# 02. Create Main App Shell with Tab Navigation
meta:
id: podcast-tui-app-02
feature: podcast-tui-app
priority: P0
depends_on: [01]
tags: [layout, navigation, solidjs, opentui]
objective:
- Create the main application shell with a responsive layout
- Implement tab-based navigation system
- Set up the root component structure
- Configure Flexbox layout for terminal UI
deliverables:
- `src/App.tsx` with main app shell
- `src/components/Navigation.tsx` with tab navigation
- `src/components/Layout.tsx` with responsive layout
- Tab navigation component with 5 tabs: Discover, My Feeds, Search, Player, Settings
steps:
- Create `src/App.tsx` with main component that renders Navigation
- Create `src/components/Navigation.tsx` with tab navigation
- Use `<box>` with `<scrollbox>` for navigation
- Implement tab selection state with `createSignal`
- Add keyboard navigation (arrow keys)
- Add tab labels: "Discover", "My Feeds", "Search", "Player", "Settings"
- Create `src/components/Layout.tsx` with responsive layout
- Use Flexbox with `flexDirection="column"`
- Create top navigation bar
- Create main content area
- Handle terminal resizing
- Update `src/index.tsx` to render the app
tests:
- Unit: Test Navigation component renders with correct tabs
- Integration: Test keyboard navigation moves between tabs
- Component: Verify layout adapts to terminal size changes
acceptance_criteria:
- Navigation component displays 5 tabs correctly
- Tab selection is visually indicated
- Arrow keys navigate between tabs
- Layout fits within terminal bounds
validation:
- Run application and verify navigation appears
- Press arrow keys to test navigation
- Resize terminal and verify layout adapts
notes:
- Use SolidJS `createSignal` for state management
- Follow OpenTUI layout patterns from `layout/REFERENCE.md`
- Navigation should be persistent across all screens

View File

@@ -1,64 +0,0 @@
# 03. Implement Direct File Sync (JSON/XML Import/Export)
meta:
id: podcast-tui-app-03
feature: podcast-tui-app
priority: P1
depends_on: [02]
tags: [file-sync, json, xml, import-export, solidjs]
objective:
- Create data models for JSON and XML sync formats
- Implement import functionality to load feeds and settings from files
- Implement export functionality to save feeds and settings to files
- Add file picker UI for selecting files
deliverables:
- `src/types/sync.ts` with sync data models
- `src/utils/sync.ts` with import/export functions
- `src/components/SyncPanel.tsx` with sync UI
- File picker component for file selection
steps:
- Create `src/types/sync.ts` with data models:
- `SyncData` interface for JSON format
- `SyncDataXML` interface for XML format
- Include fields: feeds, sources, settings, preferences
- Create `src/utils/sync.ts` with functions:
- `exportToJSON(data: SyncData): string`
- `importFromJSON(json: string): SyncData`
- `exportToXML(data: SyncDataXML): string`
- `importFromXML(xml: string): SyncDataXML`
- Handle validation and error checking
- Create `src/components/SyncPanel.tsx` with:
- Import button
- Export button
- File picker UI using `<input>` component
- Sync status indicator
- Add sync functionality to Settings screen
tests:
- Unit: Test JSON import/export with sample data
- Unit: Test XML import/export with sample data
- Integration: Test file picker selects correct files
- Integration: Test sync panel buttons work correctly
acceptance_criteria:
- Export creates valid JSON file with all data
- Export creates valid XML file with all data
- Import loads data from JSON file
- Import loads data from XML file
- File picker allows selecting files from disk
validation:
- Run `bun run src/index.tsx`
- Go to Settings > Sync
- Click Export, verify file created
- Click Import, select file, verify data loaded
- Test with both JSON and XML formats
notes:
- JSON format: Simple, human-readable
- XML format: More structured, better for complex data
- Use `FileReader` API for file operations
- Handle file not found and invalid format errors

View File

@@ -1,78 +0,0 @@
# 04. Build Optional Authentication System
meta:
id: podcast-tui-app-04
feature: podcast-tui-app
priority: P2
depends_on: [03]
tags: [authentication, optional, solidjs, security]
objective:
- Create authentication state management (disabled by default)
- Build simple login screen with email/password
- Implement 8-character code validation flow
- Add OAuth placeholder screens
- Create sync-only user profile
deliverables:
- `src/stores/auth.ts` with authentication state store
- `src/components/LoginScreen.tsx` with login form
- `src/components/CodeValidation.tsx` with 8-character code input
- `src/components/OAuthPlaceholder.tsx` with OAuth info
- `src/components/SyncProfile.tsx` with sync-only profile
steps:
- Create `src/stores/auth.ts` with Zustand store:
- `user` state (initially null)
- `isAuthenticated` state (initially false)
- `login()` function
- `logout()` function
- `validateCode()` function
- Create `src/components/LoginScreen.tsx`:
- Email input field
- Password input field
- Login button
- Link to code validation flow
- Link to OAuth placeholder
- Create `src/components/CodeValidation.tsx`:
- 8-character code input
- Validation logic (alphanumeric)
- Submit button
- Error message display
- Create `src/components/OAuthPlaceholder.tsx`:
- Display OAuth information
- Explain terminal limitations
- Link to browser redirect flow
- Create `src/components/SyncProfile.tsx`:
- User profile display
- Sync status indicator
- Profile management options
- Add auth screens to Navigation (hidden by default)
tests:
- Unit: Test authentication state management
- Unit: Test code validation logic
- Integration: Test login flow completes
- Integration: Test logout clears state
acceptance_criteria:
- Authentication is disabled by default (isAuthenticated = false)
- Login screen accepts email and password
- Code validation accepts 8-character codes
- OAuth placeholder displays limitations
- Sync profile shows user info
- Login state persists across sessions
validation:
- Run application and verify login screen is accessible
- Try to log in with valid credentials
- Try to log in with invalid credentials
- Test code validation with valid/invalid codes
- Verify authentication state persists
notes:
- Authentication is optional and disabled by default
- Focus on file sync, not user accounts
- Use simple validation (no real backend)
- Store authentication state in localStorage
- OAuth not feasible in terminal, document limitation

View File

@@ -1,58 +0,0 @@
# 05. Create Feed Data Models and Types
meta:
id: podcast-tui-app-05
feature: podcast-tui-app
priority: P0
depends_on: [04]
tags: [types, data-models, solidjs, typescript]
objective:
- Define TypeScript interfaces for all podcast-related data types
- Create models for feeds, episodes, sources, and user preferences
- Set up type definitions for sync functionality
deliverables:
- `src/types/podcast.ts` with all data models
- `src/types/episode.ts` with episode-specific types
- `src/types/source.ts` with podcast source types
- `src/types/preference.ts` with user preference types
steps:
- Create `src/types/podcast.ts` with core types:
- `Podcast` interface (id, title, description, coverUrl, feedUrl, lastUpdated)
- `Episode` interface (id, title, description, audioUrl, duration, pubDate, episodeNumber)
- `Feed` interface (id, podcast, episodes[], isPublic, sourceId)
- `FeedItem` interface (represents a single episode in a feed)
- Create `src/types/episode.ts` with episode types:
- `Episode` interface
- `EpisodeStatus` enum (playing, paused, completed)
- `Progress` interface (episodeId, position, duration)
- Create `src/types/source.ts` with source types:
- `PodcastSource` interface (id, name, baseUrl, type, apiKey)
- `SourceType` enum (rss, api, custom)
- `SearchQuery` interface (query, sourceIds, filters)
- Create `src/types/preference.ts` with preference types:
- `UserPreference` interface (theme, fontSize, playbackSpeed, autoDownload)
- `SyncPreference` interface (autoSync, backupInterval, syncMethod)
- Add type exports in `src/index.ts`
tests:
- Unit: Verify all interfaces compile correctly
- Unit: Test enum values are correct
- Integration: Test type definitions match expected data structures
acceptance_criteria:
- All TypeScript interfaces compile without errors
- Types are exported for use across the application
- Type definitions cover all podcast-related data
validation:
- Run `bun run build` to verify TypeScript compilation
- Check `src/types/` directory contains all required files
notes:
- Use strict TypeScript mode
- Include JSDoc comments for complex types
- Keep types simple and focused
- Ensure types are compatible with sync JSON/XML formats

View File

@@ -1,63 +0,0 @@
# 06. Build Feed List Component (Public/Private Feeds)
meta:
id: podcast-tui-app-06
feature: podcast-tui-app
priority: P0
depends_on: [05]
tags: [feed-list, components, solidjs, opentui]
objective:
- Create a scrollable feed list component
- Display public and private feeds
- Implement feed selection and display
- Add reverse chronological ordering
deliverables:
- `src/components/FeedList.tsx` with feed list component
- `src/components/FeedItem.tsx` with individual feed item
- `src/components/FeedFilter.tsx` with public/private toggle
steps:
- Create `src/components/FeedList.tsx`:
- Use `<scrollbox>` for scrollable list
- Accept feeds array as prop
- Implement feed rendering with `createSignal` for selection
- Add keyboard navigation (arrow keys, enter)
- Display feed title, description, episode count
- Create `src/components/FeedItem.tsx`:
- Display feed information
- Show public/private indicator
- Highlight selected feed
- Add hover effects
- Create `src/components/FeedFilter.tsx`:
- Toggle button for public/private feeds
- Filter logic implementation
- Update parent FeedList when filtered
- Add feed list to "My Feeds" navigation tab
tests:
- Unit: Test FeedList renders with feeds
- Unit: Test FeedItem displays correctly
- Integration: Test public/private filtering
- Integration: Test keyboard navigation in feed list
acceptance_criteria:
- Feed list displays all feeds correctly
- Public/private toggle filters feeds
- Feed selection is visually indicated
- Keyboard navigation works (arrow keys, enter)
- List scrolls properly when many feeds
validation:
- Run application and navigate to "My Feeds"
- Add some feeds and verify they appear
- Test public/private toggle
- Use arrow keys to navigate feeds
- Scroll list with many feeds
notes:
- Use SolidJS `createSignal` for selection state
- Follow OpenTUI component patterns from `components/REFERENCE.md`
- Feed list should be scrollable with many items
- Use Flexbox for layout

View File

@@ -1,68 +0,0 @@
# 07. Implement Multi-Source Search Interface
meta:
id: podcast-tui-app-07
feature: podcast-tui-app
priority: P1
depends_on: [06]
tags: [search, multi-source, solidjs, opentui]
objective:
- Create search input component
- Implement multi-source search functionality
- Display search results with sources
- Add search history with persistent storage
deliverables:
- `src/components/SearchBar.tsx` with search input
- `src/components/SearchResults.tsx` with results display
- `src/components/SearchHistory.tsx` with history list
- `src/utils/search.ts` with search logic
steps:
- Create `src/components/SearchBar.tsx`:
- Search input field using `<input>` component
- Search button
- Clear history button
- Enter key handler
- Create `src/utils/search.ts`:
- `searchPodcasts(query: string, sourceIds: string[]): Promise<Podcast[]>`
- `searchEpisodes(query: string, feedId: string): Promise<Episode[]>`
- Handle multiple sources
- Cache search results
- Create `src/components/SearchResults.tsx`:
- Display search results with source indicators
- Show podcast/episode info
- Add click to add to feeds
- Keyboard navigation through results
- Create `src/components/SearchHistory.tsx`:
- Display recent search queries
- Click to re-run search
- Delete individual history items
- Persist to localStorage
tests:
- Unit: Test search logic returns correct results
- Unit: Test search history persistence
- Integration: Test search bar accepts input
- Integration: Test results display correctly
acceptance_criteria:
- Search bar accepts and processes queries
- Multi-source search works across all enabled sources
- Search results display with source information
- Search history persists across sessions
- Keyboard navigation works in results
validation:
- Run application and navigate to "Search"
- Type a query and press Enter
- Verify results appear
- Click a result to add to feed
- Restart app and verify history persists
notes:
- Use localStorage for search history
- Implement basic caching to avoid repeated searches
- Handle empty results gracefully
- Add loading state during search

View File

@@ -1,63 +0,0 @@
# 08. Build Discover Feed with Popular Shows
meta:
id: podcast-tui-app-08
feature: podcast-tui-app
priority: P1
depends_on: [07]
tags: [discover, popular-shows, solidjs, opentui]
objective:
- Create popular shows data structure
- Build discover page component
- Display trending shows with categories
- Implement category filtering
deliverables:
- `src/data/popular-shows.ts` with trending shows data
- `src/components/DiscoverPage.tsx` with discover UI
- `src/components/CategoryFilter.tsx` with category buttons
steps:
- Create `src/data/popular-shows.ts`:
- Array of popular podcasts with metadata
- Categories: Technology, Business, Science, Entertainment
- Reverse chronological ordering (newest first)
- Include feed URLs and descriptions
- Create `src/components/DiscoverPage.tsx`:
- Title header
- Category filter buttons
- Grid/list display of popular shows
- Show details on selection
- Add to feed button
- Create `src/components/CategoryFilter.tsx`:
- Category button group
- Active category highlighting
- Filter logic implementation
- Add discover page to "Discover" navigation tab
tests:
- Unit: Test popular shows data structure
- Unit: Test category filtering
- Integration: Test discover page displays correctly
- Integration: Test add to feed functionality
acceptance_criteria:
- Discover page displays popular shows
- Category filtering works correctly
- Shows are ordered reverse chronologically
- Clicking a show shows details
- Add to feed button works
validation:
- Run application and navigate to "Discover"
- Verify popular shows appear
- Click different categories
- Click a show and verify details
- Try add to feed
notes:
- Popular shows data can be static or fetched from sources
- If sources don't provide trending, use curated list
- Categories help users find shows by topic
- Use Flexbox for category filter layout

View File

@@ -1,70 +0,0 @@
# 09. Create Player UI with Waveform Visualization
meta:
id: podcast-tui-app-09
feature: podcast-tui-app
priority: P0
depends_on: [08]
tags: [player, waveform, visualization, solidjs, opentui]
objective:
- Create player UI layout
- Implement playback controls (play/pause, skip, progress)
- Build ASCII waveform visualization
- Add progress tracking and seek functionality
deliverables:
- `src/components/Player.tsx` with player UI
- `src/components/PlaybackControls.tsx` with controls
- `src/components/Waveform.tsx` with ASCII waveform
- `src/utils/waveform.ts` with visualization logic
steps:
- Create `src/components/Player.tsx`:
- Player header with episode info
- Progress bar with seek functionality
- Waveform visualization area
- Playback controls
- Create `src/components/PlaybackControls.tsx`:
- Play/Pause button
- Previous/Next episode buttons
- Volume control
- Speed control
- Keyboard shortcuts (space, arrows)
- Create `src/components/Waveform.tsx`:
- ASCII waveform visualization
- Click to seek
- Color-coded for played/paused
- Use frame buffer for drawing
- Create `src/utils/waveform.ts`:
- Generate waveform data from audio
- Convert to ASCII characters
- Handle audio duration and position
- Add player to "Player" navigation tab
tests:
- Unit: Test waveform generation
- Unit: Test playback controls
- Integration: Test player UI displays correctly
- Integration: Test keyboard shortcuts work
acceptance_criteria:
- Player UI displays episode information
- Playback controls work (play/pause, skip)
- Waveform visualization shows audio waveform
- Progress bar updates during playback
- Clicking waveform seeks to position
validation:
- Run application and navigate to "Player"
- Select an episode to play
- Test playback controls
- Verify waveform visualization
- Test seeking by clicking waveform
- Test keyboard shortcuts
notes:
- ASCII waveform: Use `#` for peaks, `.` for valleys
- Audio integration: Trigger system player or use Web Audio API
- Waveform data needs to be cached for performance
- Use SolidJS `useTimeline` for animation

View File

@@ -1,70 +0,0 @@
# 10. Build Settings Screen and Preferences
meta:
id: podcast-tui-app-10
feature: podcast-tui-app
priority: P1
depends_on: [09]
tags: [settings, preferences, solidjs, opentui]
objective:
- Create settings screen component
- Add source management UI
- Build user preferences panel
- Implement data persistence
deliverables:
- `src/components/SettingsScreen.tsx` with settings UI
- `src/components/SourceManager.tsx` with source management
- `src/components/PreferencesPanel.tsx` with user preferences
- `src/utils/persistence.ts` with localStorage utilities
steps:
- Create `src/components/SettingsScreen.tsx`:
- Settings menu with sections
- Navigation between settings sections
- Save/Cancel buttons
- Create `src/components/SourceManager.tsx`:
- List of enabled sources
- Add source button
- Remove source button
- Enable/disable toggle
- Create `src/components/PreferencesPanel.tsx`:
- Theme selection (light/dark)
- Font size control
- Playback speed control
- Auto-download settings
- Create `src/utils/persistence.ts`:
- `savePreference(key, value)`
- `loadPreference(key)`
- `saveFeeds(feeds)`
- `loadFeeds()`
- `saveSettings(settings)`
- `loadSettings()`
- Add settings screen to "Settings" navigation tab
tests:
- Unit: Test persistence functions
- Unit: Test source management
- Integration: Test settings screen navigation
- Integration: Test preferences save/load
acceptance_criteria:
- Settings screen displays all sections
- Source management adds/removes sources
- Preferences save correctly
- Data persists across sessions
- Settings screen is accessible
validation:
- Run application and navigate to "Settings"
- Test source management
- Change preferences and verify save
- Restart app and verify preferences persist
notes:
- Use localStorage for simple persistence
- Settings are application-level, not user-specific
- Source management requires API keys if needed
- Preferences are per-user
- Add error handling for persistence failures

View File

@@ -1,68 +0,0 @@
# 11. Create Global State Store and Data Layer
meta:
id: podcast-tui-app-11
feature: podcast-tui-app
priority: P0
depends_on: [10]
tags: [state-management, global-store, signals, solidjs]
objective:
- Create global state store using Signals
- Implement data fetching and caching
- Build file-based storage for sync
- Connect all components to shared state
deliverables:
- `src/stores/appStore.ts` with global state store
- `src/stores/feedStore.ts` with feed state
- `src/stores/playerStore.ts` with player state
- `src/stores/searchStore.ts` with search state
- `src/utils/storage.ts` with file-based storage
steps:
- Create `src/stores/appStore.ts`:
- Use SolidJS signals for global state
- Store application state: currentTab, isAuthEnabled, settings
- Provide state to all child components
- Create `src/stores/feedStore.ts`:
- Signals for feeds array
- Signals for selectedFeed
- Methods: addFeed, removeFeed, updateFeed
- Create `src/stores/playerStore.ts`:
- Signals for currentEpisode
- Signals for playbackState
- Methods: play, pause, seek, setSpeed
- Create `src/stores/searchStore.ts`:
- Signals for searchResults
- Signals for searchHistory
- Methods: search, addToHistory, clearHistory
- Create `src/utils/storage.ts`:
- `saveToLocal()`
- `loadFromLocal()`
- File-based sync for feeds and settings
tests:
- Unit: Test store methods update signals correctly
- Unit: Test storage functions
- Integration: Test state persists across components
- Integration: Test data sync with file storage
acceptance_criteria:
- Global state store manages all app state
- Store methods update signals correctly
- State persists across component re-renders
- File-based storage works for sync
validation:
- Run application and verify state is initialized
- Modify state and verify UI updates
- Restart app and verify state persistence
- Test sync functionality
notes:
- Use SolidJS `createSignal` for reactivity
- Store should be singleton pattern
- Use Zustand if complex state management needed
- Keep store simple and focused
- File-based storage for sync with JSON/XML

View File

@@ -1,68 +0,0 @@
# 12. Set Up Testing Framework and Write Tests
meta:
id: podcast-tui-app-12
feature: podcast-tui-app
priority: P1
depends_on: [11]
tags: [testing, snapshot-testing, solidjs, opentui]
objective:
- Set up OpenTUI testing framework
- Write component tests for all major components
- Add keyboard interaction tests
- Implement error handling tests
deliverables:
- `tests/` directory with test files
- `tests/components/` with component tests
- `tests/integration/` with integration tests
- `tests/utils/` with utility tests
- Test coverage for all components
steps:
- Set up OpenTUI testing framework:
- Install testing dependencies
- Configure test runner
- Set up snapshot testing
- Write component tests:
- `tests/components/Navigation.test.tsx`
- `tests/components/FeedList.test.tsx`
- `tests/components/SearchBar.test.tsx`
- `tests/components/Player.test.tsx`
- `tests/components/SettingsScreen.test.tsx`
- Write integration tests:
- `tests/integration/navigation.test.tsx`
- `tests/integration/feed-management.test.tsx`
- `tests/integration/search.test.tsx`
- Write utility tests:
- `tests/utils/sync.test.ts`
- `tests/utils/search.test.ts`
- `tests/utils/storage.test.ts`
- Add error handling tests:
- Test invalid file imports
- Test network errors
- Test malformed data
tests:
- Unit: Run all unit tests
- Integration: Run all integration tests
- Coverage: Verify all components tested
acceptance_criteria:
- All tests pass
- Test coverage > 80%
- Snapshot tests match expected output
- Error handling tests verify proper behavior
validation:
- Run `bun test` to execute all tests
- Run `bun test --coverage` for coverage report
- Fix any failing tests
notes:
- Use OpenTUI's testing framework for snapshot testing
- Test keyboard interactions separately
- Mock external dependencies (API calls)
- Keep tests fast and focused
- Add CI/CD integration for automated testing

View File

@@ -1,63 +0,0 @@
# 13. Set Up TypeScript Configuration and Build System
meta:
id: podcast-tui-app-13
feature: podcast-tui-app
priority: P0
depends_on: [01]
tags: [typescript, build-system, configuration, solidjs]
objective:
- Configure TypeScript for SolidJS and OpenTUI
- Set up build system for production
- Configure bundler for optimal output
- Add development and production scripts
deliverables:
- `tsconfig.json` with TypeScript configuration
- `bunfig.toml` with Bun configuration
- `package.json` with build scripts
- `.bunfig.toml` with build settings
steps:
- Configure TypeScript in `tsconfig.json`:
- Set target to ES2020
- Configure paths for SolidJS
- Add OpenTUI type definitions
- Enable strict mode
- Configure module resolution
- Configure Bun in `bunfig.toml`:
- Set up dependencies
- Configure build output
- Add optimization flags
- Update `package.json`:
- Add build script: `bun run build`
- Add dev script: `bun run dev`
- Add test script: `bun run test`
- Add lint script: `bun run lint`
- Configure bundler for production:
- Optimize bundle size
- Minify output
- Tree-shake unused code
tests:
- Unit: Verify TypeScript configuration
- Integration: Test build process
- Integration: Test dev script
acceptance_criteria:
- TypeScript compiles without errors
- Build script creates optimized bundle
- Dev script runs development server
- All scripts work correctly
validation:
- Run `bun run build` to verify build
- Run `bun run dev` to verify dev server
- Check bundle size is reasonable
notes:
- Use `bun build` for production builds
- Enable source maps for debugging
- Configure TypeScript to match OpenTUI requirements
- Add path aliases for cleaner imports

View File

@@ -1,70 +0,0 @@
# 14. Create Project Directory Structure and Dependencies
meta:
id: podcast-tui-app-14
feature: podcast-tui-app
priority: P0
depends_on: [01]
tags: [project-structure, organization, solidjs]
objective:
- Create organized project directory structure
- Set up all necessary folders and files
- Install and configure all dependencies
- Create placeholder files for future implementation
deliverables:
- `src/` directory with organized structure
- `src/components/` with component folders
- `src/stores/` with store files
- `src/types/` with type definitions
- `src/utils/` with utility functions
- `src/data/` with static data
- `tests/` with test structure
- `public/` for static assets
- `docs/` for documentation
steps:
- Create directory structure:
- `src/components/` (all components)
- `src/stores/` (state management)
- `src/types/` (TypeScript types)
- `src/utils/` (utility functions)
- `src/data/` (static data)
- `src/hooks/` (custom hooks)
- `src/api/` (API clients)
- `tests/` (test files)
- `public/` (static assets)
- `docs/` (documentation)
- Create placeholder files:
- `src/index.tsx` (main entry)
- `src/App.tsx` (app shell)
- `src/main.ts` (Bun entry)
- Install dependencies:
- Core: `@opentui/solid`, `@opentui/core`, `solid-js`
- State: `zustand`
- Testing: `@opentui/testing`
- Utilities: `date-fns`, `uuid`
- Optional: `react`, `@opentui/react` (for testing)
tests:
- Unit: Verify directory structure exists
- Integration: Verify all dependencies installed
- Component: Verify placeholder files exist
acceptance_criteria:
- All directories created
- All placeholder files exist
- All dependencies installed
- Project structure follows conventions
validation:
- Run `ls -R src/` to verify structure
- Run `bun pm ls` to verify dependencies
- Check all placeholder files exist
notes:
- Follow OpenTUI project structure conventions
- Keep structure organized and scalable
- Add comments to placeholder files
- Consider adding `eslint`, `prettier` for code quality

View File

@@ -1,63 +0,0 @@
# 15. Build Responsive Layout System (Flexbox)
meta:
id: podcast-tui-app-15
feature: podcast-tui-app
priority: P0
depends_on: [14]
tags: [layout, flexbox, responsive, solidjs, opentui]
objective:
- Create reusable Flexbox layout components
- Handle terminal resizing gracefully
- Ensure responsive design across different terminal sizes
- Build layout patterns for common UI patterns
deliverables:
- `src/components/BoxLayout.tsx` with Flexbox container
- `src/components/Row.tsx` with horizontal layout
- `src/components/Column.tsx` with vertical layout
- `src/components/ResponsiveContainer.tsx` with resizing logic
steps:
- Create `src/components/BoxLayout.tsx`:
- Generic Flexbox container
- Props: flexDirection, justifyContent, alignItems, gap
- Use OpenTUI layout patterns
- Create `src/components/Row.tsx`:
- Horizontal layout (flexDirection="row")
- Props for spacing and alignment
- Handle overflow
- Create `src/components/Column.tsx`:
- Vertical layout (flexDirection="column")
- Props for spacing and alignment
- Scrollable content area
- Create `src/components/ResponsiveContainer.tsx`:
- Listen to terminal resize events
- Adjust layout based on width/height
- Handle different screen sizes
- Add usage examples and documentation
tests:
- Unit: Test BoxLayout with different props
- Unit: Test Row and Column layouts
- Integration: Test responsive behavior on resize
- Integration: Test layouts fit within terminal bounds
acceptance_criteria:
- BoxLayout renders with correct Flexbox properties
- Row and Column layouts work correctly
- ResponsiveContainer adapts to terminal resize
- All layouts fit within terminal bounds
validation:
- Run application and test layouts
- Resize terminal and verify responsive behavior
- Test with different terminal sizes
- Check layouts don't overflow
notes:
- Use OpenTUI Flexbox patterns from `layout/REFERENCE.md`
- Terminal dimensions: width (columns) x height (rows)
- Handle edge cases (very small screens)
- Add comments explaining layout decisions

View File

@@ -1,60 +0,0 @@
# 16. Implement Tab Navigation Component
meta:
id: podcast-tui-app-16
feature: podcast-tui-app
priority: P0
depends_on: [15]
tags: [navigation, tabs, solidjs, opentui]
objective:
- Create reusable tab navigation component
- Implement tab selection state
- Add keyboard navigation for tabs
- Handle active tab highlighting
deliverables:
- `src/components/TabNavigation.tsx` with tab navigation
- `src/components/Tab.tsx` with individual tab component
- `src/hooks/useTabNavigation.ts` with tab logic
steps:
- Create `src/components/TabNavigation.tsx`:
- Accept tabs array as prop
- Render tab buttons
- Manage selected tab state
- Update parent component on tab change
- Create `src/components/Tab.tsx`:
- Individual tab button
- Highlight selected tab
- Handle click and keyboard events
- Show tab icon if needed
- Create `src/hooks/useTabNavigation.ts`:
- Manage tab selection state
- Handle keyboard navigation (arrow keys)
- Update parent component
- Persist selected tab
tests:
- Unit: Test Tab component renders correctly
- Unit: Test tab selection updates state
- Integration: Test keyboard navigation
- Integration: Test tab persists across renders
acceptance_criteria:
- TabNavigation displays all tabs correctly
- Tab selection is visually indicated
- Keyboard navigation works (arrow keys, enter)
- Active tab persists
validation:
- Run application and verify tabs appear
- Click tabs to test selection
- Use arrow keys to navigate
- Restart app and verify active tab persists
notes:
- Use SolidJS `createSignal` for tab state
- Follow OpenTUI component patterns
- Tabs: Discover, My Feeds, Search, Player, Settings
- Add keyboard shortcuts documentation

View File

@@ -1,69 +0,0 @@
# 17. Add Keyboard Shortcuts and Navigation Handling
meta:
id: podcast-tui-app-17
feature: podcast-tui-app
priority: P1
depends_on: [16]
tags: [keyboard, shortcuts, navigation, solidjs, opentui]
objective:
- Implement global keyboard shortcuts
- Add shortcut documentation
- Handle keyboard events across components
- Prevent conflicts with input fields
deliverables:
- `src/components/KeyboardHandler.tsx` with global shortcuts
- `src/components/ShortcutHelp.tsx` with shortcut list
- `src/hooks/useKeyboard.ts` with keyboard utilities
- `src/config/shortcuts.ts` with shortcut definitions
steps:
- Create `src/config/shortcuts.ts`:
- Define all keyboard shortcuts
- Map keys to actions
- Document each shortcut
- Create `src/hooks/useKeyboard.ts`:
- Global keyboard event listener
- Filter events based on focused element
- Handle modifier keys (Ctrl, Shift, Alt)
- Prevent default browser behavior
- Create `src/components/KeyboardHandler.tsx`:
- Wrap application with keyboard handler
- Handle escape to close modals
- Handle Ctrl+Q to quit
- Handle Ctrl+S to save
- Create `src/components/ShortcutHelp.tsx`:
- Display all shortcuts
- Organize by category
- Click to copy shortcut
tests:
- Unit: Test keyboard hook handles events
- Unit: Test modifier key combinations
- Integration: Test shortcuts work globally
- Integration: Test shortcuts don't interfere with inputs
acceptance_criteria:
- All shortcuts work as defined
- Shortcuts help displays correctly
- Shortcuts don't interfere with input fields
- Escape closes modals
validation:
- Run application and test each shortcut
- Try shortcuts in input fields (shouldn't trigger)
- Check ShortcutHelp displays all shortcuts
- Test Ctrl+Q quits app
notes:
- Use OpenTUI keyboard patterns from `keyboard/REFERENCE.md`
- Common shortcuts:
- Ctrl+Q: Quit
- Ctrl+S: Save
- Escape: Close modal/exit input
- Arrow keys: Navigate
- Space: Play/Pause
- Document shortcuts in README
- Test on different terminals

View File

@@ -1,65 +0,0 @@
# 18. Create Sync Data Models (JSON/XML Formats)
meta:
id: podcast-tui-app-18
feature: podcast-tui-app
priority: P1
depends_on: [17]
tags: [data-models, json, xml, sync, typescript]
objective:
- Define TypeScript interfaces for JSON sync format
- Define TypeScript interfaces for XML sync format
- Ensure compatibility between formats
- Add validation logic
deliverables:
- `src/types/sync-json.ts` with JSON sync types
- `src/types/sync-xml.ts` with XML sync types
- `src/utils/sync-validation.ts` with validation logic
- `src/constants/sync-formats.ts` with format definitions
steps:
- Create `src/types/sync-json.ts`:
- `SyncData` interface with all required fields
- Include feeds, sources, settings, preferences
- Add version field for format compatibility
- Add timestamp for last sync
- Create `src/types/sync-xml.ts`:
- `SyncDataXML` interface
- XML-compatible type definitions
- Root element and child elements
- Attributes for metadata
- Create `src/utils/sync-validation.ts`:
- `validateJSONSync(data: unknown): SyncData`
- `validateXMLSync(data: unknown): SyncDataXML`
- Field validation functions
- Type checking
- Create `src/constants/sync-formats.ts`:
- JSON format version
- XML format version
- Supported versions list
- Format extensions
tests:
- Unit: Test JSON validation with valid/invalid data
- Unit: Test XML validation with valid/invalid data
- Integration: Test format compatibility
acceptance_criteria:
- JSON sync types compile without errors
- XML sync types compile without errors
- Validation rejects invalid data
- Format versions are tracked
validation:
- Run `bun run build` to verify TypeScript
- Test validation with sample data
- Test with invalid data to verify rejection
notes:
- JSON format: Simple, human-readable
- XML format: More structured, better for complex data
- Include all necessary fields for complete sync
- Add comments explaining each field
- Ensure backward compatibility

View File

@@ -1,72 +0,0 @@
# 19. Build Import/Export Functionality
meta:
id: podcast-tui-app-19
feature: podcast-tui-app
priority: P1
depends_on: [18]
tags: [import-export, file-io, sync, solidjs]
objective:
- Implement JSON export functionality
- Implement JSON import functionality
- Implement XML export functionality
- Implement XML import functionality
- Handle file operations and errors
deliverables:
- `src/utils/sync.ts` with import/export functions
- `src/components/ExportDialog.tsx` with export UI
- `src/components/ImportDialog.tsx` with import UI
- Error handling components
steps:
- Implement JSON export in `src/utils/sync.ts`:
- `exportFeedsToJSON(feeds: Feed[]): string`
- `exportSettingsToJSON(settings: Settings): string`
- Combine into `exportToJSON(data: SyncData): string`
- Implement JSON import in `src/utils/sync.ts`:
- `importFeedsFromJSON(json: string): Feed[]`
- `importSettingsFromJSON(json: string): Settings`
- Combine into `importFromJSON(json: string): SyncData`
- Implement XML export in `src/utils/sync.ts`:
- `exportToXML(data: SyncDataXML): string`
- XML serialization
- Implement XML import in `src/utils/sync.ts`:
- `importFromXML(xml: string): SyncDataXML`
- XML parsing
- Create `src/components/ExportDialog.tsx`:
- File name input
- Export format selection
- Export button
- Success message
- Create `src/components/ImportDialog.tsx`:
- File picker
- Format detection
- Import button
- Error message display
tests:
- Unit: Test JSON import/export with sample data
- Unit: Test XML import/export with sample data
- Unit: Test error handling
- Integration: Test file operations
acceptance_criteria:
- Export creates valid files
- Import loads data correctly
- Errors are handled gracefully
- Files can be opened in text editors
validation:
- Run application and test export/import
- Open exported files in text editor
- Test with different data sizes
- Test error cases (invalid files)
notes:
- Use `FileReader` API for file operations
- Handle file not found, invalid format, permission errors
- Add progress indicator for large files
- Support both JSON and XML formats
- Ensure data integrity during import

View File

@@ -1,61 +0,0 @@
# 20. Create File Picker UI for Import
meta:
id: podcast-tui-app-20
feature: podcast-tui-app
priority: P1
depends_on: [19]
tags: [file-picker, input, ui, solidjs, opentui]
objective:
- Create file picker component for selecting import files
- Implement file format detection
- Display file information
- Handle file selection and validation
deliverables:
- `src/components/FilePicker.tsx` with file picker UI
- `src/components/FileInfo.tsx` with file details
- `src/utils/file-detector.ts` with format detection
steps:
- Create `src/utils/file-detector.ts`:
- `detectFormat(file: File): 'json' | 'xml' | 'unknown'`
- Read file header
- Check file extension
- Validate format
- Create `src/components/FilePicker.tsx`:
- File input using `<input>` component
- Accept JSON and XML files
- File selection handler
- Clear button
- Create `src/components/FileInfo.tsx`:
- Display file name
- Display file size
- Display file format
- Display last modified date
- Add file picker to import dialog
tests:
- Unit: Test format detection
- Unit: Test file picker selection
- Integration: Test file validation
acceptance_criteria:
- File picker allows selecting files
- Format detection works correctly
- File information is displayed
- Invalid files are rejected
validation:
- Run application and test file picker
- Select valid files
- Select invalid files
- Verify format detection
notes:
- Use OpenTUI `<input>` component for file picker
- Accept `.json` and `.xml` extensions
- Check file size limit (e.g., 10MB)
- Add file type validation
- Handle file selection errors

View File

@@ -1,60 +0,0 @@
# 21. Build Sync Status Indicator
meta:
id: podcast-tui-app-21
feature: podcast-tui-app
priority: P1
depends_on: [20]
tags: [status-indicator, sync, ui, solidjs]
objective:
- Create sync status indicator component
- Display sync state (idle, syncing, complete, error)
- Show sync progress
- Provide sync status in settings
deliverables:
- `src/components/SyncStatus.tsx` with status indicator
- `src/components/SyncProgress.tsx` with progress bar
- `src/components/SyncError.tsx` with error display
steps:
- Create `src/components/SyncStatus.tsx`:
- Display current sync state
- Show status icon (spinner, check, error)
- Show status message
- Auto-update based on state
- Create `src/components/SyncProgress.tsx`:
- Progress bar visualization
- Percentage display
- Step indicators
- Animation
- Create `src/components/SyncError.tsx`:
- Error message display
- Retry button
- Error details (expandable)
- Add sync status to settings screen
tests:
- Unit: Test status indicator updates correctly
- Unit: Test progress bar visualization
- Unit: Test error display
acceptance_criteria:
- Status indicator shows correct state
- Progress bar updates during sync
- Error message is displayed on errors
- Status persists across sync operations
validation:
- Run application and test sync operations
- Trigger export/import
- Verify status indicator updates
- Test error cases
notes:
- Use SolidJS signals for state
- Status states: idle, syncing, complete, error
- Use ASCII icons for status indicators
- Add animation for syncing state
- Make status component reusable

View File

@@ -1,67 +0,0 @@
# 22. Add Backup/Restore Functionality
meta:
id: podcast-tui-app-22
feature: podcast-tui-app
priority: P1
depends_on: [21]
tags: [backup-restore, sync, data-protection, solidjs]
objective:
- Implement backup functionality for all user data
- Implement restore functionality
- Create scheduled backups
- Add backup management UI
deliverables:
- `src/utils/backup.ts` with backup functions
- `src/utils/restore.ts` with restore functions
- `src/components/BackupManager.tsx` with backup UI
- `src/components/ScheduledBackups.tsx` with backup settings
steps:
- Create `src/utils/backup.ts`:
- `createBackup(): Promise<string>`
- `backupFeeds(feeds: Feed[]): string`
- `backupSettings(settings: Settings): string`
- Include all user data
- Create `src/utils/restore.ts`:
- `restoreFromBackup(backupData: string): Promise<void>`
- `restoreFeeds(backupData: string): void`
- `restoreSettings(backupData: string): void`
- Validate backup data
- Create `src/components/BackupManager.tsx`:
- List of backup files
- Restore button
- Delete backup button
- Create new backup button
- Create `src/components/ScheduledBackups.tsx`:
- Enable/disable scheduled backups
- Backup interval selection
- Last backup time display
- Manual backup button
tests:
- Unit: Test backup creates valid files
- Unit: Test restore loads data correctly
- Unit: Test backup validation
- Integration: Test backup/restore workflow
acceptance_criteria:
- Backup creates complete backup file
- Restore loads all data correctly
- Scheduled backups work as configured
- Backup files can be managed
validation:
- Run application and create backup
- Restore from backup
- Test scheduled backups
- Verify data integrity
notes:
- Backup file format: JSON with timestamp
- Include version info for compatibility
- Store backups in `backups/` directory
- Add backup encryption option (optional)
- Test with large data sets

View File

@@ -1,61 +0,0 @@
# 23. Create Authentication State (Disabled by Default)
meta:
id: podcast-tui-app-23
feature: podcast-tui-app
priority: P2
depends_on: [22]
tags: [authentication, state, solidjs, security]
objective:
- Create authentication state management
- Ensure authentication is disabled by default
- Set up user state structure
- Implement auth state persistence
deliverables:
- `src/stores/auth.ts` with authentication store
- `src/types/auth.ts` with auth types
- `src/config/auth.ts` with auth configuration
steps:
- Create `src/types/auth.ts`:
- `User` interface (id, email, name, createdAt)
- `AuthState` interface (user, isAuthenticated, isLoading)
- `AuthError` interface (code, message)
- Create `src/config/auth.ts`:
- `DEFAULT_AUTH_ENABLED = false`
- `AUTH_CONFIG` with settings
- `OAUTH_PROVIDERS` with provider info
- Create `src/stores/auth.ts`:
- `createAuthStore()` with Zustand
- `user` signal (initially null)
- `isAuthenticated` signal (initially false)
- `login()` function (placeholder)
- `logout()` function
- `validateCode()` function
- Persist state to localStorage
- Set up auth state in global store
tests:
- Unit: Test auth state initializes correctly
- Unit: Test auth is disabled by default
- Unit: Test persistence
acceptance_criteria:
- Authentication is disabled by default
- Auth store manages state correctly
- State persists across sessions
- Auth is optional and not required
validation:
- Run application and verify auth is disabled
- Check localStorage for auth state
- Test login flow (should not work without backend)
notes:
- Authentication is secondary to file sync
- No real backend, just UI/UX
- Focus on sync features
- User can choose to enable auth later
- Store auth state in localStorage

View File

@@ -1,69 +0,0 @@
# 24. Build Simple Login Screen (Email/Password)
meta:
id: podcast-tui-app-24
feature: podcast-tui-app
priority: P2
depends_on: [23]
tags: [login, auth, form, solidjs, opentui]
objective:
- Create login screen component
- Implement email input field
- Implement password input field
- Add login validation and error handling
deliverables:
- `src/components/LoginScreen.tsx` with login form
- `src/components/EmailInput.tsx` with email field
- `src/components/PasswordInput.tsx` with password field
- `src/components/LoginButton.tsx` with submit button
steps:
- Create `src/components/EmailInput.tsx`:
- Email input field using `<input>` component
- Email validation regex
- Error message display
- Focus state styling
- Create `src/components/PasswordInput.tsx`:
- Password input field
- Show/hide password toggle
- Password validation rules
- Error message display
- Create `src/components/LoginButton.tsx`:
- Submit button
- Loading state
- Disabled state
- Error state
- Create `src/components/LoginScreen.tsx`:
- Combine inputs and button
- Login form validation
- Error handling
- Link to code validation
- Link to OAuth placeholder
tests:
- Unit: Test email validation
- Unit: Test password validation
- Unit: Test login form submission
- Integration: Test login screen displays correctly
acceptance_criteria:
- Login screen accepts email and password
- Validation works correctly
- Error messages display properly
- Form submission handled
validation:
- Run application and navigate to login
- Enter valid credentials
- Enter invalid credentials
- Test error handling
notes:
- Use OpenTUI `<input>` component
- Email validation: regex pattern
- Password validation: minimum length
- No real authentication, just UI
- Link to code validation for sync
- Link to OAuth placeholder

View File

@@ -1,60 +0,0 @@
# 25. Implement 8-Character Code Validation Flow
meta:
id: podcast-tui-app-25
feature: podcast-tui-app
priority: P2
depends_on: [24]
tags: [code-validation, auth, sync, solidjs]
objective:
- Create 8-character code input component
- Implement code validation logic
- Handle code submission
- Show validation results
deliverables:
- `src/components/CodeInput.tsx` with code field
- `src/utils/code-validator.ts` with validation logic
- `src/components/CodeValidationResult.tsx` with result display
steps:
- Create `src/utils/code-validator.ts`:
- `validateCode(code: string): boolean`
- Check length (8 characters)
- Check format (alphanumeric)
- Validate against stored codes
- Create `src/components/CodeInput.tsx`:
- 8-character code input
- Auto-formatting
- Clear button
- Validation error display
- Create `src/components/CodeValidationResult.tsx`:
- Success message
- Error message
- Retry button
- Link to alternative auth
tests:
- Unit: Test code validation logic
- Unit: Test code input formatting
- Unit: Test validation result display
acceptance_criteria:
- Code input accepts 8 characters
- Validation checks length and format
- Validation results display correctly
- Error handling works
validation:
- Run application and test code validation
- Enter valid 8-character code
- Enter invalid code
- Test validation error display
notes:
- Code format: alphanumeric (A-Z, 0-9)
- No real backend validation
- Store codes in localStorage for testing
- Link to OAuth placeholder
- Link to email/password login

View File

@@ -1,61 +0,0 @@
# 26. Add OAuth Placeholder Screens (Document Limitations)
meta:
id: podcast-tui-app-26
feature: podcast-tui-app
priority: P2
depends_on: [25]
tags: [oauth, documentation, placeholders, solidjs]
objective:
- Create OAuth placeholder screens
- Document terminal limitations for OAuth
- Provide alternative authentication methods
- Explain browser redirect flow
deliverables:
- `src/components/OAuthPlaceholder.tsx` with OAuth info
- `src/components/BrowserRedirect.tsx` with redirect flow
- `src/docs/oauth-limitations.md` with documentation
steps:
- Create `src/components/OAuthPlaceholder.tsx`:
- Display OAuth information
- Explain terminal limitations
- Show supported providers (Google, Apple)
- Link to browser redirect flow
- Create `src/components/BrowserRedirect.tsx`:
- Display QR code for mobile
- Display 8-character code
- Instructions for browser flow
- Link to website
- Create `src/docs/oauth-limitations.md`:
- Detailed explanation of OAuth in terminal
- Why OAuth is not feasible
- Alternative authentication methods
- Browser redirect flow instructions
- Add OAuth placeholder to login screen
tests:
- Unit: Test OAuth placeholder displays correctly
- Unit: Test browser redirect flow displays
- Documentation review
acceptance_criteria:
- OAuth placeholder screens display correctly
- Limitations are clearly documented
- Alternative methods are provided
- Browser redirect flow is explained
validation:
- Run application and navigate to OAuth placeholder
- Read documentation
- Verify flow instructions are clear
notes:
- OAuth in terminal is not feasible
- Terminal cannot handle OAuth flows
- Document this limitation clearly
- Provide browser redirect as alternative
- User can still use file sync
- Google and Apple OAuth are supported by browser

View File

@@ -1,62 +0,0 @@
# 27. Create Sync-Only User Profile
meta:
id: podcast-tui-app-27
feature: podcast-tui-app
priority: P2
depends_on: [26]
tags: [profile, sync, user-info, solidjs]
objective:
- Create user profile component for sync-only users
- Display user information
- Show sync status
- Provide profile management options
deliverables:
- `src/components/SyncProfile.tsx` with user profile
- `src/components/SyncStatus.tsx` with sync status
- `src/components/ProfileSettings.tsx` with profile settings
steps:
- Create `src/components/SyncProfile.tsx`:
- User avatar/icon
- User name display
- Email display
- Sync status indicator
- Profile actions
- Create `src/components/SyncStatus.tsx`:
- Sync status (last sync time)
- Sync method (file-based)
- Sync frequency
- Sync history
- Create `src/components/ProfileSettings.tsx`:
- Edit profile
- Change password
- Manage sync settings
- Export data
- Add profile to settings screen
tests:
- Unit: Test profile displays correctly
- Unit: Test sync status updates
- Integration: Test profile settings
acceptance_criteria:
- Profile displays user information
- Sync status is shown
- Profile settings work correctly
- Profile is accessible from settings
validation:
- Run application and navigate to profile
- View profile information
- Test profile settings
- Verify sync status
notes:
- Profile for sync-only users
- No authentication required
- Profile data stored in localStorage
- Sync status shows last sync time
- Profile is optional

View File

@@ -1,56 +0,0 @@
# 28. Create Feed Data Models and Types
meta:
id: podcast-tui-app-28
feature: podcast-tui-app
priority: P0
depends_on: [27]
tags: [types, data-models, solidjs, typescript]
objective:
- Define TypeScript interfaces for all podcast-related data
- Create models for feeds, episodes, sources
- Set up type definitions for sync functionality
deliverables:
- `src/types/podcast.ts` with core types
- `src/types/episode.ts` with episode types
- `src/types/source.ts` with source types
- `src/types/feed.ts` with feed types
steps:
- Create `src/types/podcast.ts`:
- `Podcast` interface (id, title, description, coverUrl, feedUrl, lastUpdated)
- `PodcastWithEpisodes` interface (podcast + episodes array)
- Create `src/types/episode.ts`:
- `Episode` interface (id, title, description, audioUrl, duration, pubDate, episodeNumber)
- `EpisodeStatus` enum (playing, paused, completed, not_started)
- `Progress` interface (episodeId, position, duration, timestamp)
- Create `src/types/source.ts`:
- `PodcastSource` interface (id, name, baseUrl, type, apiKey, enabled)
- `SourceType` enum (rss, api, custom)
- `SearchQuery` interface (query, sourceIds, filters)
- Create `src/types/feed.ts`:
- `Feed` interface (id, podcast, episodes[], isPublic, sourceId, lastUpdated)
- `FeedItem` interface (represents a single episode in a feed)
- `FeedFilter` interface (public, private, sourceId)
tests:
- Unit: Verify all interfaces compile correctly
- Unit: Test enum values are correct
- Integration: Test type definitions match expected data structures
acceptance_criteria:
- All TypeScript interfaces compile without errors
- Types are exported for use across the application
- Type definitions cover all podcast-related data
validation:
- Run `bun run build` to verify TypeScript compilation
- Check `src/types/` directory contains all required files
notes:
- Use strict TypeScript mode
- Include JSDoc comments for complex types
- Keep types simple and focused
- Ensure types are compatible with sync JSON/XML formats

View File

@@ -1,63 +0,0 @@
# 29. Build Feed List Component (Public/Private Feeds)
meta:
id: podcast-tui-app-29
feature: podcast-tui-app
priority: P0
depends_on: [28]
tags: [feed-list, components, solidjs, opentui]
objective:
- Create a scrollable feed list component
- Display public and private feeds
- Implement feed selection and display
- Add reverse chronological ordering
deliverables:
- `src/components/FeedList.tsx` with feed list component
- `src/components/FeedItem.tsx` with individual feed item
- `src/components/FeedFilter.tsx` with public/private toggle
steps:
- Create `src/components/FeedList.tsx`:
- Use `<scrollbox>` for scrollable list
- Accept feeds array as prop
- Implement feed rendering with `createSignal` for selection
- Add keyboard navigation (arrow keys, enter)
- Display feed title, description, episode count
- Create `src/components/FeedItem.tsx`:
- Display feed information
- Show public/private indicator
- Highlight selected feed
- Add hover effects
- Create `src/components/FeedFilter.tsx`:
- Toggle button for public/private feeds
- Filter logic implementation
- Update parent FeedList when filtered
- Add feed list to "My Feeds" navigation tab
tests:
- Unit: Test FeedList renders with feeds
- Unit: Test FeedItem displays correctly
- Integration: Test public/private filtering
- Integration: Test keyboard navigation in feed list
acceptance_criteria:
- Feed list displays all feeds correctly
- Public/private toggle filters feeds
- Feed selection is visually indicated
- Keyboard navigation works (arrow keys, enter)
- List scrolls properly when many feeds
validation:
- Run application and navigate to "My Feeds"
- Add some feeds and verify they appear
- Test public/private toggle
- Use arrow keys to navigate feeds
- Scroll list with many feeds
notes:
- Use SolidJS `createSignal` for selection state
- Follow OpenTUI component patterns from `components/REFERENCE.md`
- Feed list should be scrollable with many items
- Use Flexbox for layout

View File

@@ -1,67 +0,0 @@
# 30. Implement Feed Source Management (Add/Remove Sources)
meta:
id: podcast-tui-app-30
feature: podcast-tui-app
priority: P1
depends_on: [29]
tags: [source-management, feeds, solidjs, opentui]
objective:
- Create source management component
- Implement add new source functionality
- Implement remove source functionality
- Manage enabled/disabled sources
deliverables:
- `src/components/SourceManager.tsx` with source management UI
- `src/components/AddSourceForm.tsx` with add source form
- `src/components/SourceListItem.tsx` with individual source
steps:
- Create `src/components/SourceManager.tsx`:
- List of enabled sources
- Add source button
- Remove source button
- Enable/disable toggle
- Source count display
- Create `src/components/AddSourceForm.tsx`:
- Source name input
- Source URL input
- Source type selection
- API key input (if required)
- Submit button
- Validation
- Create `src/components/SourceListItem.tsx`:
- Display source info
- Enable/disable toggle
- Remove button
- Status indicator
- Add source manager to settings screen
tests:
- Unit: Test source list displays correctly
- Unit: Test add source form validation
- Unit: Test remove source functionality
- Integration: Test source management workflow
acceptance_criteria:
- Source list displays all sources
- Add source form validates input
- Remove source works correctly
- Enable/disable toggles work
validation:
- Run application and navigate to settings
- Test add source
- Test remove source
- Test enable/disable toggle
- Verify feeds from new sources appear
notes:
- Source types: RSS, API, Custom
- RSS sources: feed URLs
- API sources: require API key
- Custom sources: user-defined
- Add validation for source URLs
- Store sources in localStorage

View File

@@ -1,61 +0,0 @@
# 31. Add Reverse Chronological Ordering
meta:
id: podcast-tui-app-31
feature: podcast-tui-app
priority: P1
depends_on: [30]
tags: [ordering, feeds, episodes, solidjs, sorting]
objective:
- Implement reverse chronological ordering for feeds
- Order episodes by publication date (newest first)
- Order feeds by last updated (newest first)
- Provide sort option toggle
deliverables:
- `src/utils/ordering.ts` with sorting functions
- `src/components/FeedSortToggle.tsx` with sort option
- `src/components/EpisodeList.tsx` with ordered episode list
steps:
- Create `src/utils/ordering.ts`:
- `orderEpisodesByDate(episodes: Episode[]): Episode[]`
- Order by pubDate descending
- Handle missing dates
- `orderFeedsByDate(feeds: Feed[]): Feed[]`
- Order by lastUpdated descending
- Handle missing updates
- Create `src/components/FeedSortToggle.tsx`:
- Toggle button for date ordering
- Display current sort order
- Update parent component
- Create `src/components/EpisodeList.tsx`:
- Accept episodes array
- Apply ordering
- Display episodes in reverse chronological order
- Add keyboard navigation
tests:
- Unit: Test episode ordering
- Unit: Test feed ordering
- Unit: Test sort toggle
acceptance_criteria:
- Episodes ordered by date (newest first)
- Feeds ordered by last updated (newest first)
- Sort toggle works correctly
- Ordering persists across sessions
validation:
- Run application and check episode order
- Check feed order
- Toggle sort order
- Restart app and verify ordering persists
notes:
- Use JavaScript `sort()` with date comparison
- Handle timezone differences
- Add loading state during sort
- Cache ordered results
- Consider adding custom sort options

View File

@@ -1,66 +0,0 @@
# 32. Create Feed Detail View
meta:
id: podcast-tui-app-32
feature: podcast-tui-app
priority: P1
depends_on: [31]
tags: [feed-detail, episodes, solidjs, opentui]
objective:
- Create feed detail view component
- Display podcast information
- List episodes in reverse chronological order
- Provide episode playback controls
deliverables:
- `src/components/FeedDetail.tsx` with feed detail view
- `src/components/EpisodeList.tsx` with episode list
- `src/components/EpisodeItem.tsx` with individual episode
steps:
- Create `src/components/FeedDetail.tsx`:
- Podcast cover image
- Podcast title and description
- Episode count
- Subscribe/unsubscribe button
- Episode list container
- Create `src/components/EpisodeList.tsx`:
- Scrollable episode list
- Display episode title, date, duration
- Playback status indicator
- Add keyboard navigation
- Create `src/components/EpisodeItem.tsx`:
- Episode information
- Play button
- Mark as complete button
- Progress bar
- Hover effects
- Add feed detail to "My Feeds" navigation tab
tests:
- Unit: Test FeedDetail displays correctly
- Unit: Test EpisodeList rendering
- Unit: Test EpisodeItem interaction
- Integration: Test feed detail navigation
acceptance_criteria:
- Feed detail displays podcast info
- Episode list shows all episodes
- Episodes ordered reverse chronological
- Play buttons work
- Mark as complete works
validation:
- Run application and navigate to feed detail
- View podcast information
- Check episode order
- Test play button
- Test mark as complete
notes:
- Use SolidJS `createSignal` for episode selection
- Display episode status (playing, completed, not started)
- Show progress for completed episodes
- Add episode filtering (all, completed, not completed)
- Use Flexbox for layout

View File

@@ -1,68 +0,0 @@
# 33. Create Search Interface
meta:
id: podcast-tui-app-33
feature: podcast-tui-app
priority: P1
depends_on: [32]
tags: [search-interface, input, solidjs, opentui]
objective:
- Create search input component
- Implement search functionality
- Display search results
- Handle search state
deliverables:
- `src/components/SearchBar.tsx` with search input
- `src/components/SearchResults.tsx` with results display
- `src/components/SearchHistory.tsx` with history list
steps:
- Create `src/components/SearchBar.tsx`:
- Search input field using `<input>` component
- Search button
- Clear button
- Enter key handler
- Loading state
- Create `src/utils/search.ts`:
- `searchPodcasts(query: string, sourceIds: string[]): Promise<Podcast[]>`
- `searchEpisodes(query: string, feedId: string): Promise<Episode[]>`
- Handle multiple sources
- Cache search results
- Create `src/components/SearchResults.tsx`:
- Display search results with source indicators
- Show podcast/episode info
- Add to feed button
- Keyboard navigation through results
- Create `src/components/SearchHistory.tsx`:
- Display recent search queries
- Click to re-run search
- Delete individual history items
- Persist to localStorage
tests:
- Unit: Test search logic returns correct results
- Unit: Test search history persistence
- Integration: Test search bar accepts input
- Integration: Test results display correctly
acceptance_criteria:
- Search bar accepts and processes queries
- Search results display with source information
- Search history persists across sessions
- Keyboard navigation works in results
validation:
- Run application and navigate to "Search"
- Type a query and press Enter
- Verify results appear
- Click a result to add to feed
- Restart app and verify history persists
notes:
- Use localStorage for search history
- Implement basic caching to avoid repeated searches
- Handle empty results gracefully
- Add loading state during search
- Search both podcasts and episodes

View File

@@ -1,62 +0,0 @@
# 34. Implement Multi-Source Search
meta:
id: podcast-tui-app-34
feature: podcast-tui-app
priority: P1
depends_on: [33]
tags: [multi-source, search, solidjs, api]
objective:
- Implement search across multiple podcast sources
- Handle different source types (RSS, API, Custom)
- Display source information in results
- Cache search results
deliverables:
- `src/utils/search.ts` with multi-source search logic
- `src/utils/source-searcher.ts` with source-specific searchers
- `src/components/SourceBadge.tsx` with source indicator
steps:
- Create `src/utils/source-searcher.ts`:
- `searchRSSSource(query: string, source: PodcastSource): Promise<Podcast[]>`
- `searchAPISource(query: string, source: PodcastSource): Promise<Podcast[]>`
- `searchCustomSource(query: string, source: PodcastSource): Promise<Podcast[]>`
- Handle source-specific search logic
- Create `src/utils/search.ts`:
- `searchPodcasts(query: string, sourceIds: string[]): Promise<Podcast[]>`
- Aggregate results from multiple sources
- Deduplicate results
- Cache results by query
- Handle source errors gracefully
- Create `src/components/SourceBadge.tsx`:
- Display source type (RSS, API, Custom)
- Show source name
- Color-coded for different types
tests:
- Unit: Test RSS source search
- Unit: Test API source search
- Unit: Test custom source search
- Unit: Test result aggregation
acceptance_criteria:
- Search works across all enabled sources
- Source information displayed correctly
- Results aggregated from multiple sources
- Errors handled gracefully
validation:
- Run application and perform search
- Verify results from multiple sources
- Test with different source types
- Test error handling for failed sources
notes:
- RSS sources: Parse feed XML
- API sources: Call API endpoints
- Custom sources: User-defined search logic
- Handle rate limiting
- Cache results to avoid repeated searches
- Show loading state for each source

View File

@@ -1,66 +0,0 @@
# 35. Add Search Results Display
meta:
id: podcast-tui-app-35
feature: podcast-tui-app
priority: P1
depends_on: [34]
tags: [search-results, display, solidjs, opentui]
objective:
- Display search results with rich information
- Show podcast/episode details
- Add to feed functionality
- Keyboard navigation through results
deliverables:
- `src/components/SearchResults.tsx` with results display
- `src/components/ResultCard.tsx` with individual result
- `src/components/ResultDetail.tsx` with detailed view
steps:
- Create `src/components/ResultCard.tsx`:
- Display result title
- Display source information
- Display description/snippet
- Add to feed button
- Click to view details
- Create `src/components/ResultDetail.tsx`:
- Full result details
- Podcast/episode information
- Episode list (if podcast)
- Subscribe button
- Close button
- Create `src/components/SearchResults.tsx`:
- Scrollable results list
- Empty state display
- Loading state display
- Error state display
- Keyboard navigation
tests:
- Unit: Test ResultCard displays correctly
- Unit: Test ResultDetail displays correctly
- Unit: Test search results list
- Integration: Test add to feed
acceptance_criteria:
- Search results display with all information
- Result cards show source and details
- Add to feed button works
- Keyboard navigation works
validation:
- Run application and perform search
- Verify results display correctly
- Click result to view details
- Test add to feed
- Test keyboard navigation
notes:
- Use SolidJS `createSignal` for result selection
- Display result type (podcast/episode)
- Show source name and type
- Add loading state during search
- Handle empty results
- Add pagination for large result sets

View File

@@ -1,64 +0,0 @@
# 36. Build Search History with Persistent Storage
meta:
id: podcast-tui-app-36
feature: podcast-tui-app
priority: P1
depends_on: [35]
tags: [search-history, persistence, storage, solidjs]
objective:
- Implement search history functionality
- Store search queries in localStorage
- Display recent searches
- Add search to history
- Clear history
deliverables:
- `src/components/SearchHistory.tsx` with history list
- `src/utils/history.ts` with history management
- `src/hooks/useSearchHistory.ts` with history hook
steps:
- Create `src/utils/history.ts`:
- `addToHistory(query: string): void`
- `getHistory(): string[]`
- `removeFromHistory(query: string): void`
- `clearHistory(): void`
- `maxHistorySize = 50`
- Create `src/hooks/useSearchHistory.ts`:
- `createSignal` for history array
- Update history on search
- Persist to localStorage
- Methods to manage history
- Create `src/components/SearchHistory.tsx`:
- Display recent search queries
- Click to re-run search
- Delete individual history items
- Clear all history button
- Persistent across sessions
tests:
- Unit: Test history management functions
- Unit: Test history persistence
- Integration: Test history display
acceptance_criteria:
- Search queries added to history
- History persists across sessions
- History displays correctly
- Clear history works
validation:
- Run application and perform searches
- Check search history persists
- Test clearing history
- Restart app and verify
notes:
- Use localStorage for persistence
- Limit history to 50 items
- Remove duplicates
- Store timestamps (optional)
- Clear history button in search screen
- Add delete button on individual items

View File

@@ -1,56 +0,0 @@
# 37. Create Popular Shows Data Structure
meta:
id: podcast-tui-app-37
feature: podcast-tui-app
priority: P1
depends_on: [36]
tags: [popular-shows, data, discovery, solidjs]
objective:
- Create data structure for popular shows
- Define podcast metadata
- Categorize shows by topic
- Include feed URLs and descriptions
deliverables:
- `src/data/popular-shows.ts` with popular podcasts data
- `src/types/popular-shows.ts` with data types
- `src/constants/categories.ts` with category definitions
steps:
- Create `src/types/popular-shows.ts`:
- `PopularPodcast` interface (id, title, description, coverUrl, feedUrl, category, tags)
- `Category` enum (Technology, Business, Science, Entertainment, Health, Education)
- Create `src/constants/categories.ts`:
- List of all categories
- Category descriptions
- Sample podcasts per category
- Create `src/data/popular-shows.ts`:
- Array of popular podcasts
- Categorized by topic
- Reverse chronological ordering
- Include metadata for each show
tests:
- Unit: Test data structure compiles
- Unit: Test category definitions
- Integration: Test popular shows display
acceptance_criteria:
- Popular shows data structure defined
- Categories defined correctly
- Shows categorized properly
- Data ready for display
validation:
- Run `bun run build` to verify TypeScript
- Check data structure compiles
- Review data for completeness
notes:
- Popular shows data can be static or fetched
- If sources don't provide trending, use curated list
- Categories help users find shows by topic
- Include diverse range of shows
- Add RSS feed URLs for easy subscription

View File

@@ -1,64 +0,0 @@
# 38. Build Discover Page Component
meta:
id: podcast-tui-app-38
feature: podcast-tui-app
priority: P1
depends_on: [37]
tags: [discover-page, popular-shows, solidjs, opentui]
objective:
- Create discover page component
- Display popular shows
- Implement category filtering
- Add to feed functionality
deliverables:
- `src/components/DiscoverPage.tsx` with discover UI
- `src/components/PopularShows.tsx` with shows grid
- `src/components/CategoryFilter.tsx` with category buttons
steps:
- Create `src/components/DiscoverPage.tsx`:
- Page header with title
- Category filter buttons
- Popular shows grid
- Show details view
- Add to feed button
- Create `src/components/PopularShows.tsx`:
- Grid display of popular podcasts
- Show cover image
- Show title and description
- Add to feed button
- Click to view details
- Create `src/components/CategoryFilter.tsx`:
- Category button group
- Active category highlighting
- Filter logic implementation
- Update parent DiscoverPage
tests:
- Unit: Test PopularShows displays correctly
- Unit: Test CategoryFilter functionality
- Integration: Test discover page navigation
acceptance_criteria:
- Discover page displays popular shows
- Category filtering works correctly
- Shows are ordered reverse chronologically
- Clicking a show shows details
- Add to feed button works
validation:
- Run application and navigate to "Discover"
- Verify popular shows appear
- Click different categories
- Click a show and verify details
- Try add to feed
notes:
- Use Flexbox for category filter layout
- Use Grid for shows display
- Add loading state if fetching from API
- Handle empty categories
- Add hover effects for interactivity

View File

@@ -1,61 +0,0 @@
# 39. Add Trending Shows Display
meta:
id: podcast-tui-app-39
feature: podcast-tui-app
priority: P1
depends_on: [38]
tags: [trending-shows, display, solidjs]
objective:
- Display trending shows section
- Show top podcasts by popularity
- Implement trend indicators
- Display show rankings
deliverables:
- `src/components/TrendingShows.tsx` with trending section
- `src/components/ShowRanking.tsx` with ranking display
- `src/components/TrendIndicator.tsx` with trend icon
steps:
- Create `src/components/TrendingShows.tsx`:
- Trending section header
- Top shows list
- Show ranking (1, 2, 3...)
- Trending indicator
- Add to feed button
- Create `src/components/ShowRanking.tsx`:
- Display ranking number
- Show cover image
- Show title
- Trending score display
- Create `src/components/TrendIndicator.tsx`:
- Display trend icon (up arrow, down arrow, flat)
- Color-coded for trend direction
- Show trend percentage
- Add trending section to Discover page
tests:
- Unit: Test TrendingShows displays correctly
- Unit: Test ranking display
- Unit: Test trend indicator
acceptance_criteria:
- Trending shows section displays correctly
- Rankings shown for top shows
- Trend indicators display correctly
- Add to feed buttons work
validation:
- Run application and navigate to "Discover"
- View trending shows section
- Check rankings and indicators
- Test add to feed
notes:
- Trending shows: Top 10 podcasts
- Trending score: Based on downloads, listens, or engagement
- Trend indicators: Up/down/flat arrows
- Color-coded: Green for up, red for down, gray for flat
- Update trend scores periodically

View File

@@ -1,62 +0,0 @@
# 41. Create Player UI Layout
meta:
id: podcast-tui-app-41
feature: podcast-tui-app
priority: P0
depends_on: [40]
tags: [player, layout, solidjs, opentui]
objective:
- Create player UI layout component
- Display episode information
- Position player controls and waveform
- Handle player state
deliverables:
- `src/components/Player.tsx` with player layout
- `src/components/PlayerHeader.tsx` with episode info
- `src/components/PlayerControls.tsx` with controls area
steps:
- Create `src/components/Player.tsx`:
- Player container with borders
- Episode information header
- Waveform visualization area
- Playback controls area
- Progress bar area
- Create `src/components/PlayerHeader.tsx`:
- Episode title
- Podcast name
- Episode duration
- Close player button
- Create `src/components/PlayerControls.tsx`:
- Play/Pause button
- Previous/Next episode buttons
- Volume control
- Speed control
- Keyboard shortcuts display
tests:
- Unit: Test Player layout renders correctly
- Unit: Test PlayerHeader displays correctly
- Unit: Test PlayerControls layout
acceptance_criteria:
- Player UI displays episode information
- Controls positioned correctly
- Player fits within terminal bounds
- Layout is responsive
validation:
- Run application and navigate to "Player"
- Select an episode to play
- Verify player UI displays
- Check layout and positioning
notes:
- Use Flexbox for player layout
- Player should be at bottom or overlay
- Use `<scrollbox>` for waveform area
- Add loading state when no episode
- Use SolidJS signals for player state

View File

@@ -1,70 +0,0 @@
# 42. Implement Playback Controls
meta:
id: podcast-tui-app-42
feature: podcast-tui-app
priority: P0
depends_on: [41]
tags: [playback, controls, audio, solidjs]
objective:
- Implement playback controls (play/pause, skip, progress)
- Create control buttons
- Add keyboard shortcuts
- Handle playback state
deliverables:
- `src/components/PlaybackControls.tsx` with control buttons
- `src/utils/playback.ts` with playback logic
- `src/hooks/usePlayback.ts` with playback hook
steps:
- Create `src/utils/playback.ts`:
- `playAudio(audioUrl: string): void`
- `pauseAudio(): void`
- `skipForward(): void`
- `skipBackward(): void`
- `setPlaybackSpeed(speed: number): void`
- Handle audio player integration
- Create `src/hooks/usePlayback.ts`:
- `createSignal` for playback state
- Methods: play, pause, seek, setSpeed
- Handle audio events (timeupdate, ended)
- Create `src/components/PlaybackControls.tsx`:
- Play/Pause button
- Previous/Next episode buttons
- Volume control (slider)
- Speed control (1x, 1.25x, 1.5x, 2x)
- Keyboard shortcuts (space, arrows)
tests:
- Unit: Test playback logic
- Unit: Test playback hook
- Integration: Test playback controls
acceptance_criteria:
- Play/pause button works
- Skip forward/backward works
- Speed control works
- Volume control works
- Keyboard shortcuts work
validation:
- Run application and test playback
- Press play/pause
- Test skip buttons
- Change playback speed
- Test keyboard shortcuts
notes:
- Audio integration: Trigger system player or use Web Audio API
- Use Web Audio API for waveform
- Handle audio errors
- Add loading state during playback
- Store playback speed preference
- Support keyboard shortcuts:
- Space: Play/Pause
- Arrow Right: Skip forward
- Arrow Left: Skip backward
- Arrow Up: Volume up
- Arrow Down: Volume down

View File

@@ -1,61 +0,0 @@
# 43. Build ASCII Waveform Visualization
meta:
id: podcast-tui-app-43
feature: podcast-tui-app
priority: P0
depends_on: [42]
tags: [waveform, visualization, ascii, solidjs]
objective:
- Create ASCII waveform visualization
- Generate waveform from audio data
- Display waveform in player
- Handle click to seek
deliverables:
- `src/components/Waveform.tsx` with waveform display
- `src/utils/waveform.ts` with waveform generation
- `src/components/WaveformProgress.tsx` with progress overlay
steps:
- Create `src/utils/waveform.ts`:
- `generateWaveform(audioUrl: string): Promise<string>`
- Fetch audio data
- Analyze audio frequencies
- Convert to ASCII characters
- Return ASCII waveform string
- Create `src/components/Waveform.tsx`:
- Display ASCII waveform
- Color-coded for played/paused
- Click to seek to position
- Handle terminal width
- Create `src/components/WaveformProgress.tsx`:
- Progress overlay on waveform
- Show current position
- Highlight current segment
tests:
- Unit: Test waveform generation
- Unit: Test waveform display
- Integration: Test click to seek
acceptance_criteria:
- Waveform displays correctly
- Waveform generated from audio
- Color-coded for played/paused
- Clicking waveform seeks to position
validation:
- Run application and play an episode
- Verify waveform displays
- Check waveform colors
- Test seeking by clicking waveform
notes:
- ASCII waveform: Use `#` for peaks, `.` for valleys
- Use Web Audio API for waveform generation
- Cache waveform data for performance
- Handle large audio files
- Use frame buffer for drawing
- Color scheme: Green for played, Gray for paused

View File

@@ -1,63 +0,0 @@
# 44. Add Progress Tracking and Seek
meta:
id: podcast-tui-app-44
feature: podcast-tui-app
priority: P0
depends_on: [43]
tags: [progress, tracking, seek, solidjs]
objective:
- Track playback progress
- Save progress to storage
- Implement seek functionality
- Display progress bar
deliverables:
- `src/utils/progress.ts` with progress tracking
- `src/hooks/useProgress.ts` with progress hook
- `src/components/ProgressBar.tsx` with progress bar
steps:
- Create `src/utils/progress.ts`:
- `saveProgress(episodeId: string, position: number, duration: number): void`
- `loadProgress(episodeId: string): Progress | null`
- `updateProgress(episodeId: string, position: number): void`
- Handle progress persistence
- Create `src/hooks/useProgress.ts`:
- `createSignal` for progress
- Update progress on timeupdate
- Save progress periodically
- Load progress on episode change
- Create `src/components/ProgressBar.tsx`:
- Progress bar visualization
- Percentage display
- Time display (current/total)
- Click to seek
tests:
- Unit: Test progress tracking functions
- Unit: Test progress hook
- Integration: Test progress bar
acceptance_criteria:
- Progress saved correctly
- Progress loaded correctly
- Seek works via progress bar
- Progress persists across sessions
validation:
- Run application and play episode
- Seek to different positions
- Stop and restart
- Verify progress saved
- Restart app and verify progress loaded
notes:
- Save progress to localStorage
- Auto-save every 30 seconds
- Save on episode change
- Save on pause
- Don't save on completion
- Load progress when starting episode
- Display progress bar in player

View File

@@ -1,64 +0,0 @@
# 45. Implement Audio Integration (System/External Player)
meta:
id: podcast-tui-app-45
feature: podcast-tui-app
priority: P0
depends_on: [44]
tags: [audio, integration, player, solidjs]
objective:
- Implement audio playback integration
- Support system audio player
- Support external player
- Handle audio events
deliverables:
- `src/utils/audio-player.ts` with audio integration
- `src/components/AudioSettings.tsx` with audio player settings
- `src/hooks/useAudio.ts` with audio hook
steps:
- Create `src/utils/audio-player.ts`:
- `playWithSystemPlayer(audioUrl: string): void`
- `playWithExternalPlayer(audioUrl: string): void`
- `getSupportedPlayers(): string[]`
- Detect available players
- Create `src/components/AudioSettings.tsx`:
- Player selection dropdown
- System player options
- External player options
- Default player selection
- Create `src/hooks/useAudio.ts`:
- Manage audio state
- Handle player selection
- Handle audio events
- Update progress
tests:
- Unit: Test audio player detection
- Unit: Test player selection
- Integration: Test audio playback
acceptance_criteria:
- Audio plays correctly
- Multiple player options available
- Player selection persists
- Audio events handled
validation:
- Run application and test audio
- Select different player options
- Play episode and verify audio
- Test progress tracking with audio
notes:
- System player: macOS Terminal, iTerm2, etc.
- External player: VLC, QuickTime, etc.
- Use `open` command for macOS
- Use `xdg-open` for Linux
- Use `start` for Windows
- Handle audio errors gracefully
- Add player selection in settings
- Default to system player
- Store player preference

View File

@@ -1,66 +0,0 @@
# 46. Create Settings Screen
meta:
id: podcast-tui-app-46
feature: podcast-tui-app
priority: P1
depends_on: [45]
tags: [settings, screen, solidjs, opentui]
objective:
- Create settings screen component
- Implement settings navigation
- Display all settings sections
- Save/apply settings
deliverables:
- `src/components/SettingsScreen.tsx` with settings UI
- `src/components/SettingsNavigation.tsx` with settings menu
- `src/components/SettingsContent.tsx` with settings content
steps:
- Create `src/components/SettingsNavigation.tsx`:
- Settings menu with sections
- Navigation between sections
- Active section highlighting
- Keyboard navigation
- Create `src/components/SettingsContent.tsx`:
- Feed settings section
- Player settings section
- Sync settings section
- Appearance settings section
- Account settings section
- Create `src/components/SettingsScreen.tsx`:
- Combine navigation and content
- Save/Cancel buttons
- Reset to defaults button
tests:
- Unit: Test SettingsNavigation displays correctly
- Unit: Test SettingsContent sections
- Integration: Test settings navigation
acceptance_criteria:
- Settings screen displays all sections
- Navigation between sections works
- Settings save correctly
- Settings persist
validation:
- Run application and navigate to "Settings"
- Navigate between settings sections
- Change settings
- Verify settings persist
- Test reset to defaults
notes:
- Use SolidJS signals for settings state
- Settings sections:
- Feeds: Source management, auto-download
- Player: Playback speed, auto-play next
- Sync: Backup frequency, sync method
- Appearance: Theme, font size
- Account: Login, sync profile
- Add keyboard shortcuts for navigation
- Save settings on change
- Reset to defaults button

View File

@@ -1,69 +0,0 @@
# 47. Add Source Management UI
meta:
id: podcast-tui-app-47
feature: podcast-tui-app
priority: P1
depends_on: [46]
tags: [source-management, settings, ui, solidjs]
objective:
- Create source management UI in settings
- List all enabled sources
- Add new source functionality
- Remove source functionality
- Enable/disable sources
deliverables:
- `src/components/SourceManager.tsx` with source management
- `src/components/AddSourceForm.tsx` with add source form
- `src/components/SourceListItem.tsx` with individual source
steps:
- Create `src/components/SourceManager.tsx`:
- List of enabled sources
- Add source button
- Remove source button
- Enable/disable toggle
- Source count display
- Create `src/components/AddSourceForm.tsx`:
- Source name input
- Source URL input
- Source type selection (RSS, API, Custom)
- API key input (if required)
- Submit button
- Validation
- Create `src/components/SourceListItem.tsx`:
- Display source info
- Enable/disable toggle
- Remove button
- Status indicator (working, error)
tests:
- Unit: Test source list displays correctly
- Unit: Test add source form validation
- Unit: Test remove source functionality
- Integration: Test source management workflow
acceptance_criteria:
- Source list displays all sources
- Add source form validates input
- Remove source works correctly
- Enable/disable toggles work
validation:
- Run application and navigate to settings
- Test add source
- Test remove source
- Test enable/disable toggle
- Verify feeds from new sources appear
notes:
- Source types: RSS, API, Custom
- RSS sources: feed URLs
- API sources: require API key
- Custom sources: user-defined
- Add validation for source URLs
- Store sources in localStorage
- Show source status (working/error)
- Add error handling for failed sources

View File

@@ -1,69 +0,0 @@
# 48. Build User Preferences
meta:
id: podcast-tui-app-48
feature: podcast-tui-app
priority: P1
depends_on: [47]
tags: [preferences, settings, solidjs]
objective:
- Create user preferences panel
- Implement theme selection
- Implement font size control
- Implement playback speed control
- Implement auto-download settings
deliverables:
- `src/components/PreferencesPanel.tsx` with preferences
- `src/components/ThemeSelector.tsx` with theme options
- `src/components/FontSelector.tsx` with font size options
- `src/components/AutoDownload.tsx` with auto-download settings
steps:
- Create `src/components/PreferencesPanel.tsx`:
- Preferences sections
- Navigation between preferences
- Save/Cancel buttons
- Create `src/components/ThemeSelector.tsx`:
- Theme options (light, dark, terminal)
- Preview theme
- Select theme
- Create `src/components/FontSelector.tsx`:
- Font size options (small, medium, large)
- Preview font size
- Select font size
- Create `src/components/AutoDownload.tsx`:
- Auto-download episodes
- Download after playback
- Download schedule
- Storage limit warning
tests:
- Unit: Test theme selector
- Unit: Test font selector
- Unit: Test auto-download settings
- Integration: Test preferences save/load
acceptance_criteria:
- Preferences display correctly
- Theme selection works
- Font size selection works
- Auto-download settings work
- Preferences persist
validation:
- Run application and navigate to preferences
- Change theme and verify
- Change font size and verify
- Test auto-download settings
- Restart app and verify preferences
notes:
- Use localStorage for preferences
- Theme: Terminal colors (green/amber on black)
- Font size: Small (12px), Medium (14px), Large (16px)
- Auto-download: Download completed episodes
- Add preferences to settings screen
- Save preferences on change
- Reset to defaults button

View File

@@ -1,52 +0,0 @@
# 50. Create Global State Store (Signals)
meta:
id: podcast-tui-app-50
feature: podcast-tui-app
priority: P1
depends_on: [66]
tags: [state-management, signals, solidjs, global-store]
objective:
- Create a global state store using SolidJS Signals for reactive state management
- Implement a store that manages application-wide state (feeds, settings, user, etc.)
- Provide reactive subscriptions for state changes
- Ensure thread-safe state updates
deliverables:
- `/src/store/index.ts` - Global state store with Signals
- `/src/store/types.ts` - State type definitions
- `/src/store/hooks.ts` - Custom hooks for state access
- Updated `src/index.tsx` to initialize the store
steps:
- Define state interface with all application state properties (feeds, settings, user, etc.)
- Create Signal-based store using `createSignal` from SolidJS
- Implement computed signals for derived state (filtered feeds, search results, etc.)
- Create state update functions that trigger reactivity
- Add subscription mechanism for reactive UI updates
- Export store and hooks for use across components
tests:
- Unit: Test that signals update correctly when state changes
- Unit: Test computed signals produce correct derived values
- Integration: Verify store updates trigger UI re-renders
- Integration: Test multiple components can subscribe to same state
acceptance_criteria:
- Store can be initialized with default state
- State changes trigger reactive updates in components
- Computed signals work correctly for derived state
- Multiple components can access and subscribe to store
- State updates are thread-safe
validation:
- Run `bun run build` to verify TypeScript compilation
- Run application and verify state changes are reactive
- Check console for any errors during state updates
notes:
- Use `createSignal`, `createComputed`, `createEffect` from SolidJS
- Store should follow single source of truth pattern
- Consider using `batch` for multiple state updates
- State should be serializable for persistence

View File

@@ -1,62 +0,0 @@
# 50. Create Global State Store (Signals)
meta:
id: podcast-tui-app-50
feature: podcast-tui-app
priority: P0
depends_on: [49]
tags: [state-management, global-store, signals, solidjs]
objective:
- Create global state store using SolidJS Signals
- Manage application-wide state
- Provide state to all components
- Handle state updates and persistence
deliverables:
- `src/stores/appStore.ts` with global state store
- `src/stores/feedStore.ts` with feed state
- `src/stores/playerStore.ts` with player state
- `src/stores/searchStore.ts` with search state
steps:
- Create `src/stores/appStore.ts`:
- Use SolidJS signals for global state
- Store application state: currentTab, isAuthEnabled, settings
- Provide state to all child components
- Update state when needed
- Create `src/stores/feedStore.ts`:
- Signals for feeds array
- Signals for selectedFeed
- Methods: addFeed, removeFeed, updateFeed
- Create `src/stores/playerStore.ts`:
- Signals for currentEpisode
- Signals for playbackState
- Methods: play, pause, seek, setSpeed
- Create `src/stores/searchStore.ts`:
- Signals for searchResults
- Signals for searchHistory
- Methods: search, addToHistory, clearHistory
tests:
- Unit: Test store methods update signals correctly
- Unit: Test state persistence
- Integration: Test state updates across components
acceptance_criteria:
- Global state store manages all app state
- Store methods update signals correctly
- State persists across component re-renders
- State updates propagate to UI
validation:
- Run application and verify state is initialized
- Modify state and verify UI updates
- Restart app and verify state persistence
notes:
- Use SolidJS `createSignal` for reactivity
- Store should be singleton pattern
- Use `createStore` if complex state needed
- Keep store simple and focused
- Store state in localStorage

View File

@@ -1,63 +0,0 @@
# 51. Implement API Client for Podcast Sources
meta:
id: podcast-tui-app-51
feature: podcast-tui-app
priority: P1
depends_on: [50]
tags: [api-client, sources, solidjs]
objective:
- Create API client for podcast sources
- Handle RSS feed parsing
- Handle API source queries
- Handle custom sources
- Implement error handling
deliverables:
- `src/api/client.ts` with API client
- `src/api/rss-parser.ts` with RSS parsing
- `src/api/source-handler.ts` with source-specific handlers
steps:
- Create `src/api/client.ts`:
- `fetchFeeds(sourceIds: string[]): Promise<Feed[]>`
- `fetchEpisodes(feedUrl: string): Promise<Episode[]>`
- `searchPodcasts(query: string): Promise<Podcast[]>`
- Handle API calls and errors
- Create `src/api/rss-parser.ts`:
- `parseRSSFeed(xml: string): Podcast`
- Parse RSS XML
- Extract podcast metadata
- Extract episodes
- Create `src/api/source-handler.ts`:
- `handleRSSSource(source: PodcastSource): Promise<Feed[]>`
- `handleAPISource(source: PodcastSource, query: string): Promise<Podcast[]>`
- `handleCustomSource(source: PodcastSource, query: string): Promise<Podcast[]>`
- Source-specific logic
tests:
- Unit: Test RSS parsing
- Unit: Test API client
- Unit: Test source handlers
- Integration: Test API calls
acceptance_criteria:
- API client fetches data correctly
- RSS parsing works
- Source handlers work for all types
- Errors handled gracefully
validation:
- Run application and test API calls
- Test RSS feed parsing
- Test API source queries
- Test error handling
notes:
- Use `feed-parser` library for RSS parsing
- Use `axios` for API calls
- Handle rate limiting
- Cache API responses
- Add error handling for failed requests
- Store API keys securely

View File

@@ -1,66 +0,0 @@
# 52. Add Data Fetching and Caching
meta:
id: podcast-tui-app-52
feature: podcast-tui-app
priority: P1
depends_on: [51]
tags: [data-fetching, caching, performance, solidjs]
objective:
- Implement data fetching with caching
- Cache podcast feeds and episodes
- Cache search results
- Cache popular shows
- Handle cache invalidation
deliverables:
- `src/utils/cache.ts` with cache management
- `src/utils/data-fetcher.ts` with data fetching logic
- `src/hooks/useCachedData.ts` with cache hook
steps:
- Create `src/utils/cache.ts`:
- `cacheFeed(feedUrl: string, data: Feed): void`
- `getCachedFeed(feedUrl: string): Feed | null`
- `cacheSearch(query: string, results: Podcast[]): void`
- `getCachedSearch(query: string): Podcast[] | null`
- `invalidateCache(type: string): void`
- `cacheExpiration = 3600000` (1 hour)
- Create `src/utils/data-fetcher.ts`:
- `fetchFeedWithCache(feedUrl: string): Promise<Feed>`
- `fetchEpisodesWithCache(feedUrl: string): Promise<Episode[]>`
- `searchWithCache(query: string): Promise<Podcast[]>`
- Use cache when available
- Create `src/hooks/useCachedData.ts`:
- `createSignal` for cached data
- Fetch data with cache
- Update cache on fetch
- Handle cache expiration
tests:
- Unit: Test cache management
- Unit: Test data fetcher
- Unit: Test cache hook
acceptance_criteria:
- Data is cached correctly
- Cache is used on subsequent requests
- Cache invalidation works
- Cache expiration handled
validation:
- Run application and fetch data
- Verify data is cached
- Make same request and use cache
- Test cache invalidation
- Test cache expiration
notes:
- Use localStorage for cache
- Cache feeds, episodes, search results
- Cache popular shows
- Invalidate cache on feed update
- Set cache expiration time
- Add cache size limit
- Clear cache on settings change

View File

@@ -1,65 +0,0 @@
# 53. Build File-Based Storage for Sync
meta:
id: podcast-tui-app-53
feature: podcast-tui-app
priority: P1
depends_on: [52]
tags: [file-based-storage, sync, solidjs]
objective:
- Implement file-based storage for sync
- Store feeds and settings in files
- Implement backup functionality
- Implement restore functionality
- Handle file operations
deliverables:
- `src/utils/file-storage.ts` with file storage
- `src/utils/backup.ts` with backup functions
- `src/utils/restore.ts` with restore functions
steps:
- Create `src/utils/file-storage.ts`:
- `saveFeedsToFile(feeds: Feed[]): Promise<void>`
- `loadFeedsFromFile(): Promise<Feed[]>`
- `saveSettingsToFile(settings: Settings): Promise<void>`
- `loadSettingsFromFile(): Promise<Settings>`
- Handle file operations and errors
- Create `src/utils/backup.ts`:
- `createBackup(): Promise<string>`
- `backupFeeds(feeds: Feed[]): string`
- `backupSettings(settings: Settings): string`
- Include all user data
- Create backup directory
- Create `src/utils/restore.ts`:
- `restoreFromBackup(backupData: string): Promise<void>`
- `restoreFeeds(backupData: string): void`
- `restoreSettings(backupData: string): void`
- Validate backup data
tests:
- Unit: Test file storage functions
- Unit: Test backup functions
- Unit: Test restore functions
acceptance_criteria:
- File storage works correctly
- Backup creates valid files
- Restore loads data correctly
- File operations handle errors
validation:
- Run application and test file storage
- Create backup
- Restore from backup
- Test with different data sizes
- Test error cases
notes:
- Store files in `data/` directory
- Use JSON format for storage
- Include version info for compatibility
- Add file encryption option (optional)
- Test with large files
- Handle file permission errors

View File

@@ -1,67 +0,0 @@
# 54. Set Up Testing Framework (Snapshot Testing)
meta:
id: podcast-tui-app-54
feature: podcast-tui-app
priority: P1
depends_on: [53]
tags: [testing, framework, setup, opentui]
objective:
- Set up OpenTUI testing framework
- Configure test runner
- Set up snapshot testing
- Configure test environment
deliverables:
- `tests/` directory with test structure
- `tests/setup.ts` with test setup
- `tests/config.ts` with test configuration
- `package.json` with test scripts
steps:
- Install testing dependencies:
- `@opentui/testing`
- `@opentui/react` (for testing)
- `@opentui/solid` (for testing)
- `bun test` (built-in test runner)
- Configure test environment:
- Set up test renderer
- Configure test options
- Set up test fixtures
- Create test structure:
- `tests/components/` for component tests
- `tests/integration/` for integration tests
- `tests/utils/` for utility tests
- `tests/fixtures/` for test data
- Add test scripts to `package.json`:
- `test`: Run all tests
- `test:watch`: Run tests in watch mode
- `test:coverage`: Run tests with coverage
- `test:components`: Run component tests
- `test:integration`: Run integration tests
tests:
- Unit: Verify testing framework is set up
- Integration: Verify test scripts work
- Component: Verify snapshot testing works
acceptance_criteria:
- Testing framework installed and configured
- Test scripts work correctly
- Snapshot testing configured
- Test environment set up
validation:
- Run `bun test` to execute all tests
- Run `bun test:components` to run component tests
- Run `bun test:coverage` for coverage report
- Check all tests pass
notes:
- Use OpenTUI's testing framework for snapshot testing
- Test components in isolation
- Use test fixtures for common data
- Mock external dependencies
- Keep tests fast and focused
- Add CI/CD integration for automated testing

View File

@@ -1,68 +0,0 @@
# 55. Write Component Tests
meta:
id: podcast-tui-app-55
feature: podcast-tui-app
priority: P1
depends_on: [54]
tags: [testing, components, snapshot, solidjs]
objective:
- Write component tests for all major components
- Test component rendering
- Test component behavior
- Test component interactions
deliverables:
- `tests/components/Navigation.test.tsx`
- `tests/components/FeedList.test.tsx`
- `tests/components/SearchBar.test.tsx`
- `tests/components/Player.test.tsx`
- `tests/components/SettingsScreen.test.tsx`
- Test coverage for all components
steps:
- Write component tests:
- `tests/components/Navigation.test.tsx`:
- Test Navigation renders with correct tabs
- Test tab selection updates state
- Test keyboard navigation
- `tests/components/FeedList.test.tsx`:
- Test FeedList renders with feeds
- Test FeedItem displays correctly
- Test public/private filtering
- Test keyboard navigation
- `tests/components/SearchBar.test.tsx`:
- Test search bar accepts input
- Test search button triggers search
- Test clear button clears input
- `tests/components/Player.test.tsx`:
- Test Player UI displays correctly
- Test playback controls work
- Test waveform visualization
- `tests/components/SettingsScreen.test.tsx`:
- Test SettingsScreen renders correctly
- Test settings navigation
- Test settings save/load
tests:
- Unit: Run all component tests
- Coverage: Verify component coverage
acceptance_criteria:
- All component tests pass
- Test coverage > 80%
- Component behavior verified
validation:
- Run `bun test:components` to execute component tests
- Run `bun test --coverage` for coverage report
- Fix any failing tests
notes:
- Use OpenTUI testing framework
- Test components in isolation
- Mock external dependencies
- Use test fixtures for data
- Keep tests fast
- Test both happy path and error cases

View File

@@ -1,57 +0,0 @@
# 56. Add Keyboard Interaction Tests
meta:
id: podcast-tui-app-56
feature: podcast-tui-app
priority: P1
depends_on: [55]
tags: [testing, keyboard, interaction, solidjs]
objective:
- Write keyboard interaction tests
- Test keyboard shortcuts
- Test keyboard navigation
- Test input field keyboard handling
deliverables:
- `tests/integration/navigation.test.tsx`
- `tests/integration/keyboard.test.tsx`
- `tests/integration/inputs.test.tsx`
steps:
- Write keyboard tests:
- `tests/integration/navigation.test.tsx`:
- Test arrow keys navigate tabs
- Test enter selects tab
- Test escape closes modal
- `tests/integration/keyboard.test.tsx`:
- Test Ctrl+Q quits app
- Test Ctrl+S saves settings
- Test space plays/pauses
- Test arrow keys for playback
- `tests/integration/inputs.test.tsx`:
- Test input field accepts text
- Test keyboard input in search bar
- Test code input validation
tests:
- Unit: Run keyboard tests
- Integration: Test keyboard interactions
acceptance_criteria:
- All keyboard tests pass
- Keyboard shortcuts work correctly
- Input fields handle keyboard input
validation:
- Run `bun test` to execute all tests
- Manually test keyboard shortcuts
- Verify keyboard navigation works
notes:
- Use OpenTUI testing framework
- Test keyboard events
- Mock keyboard input
- Test on different terminals
- Document keyboard shortcuts
- Test modifier key combinations

View File

@@ -1,63 +0,0 @@
# 57. Implement Error Handling
meta:
id: podcast-tui-app-57
feature: podcast-tui-app
priority: P1
depends_on: [56]
tags: [error-handling, debugging, solidjs]
objective:
- Implement error handling throughout the app
- Handle API errors
- Handle file errors
- Handle user input errors
- Display error messages
deliverables:
- `src/utils/error-handler.ts` with error handling
- `src/components/ErrorDisplay.tsx` with error UI
- `src/components/LoadingSpinner.tsx` with loading UI
steps:
- Create `src/utils/error-handler.ts`:
- `handleError(error: Error, context: string): void`
- Log errors to console
- Display user-friendly error messages
- Handle different error types
- Create `src/components/ErrorDisplay.tsx`:
- Display error message
- Error type indicator
- Retry button
- Error details (expandable)
- Create `src/components/LoadingSpinner.tsx`:
- Loading indicator
- Loading message
- Different loading states
tests:
- Unit: Test error handler
- Unit: Test error display
- Integration: Test error handling flow
acceptance_criteria:
- Errors are caught and handled
- Error messages display correctly
- Retry functionality works
- User sees helpful error messages
validation:
- Run application and trigger errors
- Test API errors
- Test file errors
- Test input errors
- Verify error messages display
notes:
- Handle network errors (API calls)
- Handle file errors (import/export)
- Handle validation errors (inputs)
- Handle parsing errors (RSS feeds)
- Display errors in user-friendly way
- Add error logging for debugging
- Consider adding Sentry for production

View File

@@ -1,56 +0,0 @@
# 59. Create Theme System Architecture
meta:
id: podcast-tui-app-59
feature: podcast-tui-app
priority: P1
depends_on: [09]
tags: [theming, architecture, solidjs, design-system]
objective:
- Design and implement a flexible theme system architecture
- Support multiple color themes (Catppuccin, Gruvbox, Tokyo, Nord, custom)
- Provide theme switching capabilities
- Ensure theme persistence across sessions
- Define theme properties (colors, fonts, spacing, borders)
deliverables:
- `/src/themes/types.ts` - Theme type definitions
- `/src/themes/theme.ts` - Theme system core implementation
- `/src/themes/themes/` - Directory with theme files
- `/src/themes/default.ts` - Default system theme
- `/src/themes/hooks.ts` - Theme hooks (useTheme, useThemeColor)
steps:
- Define `Theme` interface with properties: name, colors, fonts, spacing, borders
- Create theme color palettes (background, foreground, primary, secondary, accent, etc.)
- Implement `ThemeManager` class to handle theme loading, switching, and persistence
- Create `useTheme` hook for React/Solid components to access current theme
- Implement `useThemeColor` hook for accessing specific theme colors
- Add theme persistence using localStorage or file-based storage
- Export theme utilities for programmatic theme access
tests:
- Unit: Test theme loading from JSON file
- Unit: Test theme switching updates current theme
- Unit: Test theme persistence saves/loads correctly
- Integration: Verify components update when theme changes
acceptance_criteria:
- Theme system can load multiple theme definitions
- Theme switching works instantly across all components
- Theme preferences persist across application restarts
- Theme colors are accessible via hooks
- Theme manager handles errors gracefully
validation:
- Run `bun run build` to verify TypeScript compilation
- Test theme switching manually in application
- Verify localStorage/file storage works
- Check all theme colors render correctly
notes:
- Theme files should be JSON or TypeScript modules
- Use CSS variables or terminal color codes
- Consider dark/light mode compatibility
- Themes should be easily extensible

View File

@@ -1,52 +0,0 @@
# 60. Implement Default Theme (System Terminal)
meta:
id: podcast-tui-app-60
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, default, solidjs, terminal]
objective:
- Implement the default system terminal theme
- Define colors matching common terminal environments
- Ensure good readability and contrast
- Support both light and dark terminal modes
deliverables:
- `/src/themes/themes/default.ts` - Default theme definition
- `/src/themes/themes/default-light.ts` - Light mode theme
- `/src/themes/themes/default-dark.ts` - Dark mode theme
- Updated `/src/themes/theme.ts` to load default theme
steps:
- Define default color palette for system terminals
- Create light mode theme with standard terminal colors
- Create dark mode theme with standard terminal colors
- Ensure proper contrast ratios for readability
- Test theme in both light and dark terminal environments
- Export default theme as fallback
tests:
- Unit: Verify default theme colors are defined
- Unit: Test theme renders correctly in light mode
- Unit: Test theme renders correctly in dark mode
- Visual: Verify text contrast meets accessibility standards
acceptance_criteria:
- Default theme works in light terminal mode
- Default theme works in dark terminal mode
- Colors have good readability and contrast
- Theme is used as fallback when no theme selected
validation:
- Run `bun run build` to verify TypeScript compilation
- Test in light terminal (e.g., iTerm2, Terminal.app)
- Test in dark terminal (e.g., Kitty, Alacritty)
- Check color contrast visually
notes:
- Use standard terminal color codes (ANSI escape codes)
- Consider common terminal themes (Solarized, Dracula, etc.)
- Test on multiple terminal emulators
- Document terminal requirements

View File

@@ -1,52 +0,0 @@
# 61. Add Catppuccin Theme
meta:
id: podcast-tui-app-61
feature: podcast-tui-app
priority: P1
depends_on: [59]
tags: [theming, catppuccin, solidjs, popular]
objective:
- Implement Catppuccin Mocha theme for the podcast TUI
- Provide beautiful, modern color scheme
- Ensure high contrast and readability
- Support both dark and light variants
deliverables:
- `/src/themes/themes/catppuccin.ts` - Catppuccin theme definition
- `/src/themes/themes/catppuccin-mocha.ts` - Dark mode Catppuccin
- `/src/themes/themes/catppuccin-latte.ts` - Light mode Catppuccin
- Updated `/src/themes/themes/index.ts` to export Catppuccin themes
steps:
- Research and implement Catppuccin Mocha color palette
- Define all color tokens (background, foreground, surface, primary, secondary, etc.)
- Create Catppuccin Mocha (dark) theme
- Create Catppuccin Latte (light) theme
- Ensure proper color contrast for accessibility
- Add Catppuccin to theme registry
tests:
- Unit: Verify Catppuccin theme colors are defined
- Unit: Test Catppuccin Mocha renders correctly
- Unit: Test Catppuccin Latte renders correctly
- Visual: Verify Catppuccin colors are visually appealing
acceptance_criteria:
- Catppuccin Mocha theme works in dark terminals
- Catppuccin Latte theme works in light terminals
- Colors match official Catppuccin design
- Theme is selectable from theme list
validation:
- Run `bun run build` to verify TypeScript compilation
- Test Catppuccin theme manually in application
- Compare with official Catppuccin color palette
- Check color harmony and contrast
notes:
- Catppuccin Mocha is the recommended dark theme
- Include all standard Catppuccin colors (latte, frappe, macchiato, mocha)
- Use official color values from Catppuccin repository
- Consider adding custom color variations

Some files were not shown because too many files have changed in this diff Show More