From 8004ae074637dc428dc45a4b20e6faaad02dbe81 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 24 Nov 2025 07:12:52 +0100 Subject: [PATCH 1/2] Improve podcast feed initial loading --- src/ui/PodcastView/PodcastView.svelte | 147 +++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 17 deletions(-) diff --git a/src/ui/PodcastView/PodcastView.svelte b/src/ui/PodcastView/PodcastView.svelte index 64c1c22..4295c94 100644 --- a/src/ui/PodcastView/PodcastView.svelte +++ b/src/ui/PodcastView/PodcastView.svelte @@ -39,6 +39,16 @@ import { get } from "svelte/store"; let displayedEpisodes: Episode[] = []; let displayedPlaylists: Playlist[] = []; let latestEpisodes: Episode[] = []; + let loadingFeeds: Set = new Set(); + let currentSearchQuery: string = ""; + let loadingFeedNames: string[] = []; + let loadingFeedSummary: string = ""; + + $: loadingFeedNames = Array.from(loadingFeeds); + $: loadingFeedSummary = + loadingFeedNames.length > 3 + ? `${loadingFeedNames.slice(0, 3).join(", ")} +${loadingFeedNames.length - 3} more` + : loadingFeedNames.join(", "); onMount(() => { const unsubscribePlaylists = playlists.subscribe((pl) => { @@ -46,7 +56,18 @@ onMount(() => { }); const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => { - feeds = Object.values(storeValue); + const updatedFeeds = Object.values(storeValue); + const previousFeedTitles = new Set(feeds.map((feed) => feed.title)); + + feeds = updatedFeeds; + + const newFeeds = updatedFeeds.filter( + (feed) => !previousFeedTitles.has(feed.title), + ); + + if (newFeeds.length > 0) { + void fetchEpisodesInAllFeeds(newFeeds); + } }); const unsubscribeEpisodeCache = episodeCache.subscribe((cache) => { @@ -59,15 +80,13 @@ onMount(() => { return 0; }); - }); - - (async () => { - await fetchEpisodesInAllFeeds(feeds); - if (!selectedFeed) { - displayedEpisodes = latestEpisodes; + if (!selectedFeed && !selectedPlaylist) { + displayedEpisodes = currentSearchQuery + ? searchEpisodes(currentSearchQuery, latestEpisodes) + : latestEpisodes; } - })(); + }); return () => { unsubscribeEpisodeCache(); @@ -129,14 +148,34 @@ onMount(() => { } } + function setFeedLoading(feedTitle: string, isLoading: boolean) { + const updatedLoadingFeeds = new Set(loadingFeeds); + + if (isLoading) { + updatedLoadingFeeds.add(feedTitle); + } else { + updatedLoadingFeeds.delete(feedTitle); + } + + loadingFeeds = updatedLoadingFeeds; + } + function fetchEpisodesInAllFeeds( feedsToSearch: PodcastFeed[] - ): Promise { + ): Promise { + if (!feedsToSearch.length) return Promise.resolve(); + return Promise.all( - feedsToSearch.map((feed) => fetchEpisodes(feed)) - ).then((episodes) => { - return episodes.flat(); - }); + feedsToSearch.map(async (feed) => { + setFeedLoading(feed.title, true); + + try { + await fetchEpisodes(feed); + } finally { + setFeedLoading(feed.title, false); + } + }) + ).then(() => undefined); } async function handleClickPodcast( @@ -146,8 +185,17 @@ onMount(() => { displayedEpisodes = []; selectedFeed = feed; - displayedEpisodes = await fetchEpisodes(feed); viewState.set(ViewState.EpisodeList); + setFeedLoading(feed.title, true); + + try { + const episodes = await fetchEpisodes(feed); + displayedEpisodes = currentSearchQuery + ? searchEpisodes(currentSearchQuery, episodes) + : episodes; + } finally { + setFeedLoading(feed.title, false); + } } function handleClickEpisode(event: CustomEvent<{ episode: Episode }>) { @@ -166,11 +214,21 @@ onMount(() => { async function handleClickRefresh() { if (!selectedFeed) return; - displayedEpisodes = await fetchEpisodes(selectedFeed, false); + setFeedLoading(selectedFeed.title, true); + + try { + const episodes = await fetchEpisodes(selectedFeed, false); + displayedEpisodes = currentSearchQuery + ? searchEpisodes(currentSearchQuery, episodes) + : episodes; + } finally { + setFeedLoading(selectedFeed.title, false); + } } const handleSearch = debounce((event: CustomEvent<{ query: string }>) => { const { query } = event.detail; + currentSearchQuery = query; if (selectedFeed) { const episodesInFeed = $episodeCache[selectedFeed.title]; @@ -213,6 +271,21 @@ onMount(() => { {#if $viewState === ViewState.Player} {:else if $viewState === ViewState.EpisodeList} + {#if loadingFeedNames.length > 0} +
+
+ +
+
+ + Updating {loadingFeedNames.length} feed{loadingFeedNames.length === 1 ? "" : "s"} + + {#if loadingFeedSummary} + {loadingFeedSummary} + {/if} +
+
+ {/if} { class="go-back" on:click={() => { selectedFeed = null; - displayedEpisodes = latestEpisodes; + displayedEpisodes = currentSearchQuery + ? searchEpisodes(currentSearchQuery, latestEpisodes) + : latestEpisodes; viewState.set(ViewState.EpisodeList); }} > @@ -252,7 +327,9 @@ onMount(() => { class="go-back" on:click={() => { selectedPlaylist = null; - displayedEpisodes = latestEpisodes; + displayedEpisodes = currentSearchQuery + ? searchEpisodes(currentSearchQuery, latestEpisodes) + : latestEpisodes; viewState.set(ViewState.EpisodeList); }} > @@ -298,6 +375,42 @@ onMount(() => { height: 100%; } + .feed-loading-banner { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + } + + .feed-loading-spinner { + display: inline-flex; + animation: spin 1s linear infinite; + } + + .feed-loading-text { + display: flex; + flex-direction: column; + gap: 0.1rem; + font-size: 0.9rem; + } + + .feed-loading-names { + opacity: 0.7; + font-size: 0.85rem; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } + } + .go-back { display: flex; align-items: center; From fc0b89d845e1a998f509216e02bd0615af96cdf4 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Mon, 24 Nov 2025 07:22:56 +0100 Subject: [PATCH 2/2] Guard feed search before cache ready --- src/ui/PodcastView/PodcastView.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/PodcastView/PodcastView.svelte b/src/ui/PodcastView/PodcastView.svelte index 4295c94..b0caea4 100644 --- a/src/ui/PodcastView/PodcastView.svelte +++ b/src/ui/PodcastView/PodcastView.svelte @@ -231,7 +231,7 @@ onMount(() => { currentSearchQuery = query; if (selectedFeed) { - const episodesInFeed = $episodeCache[selectedFeed.title]; + const episodesInFeed = $episodeCache[selectedFeed.title] ?? []; displayedEpisodes = searchEpisodes(query, episodesInFeed); return; }