diff --git a/src/components/podcast/EpisodeList.tsx b/src/components/podcast/EpisodeList.tsx index 483a559..e2d51be 100644 --- a/src/components/podcast/EpisodeList.tsx +++ b/src/components/podcast/EpisodeList.tsx @@ -3,6 +3,23 @@ import type { PodcastShow, PodcastEpisode } from "../../types/podcast"; import { getEpisodes } from "../../lib/podcast"; import { usePodcastStore } from "../../stores/podcast"; +function SubscribeButton({ show }: { show: PodcastShow }) { + const subscribed = usePodcastStore((s) => s.isSubscribed(show.feedUrl)); + const { subscribe, unsubscribe } = usePodcastStore.getState(); + return ( + + ); +} + function formatDuration(seconds: number): string { if (!seconds) return ""; const h = Math.floor(seconds / 3600); @@ -60,9 +77,14 @@ export function EpisodeList({ show, onBack }: EpisodeListProps) { onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} /> )} -
-

{show.title}

-
{show.author}
+
+
+
+

{show.title}

+
{show.author}
+
+ +
{show.description && (
{show.description.replace(/<[^>]+>/g, "").slice(0, 200)} diff --git a/src/components/podcast/PodcastCard.tsx b/src/components/podcast/PodcastCard.tsx index f8a7e8d..4e0ebe4 100644 --- a/src/components/podcast/PodcastCard.tsx +++ b/src/components/podcast/PodcastCard.tsx @@ -1,4 +1,5 @@ import type { PodcastShow } from "../../types/podcast"; +import { usePodcastStore } from "../../stores/podcast"; interface PodcastCardProps { show: PodcastShow; @@ -6,25 +7,27 @@ interface PodcastCardProps { } export function PodcastCard({ show, onClick }: PodcastCardProps) { + const subscribed = usePodcastStore((s) => s.isSubscribed(show.feedUrl)); + const { subscribe, unsubscribe } = usePodcastStore.getState(); + return ( - +
)} -
- + + +
); } diff --git a/src/components/podcast/PodcastsView.tsx b/src/components/podcast/PodcastsView.tsx index 628e32d..b02a4bf 100644 --- a/src/components/podcast/PodcastsView.tsx +++ b/src/components/podcast/PodcastsView.tsx @@ -1,20 +1,24 @@ import { useState, useEffect, useCallback } from "react"; import type { PodcastShow } from "../../types/podcast"; import { searchPodcasts, getTrending } from "../../lib/podcast"; +import { usePodcastStore } from "../../stores/podcast"; import { PodcastCard } from "./PodcastCard"; import { EpisodeList } from "./EpisodeList"; -type Tab = "trending" | "search"; +type Tab = "subscriptions" | "trending" | "search"; export function PodcastsView() { - const [tab, setTab] = useState("trending"); + const subscriptions = usePodcastStore((s) => s.subscriptions); + const hasSubscriptions = subscriptions.length > 0; + const [tab, setTab] = useState(hasSubscriptions ? "subscriptions" : "trending"); const [query, setQuery] = useState(""); const [shows, setShows] = useState([]); const [loading, setLoading] = useState(false); const [selectedShow, setSelectedShow] = useState(null); - // Load trending on mount + // Load trending on mount if no subscriptions useEffect(() => { + if (hasSubscriptions) return; setLoading(true); getTrending().then((results) => { setShows(results); @@ -47,6 +51,8 @@ export function PodcastsView() { return setSelectedShow(null)} />; } + const displayShows = tab === "subscriptions" ? subscriptions : shows; + return (
{/* Header */} @@ -74,7 +80,7 @@ export function PodcastsView() { {/* Tabs */}
- {(["trending", "search"] as Tab[]).map((t) => ( + {(["subscriptions", "trending", "search"] as Tab[]).map((t) => ( ))}
@@ -92,15 +98,32 @@ export function PodcastsView() { {/* Results */}
- {loading ? ( + {tab === "subscriptions" ? ( + subscriptions.length === 0 ? ( +
+

No subscriptions yet.

+

Search for a podcast or browse trending to subscribe.

+
+ ) : ( +
+ {subscriptions.map((show, i) => ( + setSelectedShow(show)} + /> + ))} +
+ ) + ) : loading ? (
Loading...
- ) : shows.length === 0 ? ( + ) : displayShows.length === 0 ? (
{tab === "search" ? "No results. Try a different search." : "No trending podcasts found."}
) : (
- {shows.map((show, i) => ( + {displayShows.map((show, i) => ( ; playCounter: number; + subscriptions: PodcastShow[]; play: (episode: PodcastEpisode) => void; pause: () => void; @@ -69,6 +71,21 @@ interface PodcastState { setV4VSatsPerMinute: (sats: number) => void; setV4VStreaming: (streaming: boolean, intervalId?: number | null) => void; stop: () => void; + subscribe: (show: PodcastShow) => void; + unsubscribe: (feedUrl: string) => void; + isSubscribed: (feedUrl: string) => boolean; +} + +function loadSubscriptions(): PodcastShow[] { + try { + return JSON.parse(localStorage.getItem(SUBS_KEY) ?? "[]"); + } catch { + return []; + } +} + +function saveSubscriptions(subs: PodcastShow[]) { + localStorage.setItem(SUBS_KEY, JSON.stringify(subs)); } const persisted = loadPersistedState(); @@ -87,6 +104,7 @@ export const usePodcastStore = create((set, get) => ({ v4vIntervalId: null, progressMap: persisted.progressMap, playCounter: 0, + subscriptions: loadSubscriptions(), play: (episode) => { const position = get().loadProgress(episode.guid); @@ -167,4 +185,22 @@ export const usePodcastStore = create((set, get) => ({ v4vIntervalId: null, }); }, + + subscribe: (show) => { + const { subscriptions } = get(); + if (subscriptions.some((s) => s.feedUrl === show.feedUrl)) return; + const updated = [...subscriptions, show]; + set({ subscriptions: updated }); + saveSubscriptions(updated); + }, + + unsubscribe: (feedUrl) => { + const updated = get().subscriptions.filter((s) => s.feedUrl !== feedUrl); + set({ subscriptions: updated }); + saveSubscriptions(updated); + }, + + isSubscribed: (feedUrl) => { + return get().subscriptions.some((s) => s.feedUrl === feedUrl); + }, }));