diff --git a/src/ui/PodcastView/EpisodeList.svelte b/src/ui/PodcastView/EpisodeList.svelte index ffaa9ea..16a418c 100644 --- a/src/ui/PodcastView/EpisodeList.svelte +++ b/src/ui/PodcastView/EpisodeList.svelte @@ -5,10 +5,12 @@ import { hidePlayedEpisodes, playedEpisodes } from "src/store"; import Icon from "../obsidian/Icon.svelte"; import Text from "../obsidian/Text.svelte"; + import Loading from "./Loading.svelte"; export let episodes: Episode[] = []; export let showThumbnails: boolean = false; export let showListMenu: boolean = true; + export let isLoading: boolean = false; let searchInputQuery: string = ""; const dispatch = createEventDispatcher(); @@ -63,7 +65,13 @@ {/if}
- {#if episodes.length === 0} + {#if isLoading} +
+ + Fetching episodes... +
+ {/if} + {#if episodes.length === 0 && !isLoading}

No episodes found.

{/if} {#each episodes as episode (episode.url || episode.streamUrl || `${episode.title}-${episode.episodeDate ?? ""}`)} @@ -115,4 +123,13 @@ width: 100%; margin-bottom: 0.5rem; } + + .episode-list-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 1rem 0; + color: var(--text-muted); + } diff --git a/src/ui/PodcastView/PodcastView.integration.test.ts b/src/ui/PodcastView/PodcastView.integration.test.ts index 5d7895a..0341fc0 100644 --- a/src/ui/PodcastView/PodcastView.integration.test.ts +++ b/src/ui/PodcastView/PodcastView.integration.test.ts @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/svelte"; +import { fireEvent, render, screen, waitFor } from "@testing-library/svelte"; import { get } from "svelte/store"; import { afterEach, @@ -144,4 +144,89 @@ describe("PodcastView integration flow", () => { expect.objectContaining({ path: expectedPath }), ); }); + + test("shows loading state while fetching and streams episodes per feed", async () => { + const secondFeed: PodcastFeed = { + title: "Second Podcast", + url: "https://pod.example.com/feed-two.xml", + artworkUrl: "https://pod.example.com/art-two.jpg", + }; + + const firstEpisode: Episode = { + title: "Episode A", + streamUrl: "https://pod.example.com/a.mp3", + url: "https://pod.example.com/a", + description: "Episode A description", + content: "

Episode A content

", + podcastName: testFeed.title, + artworkUrl: testFeed.artworkUrl, + episodeDate: new Date("2024-02-01T00:00:00.000Z"), + }; + + const secondEpisode: Episode = { + title: "Episode B", + streamUrl: "https://pod.example.com/b.mp3", + url: "https://pod.example.com/b", + description: "Episode B description", + content: "

Episode B content

", + podcastName: secondFeed.title, + artworkUrl: secondFeed.artworkUrl, + episodeDate: new Date("2024-01-15T00:00:00.000Z"), + }; + + let resolveFirstFeed!: (value: Episode[]) => void; + let resolveSecondFeed!: (value: Episode[]) => void; + + mockGetEpisodes + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveFirstFeed = resolve; + }), + ) + .mockImplementationOnce( + () => + new Promise((resolve) => { + resolveSecondFeed = resolve; + }), + ); + + plugin.set({ + settings: { + feedCache: { + enabled: false, + ttlHours: 6, + }, + }, + } as never); + + savedFeeds.set({ + [testFeed.title]: testFeed, + [secondFeed.title]: secondFeed, + }); + viewState.set(ViewState.EpisodeList); + + render(PodcastView); + + await screen.findByText("Fetching episodes..."); + + resolveFirstFeed([firstEpisode]); + + expect( + await screen.findByText(firstEpisode.title), + ).toBeInTheDocument(); + expect(screen.getByText("Fetching episodes...")).toBeInTheDocument(); + expect(screen.queryByText(secondEpisode.title)).toBeNull(); + + resolveSecondFeed([secondEpisode]); + + expect( + await screen.findByText(secondEpisode.title), + ).toBeInTheDocument(); + await waitFor(() => + expect( + screen.queryByText("Fetching episodes..."), + ).not.toBeInTheDocument(), + ); + }); }); diff --git a/src/ui/PodcastView/PodcastView.svelte b/src/ui/PodcastView/PodcastView.svelte index 411750a..b243ff7 100644 --- a/src/ui/PodcastView/PodcastView.svelte +++ b/src/ui/PodcastView/PodcastView.svelte @@ -40,6 +40,7 @@ let displayedEpisodes: Episode[] = []; let displayedPlaylists: Playlist[] = []; let latestEpisodes: Episode[] = []; + let isFetchingEpisodes: boolean = false; let loadingFeeds: Set = new Set(); let currentSearchQuery: string = ""; let loadingFeedNames: string[] = []; @@ -50,6 +51,7 @@ loadingFeedNames.length > 3 ? `${loadingFeedNames.slice(0, 3).join(", ")} +${loadingFeedNames.length - 3} more` : loadingFeedNames.join(", "); + $: isFetchingEpisodes = loadingFeedNames.length > 0; onMount(() => { const unsubscribePlaylists = playlists.subscribe((pl) => { @@ -177,9 +179,9 @@ event: CustomEvent<{ feed: PodcastFeed }> ) { const { feed } = event.detail; - displayedEpisodes = []; selectedFeed = feed; + displayedEpisodes = []; viewState.set(ViewState.EpisodeList); setFeedLoading(feed.title, true); @@ -284,6 +286,7 @@