|
63 | 63 | }); |
64 | 64 |
|
65 | 65 | $: loadingFeedNames = Array.from(loadingFeeds); |
| 66 | + // Exclude the selected feed from the banner: its loading state is already |
| 67 | + // shown by the inline EpisodeList spinner, so the banner would duplicate it. |
| 68 | + $: bannerFeedNames = loadingFeedNames.filter( |
| 69 | + (name) => name !== selectedFeed?.title, |
| 70 | + ); |
66 | 71 | $: loadingFeedSummary = |
67 | | - loadingFeedNames.length > 3 |
68 | | - ? `${loadingFeedNames.slice(0, 3).join(", ")} +${loadingFeedNames.length - 3} more` |
69 | | - : loadingFeedNames.join(", "); |
| 72 | + bannerFeedNames.length > 3 |
| 73 | + ? `${bannerFeedNames.slice(0, 3).join(", ")} +${bannerFeedNames.length - 3} more` |
| 74 | + : bannerFeedNames.join(", "); |
70 | 75 | $: isFetchingEpisodes = loadingFeedNames.length > 0; |
71 | 76 |
|
72 | 77 | onMount(() => { |
|
89 | 94 | ]; |
90 | 95 | }; |
91 | 96 |
|
| 97 | + // Refresh both the grid tiles AND an open playlist's episode list so a |
| 98 | + // context-menu add/remove updates the currently-viewed list immediately |
| 99 | + // (PL-04). |
| 100 | + const refreshPlaylists = () => { |
| 101 | + updateDisplayedPlaylists(); |
| 102 | + updateDisplayedPlaylistEpisodesIfSelected(); |
| 103 | + }; |
| 104 | +
|
92 | 105 | const playlistUnsubscribers = [ |
93 | | - playlists.subscribe(updateDisplayedPlaylists), |
| 106 | + playlists.subscribe(refreshPlaylists), |
94 | 107 | queue.subscribe(updateDisplayedPlaylists), |
95 | | - favorites.subscribe(updateDisplayedPlaylists), |
96 | | - localFiles.subscribe(updateDisplayedPlaylists), |
| 108 | + favorites.subscribe(refreshPlaylists), |
| 109 | + localFiles.subscribe(refreshPlaylists), |
97 | 110 | // Recompute when the plugin store re-emits so toggling the autoQueue |
98 | 111 | // setting hides/shows the empty Queue tile immediately (issue #108). |
99 | 112 | plugin.subscribe(updateDisplayedPlaylists), |
|
179 | 192 | async function fetchEpisodes( |
180 | 193 | feed: PodcastFeed, |
181 | 194 | useCache: boolean = true, |
| 195 | + notifyOnError: boolean = false, |
182 | 196 | ): Promise<Episode[]> { |
183 | 197 | const cacheEnabled = isFeedCacheEnabled(); |
184 | 198 | const cacheTtlMs = getFeedCacheTtlMs(); |
|
247 | 261 | } |
248 | 262 | } |
249 | 263 |
|
| 264 | + // No cache/download fallback recovered episodes. Surface a Notice |
| 265 | + // only for the interactive single-feed path; the bulk/background |
| 266 | + // path stays quiet to avoid spamming on refresh or initial load. |
| 267 | + if (notifyOnError) { |
| 268 | + new Notice( |
| 269 | + `Could not load episodes for ${feed.title}. Check your connection and try again.`, |
| 270 | + ); |
| 271 | + } |
| 272 | +
|
250 | 273 | return []; |
251 | 274 | } |
252 | 275 | } |
|
261 | 284 | return feeds.filter((feed) => playedPodcastNames.has(feed.title)); |
262 | 285 | } |
263 | 286 |
|
264 | | - async function fetchFullEpisodes(feed: PodcastFeed): Promise<Episode[]> { |
| 287 | + async function fetchFullEpisodes( |
| 288 | + feed: PodcastFeed, |
| 289 | + notifyOnError: boolean = false, |
| 290 | + ): Promise<Episode[]> { |
265 | 291 | const cacheEnabled = isFeedCacheEnabled(); |
266 | 292 | const persistedEpisodes = cacheEnabled |
267 | 293 | ? getCachedEpisodes(feed, getFeedCacheTtlMs()) |
|
272 | 298 | return inMemoryEpisodes; |
273 | 299 | } |
274 | 300 |
|
275 | | - return fetchEpisodes(feed, false); |
| 301 | + return fetchEpisodes(feed, false, notifyOnError); |
276 | 302 | } |
277 | 303 |
|
278 | 304 | async function fetchEpisodesByStrategy( |
279 | 305 | feed: PodcastFeed, |
280 | 306 | strategy: EpisodeFetchStrategy = "cached", |
| 307 | + notifyOnError: boolean = false, |
281 | 308 | ): Promise<Episode[]> { |
282 | 309 | if (strategy === "network") { |
283 | | - return fetchEpisodes(feed, false); |
| 310 | + return fetchEpisodes(feed, false, notifyOnError); |
284 | 311 | } |
285 | 312 |
|
286 | 313 | if (strategy === "full") { |
287 | | - return fetchFullEpisodes(feed); |
| 314 | + return fetchFullEpisodes(feed, notifyOnError); |
288 | 315 | } |
289 | 316 |
|
290 | | - return fetchEpisodes(feed, true); |
| 317 | + return fetchEpisodes(feed, true, notifyOnError); |
291 | 318 | } |
292 | 319 |
|
293 | 320 | function getPlayedPlaylist(): Playlist { |
|
361 | 388 | updateDisplayedPlayedEpisodes(); |
362 | 389 | } |
363 | 390 |
|
| 391 | + // Keep an OPEN playlist's episode list in sync when its backing store changes |
| 392 | + // (e.g. a context-menu add/remove). Without this the list view only reflected |
| 393 | + // the snapshot taken at click time and went stale until the user navigated |
| 394 | + // away and back (PL-04). The virtual Played list and the Queue (which routes |
| 395 | + // to the player) are handled elsewhere, so they are skipped here. |
| 396 | + function updateDisplayedPlaylistEpisodesIfSelected() { |
| 397 | + if (!selectedPlaylist || selectedPlaylist.isVirtual) return; |
| 398 | +
|
| 399 | + const name = selectedPlaylist.name; |
| 400 | + if (name === get(queue).name) return; |
| 401 | +
|
| 402 | + let live: Playlist | undefined; |
| 403 | + if (name === get(favorites).name) live = get(favorites); |
| 404 | + else if (name === get(localFiles).name) live = get(localFiles); |
| 405 | + else { |
| 406 | + live = get(playlists)[name]; |
| 407 | + if (!live) { |
| 408 | + // A custom playlist deleted while open: fall back to Latest Episodes. |
| 409 | + showLatestEpisodes(); |
| 410 | + return; |
| 411 | + } |
| 412 | + } |
| 413 | +
|
| 414 | + if (!live) return; |
| 415 | +
|
| 416 | + selectedPlaylist = live; |
| 417 | + displayedEpisodes = currentSearchQuery |
| 418 | + ? searchEpisodes(currentSearchQuery, live.episodes) |
| 419 | + : live.episodes; |
| 420 | + } |
| 421 | +
|
364 | 422 | function showLatestEpisodes() { |
365 | 423 | selectedFeed = null; |
366 | 424 | selectedPlaylist = null; |
|
428 | 486 | setFeedLoading(feed.title, true); |
429 | 487 |
|
430 | 488 | try { |
431 | | - const episodes = await fetchFullEpisodes(feed); |
| 489 | + const episodes = await fetchFullEpisodes(feed, true); |
432 | 490 | displayedEpisodes = currentSearchQuery |
433 | 491 | ? searchEpisodes(currentSearchQuery, episodes) |
434 | 492 | : episodes; |
|
481 | 539 | return; |
482 | 540 | } |
483 | 541 |
|
| 542 | + // Latest Episodes view (no feed and no playlist selected): refresh all |
| 543 | + // feeds over the network so the aggregated list actually updates. The |
| 544 | + // latestEpisodes readable + its subscriber repopulate displayedEpisodes, |
| 545 | + // and setFeedLoading inside fetchEpisodesInAllFeeds drives the banner. |
| 546 | + if (!selectedFeed && !selectedPlaylist) { |
| 547 | + await fetchEpisodesInAllFeeds(feeds, "network"); |
| 548 | + return; |
| 549 | + } |
| 550 | +
|
484 | 551 | if (!selectedFeed) return; |
485 | 552 |
|
486 | 553 | setFeedLoading(selectedFeed.title, true); |
|
489 | 556 | const episodes = await fetchEpisodesByStrategy( |
490 | 557 | selectedFeed, |
491 | 558 | "network", |
| 559 | + true, |
492 | 560 | ); |
493 | 561 | displayedEpisodeEntries = null; |
494 | 562 | displayedEpisodes = currentSearchQuery |
|
582 | 650 | {#if $viewState === ViewState.Player} |
583 | 651 | <EpisodePlayer /> |
584 | 652 | {:else if $viewState === ViewState.EpisodeList} |
585 | | - {#if loadingFeedNames.length > 0} |
| 653 | + {#if bannerFeedNames.length > 0} |
586 | 654 | <div class="feed-loading-banner"> |
587 | 655 | <div class="feed-loading-spinner"> |
588 | 656 | <Icon icon="loader-2" size={18} clickable={false} /> |
589 | 657 | </div> |
590 | 658 | <div class="feed-loading-text"> |
591 | 659 | <span> |
592 | | - Updating {loadingFeedNames.length} feed{loadingFeedNames.length === 1 ? "" : "s"} |
| 660 | + Updating {bannerFeedNames.length} feed{bannerFeedNames.length === 1 ? "" : "s"} |
593 | 661 | </span> |
594 | 662 | {#if loadingFeedSummary} |
595 | 663 | <span class="feed-loading-names">{loadingFeedSummary}</span> |
|
0 commit comments