From c4da37ebb02cd27f81f3535363fb42c399dfcbe6 Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 14 Oct 2026 02:09:28 +0200 Subject: [PATCH] fix(sources): no idle Refresh on held rows; Recent counts the filtered list - The Sources page offered Refresh on a source the startup auto-refresh, an import or a delete held, and refresh() then refused it. The row now offers Refresh only while it can run, keeping it (with its spinner) while that source refreshes. - The switcher showed Recent whenever the library had six sources, so a type filter down to one or two listed them twice and the keyboard visited each twice. Recent now depends on the filtered count. Co-Authored-By: Claude Opus 5.5 --- .../playlist-switcher-list.spec.ts | 4 ++++ .../playlist-switcher/playlist-switcher-list.ts | 8 +++++--- .../recent-playlists.component.html | 2 +- .../recent-playlists.component.spec.ts | 17 +++++++++++++++++ .../recent-playlists.component.ts | 12 ++++++++++++ 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.spec.ts b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.spec.ts index 373acc4f8..4994a7fdb 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.spec.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.spec.ts @@ -45,6 +45,10 @@ describe('buildSwitcherGroups', () => { expect( buildSwitcherGroups(many.slice(0, 3), ['a'], 'all', '').recent ).toEqual([]); + // A long library filtered down to one type is a short list too. + expect(buildSwitcherGroups(many, ['b'], 'xtream', '').recent).toEqual( + [] + ); }); it('filters by type and by name or host', () => { diff --git a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.ts b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.ts index 4970daaa3..000b6fbd1 100644 --- a/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.ts +++ b/libs/playlist/shared/ui/src/lib/playlist-switcher/playlist-switcher-list.ts @@ -65,8 +65,8 @@ export interface SwitcherGroups { /** * The switcher's two groups: "Recent" (newest first, only without a search - * and with enough sources to be worth it) and "All" in the given order, - * both narrowed by type and search. + * and when enough sources are shown to be worth it) and "All" in the given + * order, both narrowed by type and search. */ export function buildSwitcherGroups( ordered: readonly PlaylistMeta[], @@ -86,8 +86,10 @@ export function buildSwitcherGroups( group, playlist, }); + // Counted after the type filter: a short filtered list would show the + // same sources twice, and the keyboard would visit each twice. const showRecent = - !query.trim() && ordered.length >= SWITCHER_RECENT_MIN_SOURCES; + !query.trim() && visible.length >= SWITCHER_RECENT_MIN_SOURCES; const byId = new Map(visible.map((playlist) => [playlist._id, playlist])); const recent = showRecent ? recentIds diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.html b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.html index fd7673956..150f6afd0 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.html +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.html @@ -35,7 +35,7 @@ [playlist]="item" [current]="activePlaylistId() === item._id" [draggable]="isCustomSort()" - [canRefresh]="refresh.canRefresh(item)" + [canRefresh]="canRefresh(item)" [refreshing]="isRefreshing(item)" [removable]="isRemovable(item)" [deleting]="removal.progress.isDeleting(item._id)" diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.spec.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.spec.ts index 06ddd76b5..f08aba0a6 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.spec.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.spec.ts @@ -305,6 +305,23 @@ describe('RecentPlaylistsComponent', () => { ).toBe(true); }); + it('offers no Refresh while something else holds a source', () => { + const canRefresh = () => + fixture.debugElement + .queryAll(By.directive(SourceRowComponent)) + .map((row) => + (row.componentInstance as SourceRowComponent).canRefresh() + ); + expect(canRefresh()).toEqual([true, true, true]); + // The startup auto-refresh or an import: refresh() would refuse. + const release = TestBed.inject(SourceActivityService).begin(['m3u']); + fixture.detectChanges(); + expect(canRefresh()).toEqual([false, true, true]); + release(); + fixture.detectChanges(); + expect(canRefresh()).toEqual([true, true, true]); + }); + it('offers no Remove while something else holds a source', () => { const removable = () => fixture.debugElement diff --git a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts index 2e434fbef..7c955d852 100644 --- a/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts +++ b/libs/playlist/shared/ui/src/lib/recent-playlists/recent-playlists.component.ts @@ -230,6 +230,18 @@ export class RecentPlaylistsComponent { ); } + /** + * Refresh is offered while it can run, and kept (with its spinner) + * while this source refreshes. Held by anything else (the startup + * auto-refresh, an import, a delete), refresh() would refuse it. + */ + canRefresh(item: PlaylistMeta): boolean { + return ( + this.refresh.canRefresh(item) && + (this.isRefreshing(item) || !this.activity.isBusy(item._id)) + ); + } + /** Nothing else (a refresh, the startup auto-refresh) holds it. */ isRemovable(item: PlaylistMeta): boolean { return !this.activity.isBusy(item._id);