From 656985add92663440baebb871f8cd6d5723117fd Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Wed, 18 Mar 2026 13:14:02 -0400 Subject: [PATCH] feat(18-02): wire batch track-details to all 4 view context menus - track-list: branch on selection count, resolve tracks from this.tracks - cover-grid: branch on selection count, resolve from expandedTracks - queue-panel: branch on indices count, resolve via libraryStore.getCachedTracks - playlist-details: branch on selection count, resolve via libraryStore - Each view determines coverArt/coverArtMixed state and calls showBatch() --- .../src/components/cover-grid/cover-grid.ts | 46 +++++++++++++++- .../playlist-details/playlist-details.ts | 52 +++++++++++++++++- .../src/components/queue-panel/queue-panel.ts | 53 ++++++++++++++++++- .../src/components/track-list/track-list.ts | 42 ++++++++++++++- 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index b59fb72..e420739 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -26,6 +26,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@components/playlist-picker/playlist-picker.js'; import '@components/track-details/track-details.js'; import type { TrackDetails } from '@components/track-details/track-details.js'; +import type { CoverArtUrls } from '@components/track-details/track-details.js'; import { AlbumSelectionManager } from './album-selection.js'; import { ScrollManager } from './scroll-manager.js'; import type { ScrollManagerHost } from './scroll-manager.js'; @@ -1524,7 +1525,11 @@ export class CoverGrid queueStore.playTracksNext(filePaths); break; case 'track-details': - this.openTrackDetails(filePaths[0]!); + if (filePaths.length === 1) { + this.openTrackDetails(filePaths[0]!); + } else { + this.openBatchTrackDetails(filePaths); + } break; } @@ -1580,6 +1585,45 @@ export class CoverGrid ); } + private openBatchTrackDetails( + filePaths: string[], + ) { + const tracks = filePaths + .map((fp) => + this.expandedTracks.find( + (t) => t.FilePath === fp, + ), + ) + .filter( + (t): t is library.Track => t != null, + ); + + if (tracks.length === 0) return; + + const albumNames = new Set( + tracks.map((t) => t.Album), + ); + let coverArt: CoverArtUrls | null = null; + let coverArtMixed = false; + + if (albumNames.size === 1) { + const albumName = [...albumNames][0]!; + coverArt = + this.selMgr.resolveTrackCoverArt( + albumName, + this.expandedAlbumId, + ); + } else { + coverArtMixed = true; + } + + this.trackDetailsDialog?.showBatch( + tracks, + coverArt, + coverArtMixed, + ); + } + /** Resolve file paths for the playlist submenu. */ private async getPlaylistSubmenuFilePaths(): Promise< string[] diff --git a/frontend/src/components/playlist-details/playlist-details.ts b/frontend/src/components/playlist-details/playlist-details.ts index 63b6378..c58174d 100644 --- a/frontend/src/components/playlist-details/playlist-details.ts +++ b/frontend/src/components/playlist-details/playlist-details.ts @@ -17,7 +17,7 @@ import { RemovePhantomTracks, FindDuplicateTracksInPlaylist, } from '@go/playlist/Service'; -import type { playlist } from '@go/models'; +import type { playlist, library } from '@go/models'; import { EventsOn } from '@runtime/runtime'; import { Events } from '../../events'; import { queueStore } from '@store/queue-store'; @@ -350,7 +350,11 @@ export class PlaylistDetails void this.removeSelectedTracks(); break; case 'track-details': - this.openTrackDetails(filePaths[0]!); + if (filePaths.length === 1) { + this.openTrackDetails(filePaths[0]!); + } else { + this.openBatchTrackDetails(filePaths); + } break; case 'phantom-locate': this.openPhantomResolver(); @@ -446,6 +450,50 @@ export class PlaylistDetails ); } + private openBatchTrackDetails( + filePaths: string[], + ) { + const cachedTracks = + libraryStore.getCachedTracks(); + + if (!cachedTracks) return; + + const tracks = filePaths + .map((fp) => + cachedTracks.find( + (t) => t.FilePath === fp, + ), + ) + .filter( + (t): t is library.Track => + t != null, + ); + + if (tracks.length === 0) return; + + const albumNames = new Set( + tracks.map((t) => t.Album), + ); + let coverArt: CoverArtUrls | null = null; + let coverArtMixed = false; + + if (albumNames.size === 1) { + const albumName = [...albumNames][0]!; + coverArt = + this.resolvePlaylistCoverArt( + albumName, + ); + } else { + coverArtMixed = true; + } + + this.trackDetailsDialog?.showBatch( + tracks, + coverArt, + coverArtMixed, + ); + } + private resolvePlaylistCoverArt( albumName: string, ): CoverArtUrls | null { diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index 843ca0c..211bea5 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -40,6 +40,7 @@ import { removeDragImage, } from '@utils/drag-image'; import { libraryStore } from '@store/library-store'; +import type { library } from '@go/models'; import '@components/track-details/track-details.js'; import type { TrackDetails } from '@components/track-details/track-details.js'; import type { CoverArtUrls } from '@components/track-details/track-details.js'; @@ -817,7 +818,11 @@ export class QueuePanel ); break; case 'track-details': - this.openTrackDetails(indices[0]!); + if (indices.length === 1) { + this.openTrackDetails(indices[0]!); + } else { + this.openBatchTrackDetails(indices); + } break; } @@ -869,6 +874,52 @@ export class QueuePanel ); } + private openBatchTrackDetails( + indices: number[], + ) { + const queueTracks = this.queue.tracks; + const cachedTracks = + libraryStore.getCachedTracks(); + + if (!cachedTracks) return; + + const tracks = indices + .map((i) => queueTracks[i]) + .filter((qt) => qt != null) + .map((qt) => + cachedTracks.find( + (t) => + t.FilePath === qt.filePath, + ), + ) + .filter( + (t): t is library.Track => + t != null, + ); + + if (tracks.length === 0) return; + + const albumNames = new Set( + tracks.map((t) => t.Album), + ); + let coverArt: CoverArtUrls | null = null; + let coverArtMixed = false; + + if (albumNames.size === 1) { + const albumName = [...albumNames][0]!; + coverArt = + this.resolveQueueCoverArt(albumName); + } else { + coverArtMixed = true; + } + + this.trackDetailsDialog?.showBatch( + tracks, + coverArt, + coverArtMixed, + ); + } + private resolveQueueCoverArt( albumName: string, ): CoverArtUrls | null { diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index 432e136..ad401c8 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -1442,7 +1442,11 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH queueStore.playTracksNext(filePaths); break; case 'track-details': - this.openTrackDetails(filePaths[0]!); + if (filePaths.length === 1) { + this.openTrackDetails(filePaths[0]!); + } else { + this.openBatchTrackDetails(filePaths); + } break; } @@ -1486,6 +1490,42 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH ); } + private openBatchTrackDetails( + filePaths: string[], + ) { + const tracks = filePaths + .map((fp) => + this.tracks.find( + (t) => t.FilePath === fp, + ), + ) + .filter( + (t): t is library.Track => t != null, + ); + + if (tracks.length === 0) return; + + const albumNames = new Set( + tracks.map((t) => t.Album), + ); + let coverArt: CoverArtUrls | null = null; + let coverArtMixed = false; + + if (albumNames.size === 1) { + const albumName = [...albumNames][0]!; + coverArt = + this.resolveCoverArt(albumName); + } else { + coverArtMixed = true; + } + + this.trackDetailsDialog?.showBatch( + tracks, + coverArt, + coverArtMixed, + ); + } + private resolveCoverArt( albumName: string, ): CoverArtUrls | null {