From 84f027aedf4ea2e3c836d7f2558978e06d74fbe8 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Fri, 20 Feb 2026 15:22:49 -0500 Subject: [PATCH] small ui changes --- frontend/package.json.md5 | 2 +- frontend/pnpm-lock.yaml | 21 ----- .../components/playlist-view/playlist-view.ts | 85 ++++++++++++++++--- .../store/controllers/playlist-controller.ts | 8 ++ frontend/src/store/playlist-store.ts | 25 ++++++ 5 files changed, 105 insertions(+), 36 deletions(-) diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 5746819..954d495 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -cf76bbfd46ad4447fbfbaa4a1c6845ca \ No newline at end of file +db9e9335c200a37f58ae820ffcfee304 \ No newline at end of file diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 1df57e2..587f753 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,9 +17,6 @@ importers: '@lit-labs/virtualizer': specifier: ^2.1.1 version: 2.1.1 - htmx.org: - specifier: 2.0.8 - version: 2.0.8 lit: specifier: ^3.2.1 version: 3.3.2 @@ -350,79 +347,66 @@ packages: resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.57.1': resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.57.1': resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.57.1': resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.57.1': resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.57.1': resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.57.1': resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.57.1': resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.57.1': resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.57.1': resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.57.1': resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.57.1': resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.57.1': resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.57.1': resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} @@ -709,9 +693,6 @@ packages: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} - htmx.org@2.0.8: - resolution: {integrity: sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -1685,8 +1666,6 @@ snapshots: html-tags@5.1.0: {} - htmx.org@2.0.8: {} - ignore@7.0.5: {} import-fresh@3.3.1: diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index 0121095..479f950 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -67,6 +67,7 @@ export class PlaylistView @state() private entries: PlaylistEntry[] = []; @state() private loading = true; + @state() private refreshing = false; @state() private creating = false; @state() private newPlaylistName = ''; @state() private contextMenuOpen = false; @@ -213,6 +214,25 @@ export class PlaylistView font-size: 18px; font-weight: 600; color: var(--yj-text-primary, #fff); + display: flex; + align-items: center; + gap: 10px; + } + + @keyframes spin { + to { + transform: rotate(360deg); + } + } + + .header-spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid var(--yj-border-subtle, #555); + border-top-color: var(--yj-text-primary, #fff); + border-radius: 50%; + animation: spin 0.6s linear infinite; } .new-playlist-button { @@ -644,6 +664,41 @@ export class PlaylistView this.restoreScrollPosition(); } + /** + * Re-fetches playlists without clearing the current view. + * Shows a spinner in the header while the fetch is in-flight + * and preserves the expanded/collapsed state of each playlist. + */ + private async refreshPlaylists() { + this.refreshing = true; + + try { + const playlists = + await this.playlistCtrl.refetch(); + + const expandedIDs = new Set( + this.entries + .filter((e) => e.expanded) + .map((e) => e.summary.ID), + ); + + this.entries = playlists.map((p) => ({ + summary: p.Summary, + expanded: expandedIDs.has( + p.Summary.ID, + ), + tracks: p.Tracks ?? [], + })); + } catch (err) { + console.error( + 'Failed to refresh playlists:', + err, + ); + } finally { + this.refreshing = false; + } + } + private handleToggle = (index: number) => { const entry = this.entries[index]; @@ -796,8 +851,7 @@ export class PlaylistView trackIDs, ); - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to remove tracks:', @@ -954,8 +1008,7 @@ export class PlaylistView entry.summary.ID, payload.filePaths, ); - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to add tracks to playlist:', @@ -1127,8 +1180,7 @@ export class PlaylistView ) { try { await DeletePlaylist(playlistID); - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to delete playlist:', @@ -1183,8 +1235,7 @@ export class PlaylistView entry.summary.ID, trimmed, ); - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to rename playlist:', @@ -1205,8 +1256,7 @@ export class PlaylistView if (!filePath) return; await ImportPlaylist(filePath); - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to import playlist:', @@ -1246,8 +1296,7 @@ export class PlaylistView await CreatePlaylist(name); this.creating = false; this.newPlaylistName = ''; - this.playlistCtrl.invalidate(); - await this.loadPlaylists(); + await this.refreshPlaylists(); } catch (err) { console.error( 'Failed to create playlist:', @@ -1276,7 +1325,14 @@ export class PlaylistView override render() { return html`
-

Playlists

+

+ Playlists + ${this.refreshing + ? html`` + : nothing} +

@@ -1306,7 +1362,8 @@ export class PlaylistView ${this.creating ? this.renderCreateForm() : nothing} - ${this.loading + ${this.loading && + this.entries.length === 0 ? html`
Loading playlists...
` diff --git a/frontend/src/store/controllers/playlist-controller.ts b/frontend/src/store/controllers/playlist-controller.ts index f71fe22..8744aa2 100644 --- a/frontend/src/store/controllers/playlist-controller.ts +++ b/frontend/src/store/controllers/playlist-controller.ts @@ -65,6 +65,14 @@ export class PlaylistController implements ReactiveController { playlistStore.setScrollPosition(offset); } + // =================================================================== + // REFETCH + // =================================================================== + + async refetch(): Promise { + return playlistStore.refetch(); + } + // =================================================================== // INVALIDATION // =================================================================== diff --git a/frontend/src/store/playlist-store.ts b/frontend/src/store/playlist-store.ts index a2e008e..9b5cff1 100644 --- a/frontend/src/store/playlist-store.ts +++ b/frontend/src/store/playlist-store.ts @@ -90,6 +90,31 @@ class PlaylistStore { this.scrollPosition = offset; } + // =================================================================== + // REFETCH + // Fetches fresh data without clearing the cache first, so existing + // consumers keep rendering stale data until the new data arrives. + // =================================================================== + + async refetch(): Promise { + if (this.playlistsLoading) { + return this.waitForPlaylists(); + } + + this.playlistsLoading = true; + + try { + const result = + await GetAllPlaylistsWithTracks(); + this.playlists = result ?? []; + + return this.playlists; + } finally { + this.playlistsLoading = false; + this.notify(); + } + } + // =================================================================== // INVALIDATION // ===================================================================