From 955cd68be2dbf7a9071ef1c93084d687b59b6bd7 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sat, 7 Mar 2026 22:10:24 -0500 Subject: [PATCH] refactor(quick-17): simplify playlist-view to navigate instead of expand - Plain click now navigates to playlist-details subpage instead of toggling expand/collapse - Removed all inline track expansion: renderPlaylistBody, track-item rendering, chevron icons - Removed SelectionController, ContextMenuController, PlayerController (track-level interactions moved to playlist-details) - Removed track-info, track-details, phantom-resolver imports (all live in playlist-details now) - Simplified search to filter by playlist name only (no inline track search) - Kept: playlist list, context menu (rename/delete/set-default), Ctrl/Shift multi-select, drag-drop target, create/import, sort toolbar --- .../components/playlist-view/playlist-view.ts | 1332 +---------------- 1 file changed, 21 insertions(+), 1311 deletions(-) diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index 25d1fa2..c459918 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -9,46 +9,23 @@ import { CreatePlaylist, CreatePlaylistWithTracks, AddTracksToPlaylist, - RemoveTracksFromPlaylist, DeletePlaylist, RenamePlaylist, ImportPlaylists, - RemovePhantomTracks, FindDuplicateTracksInPlaylist, } from '@go/playlist/Service'; import { PlaylistFilePicker } from '@go/frontendutil/FrontendUtil'; import type { playlist } from '@go/models'; -import { queueStore } from '@store/queue-store'; -import { PlayerController } from '@store/controllers/player-controller'; import { PlaylistController } from '@store/controllers/playlist-controller'; import { SearchController } from '@store/controllers/search-controller'; -import '@components/track-info/track-info'; -import '@components/playlist-picker/playlist-picker.js'; -import { SelectionController } from '@utils/selection-controller'; -import type { SelectionHost } from '@utils/selection-controller'; import { hasTrackPayload, getDragPayload, - setDragPayload, - emitDragActive, getActiveDragSource, getActiveDragPlaylistId, } from '@utils/drag-controller'; -import { - createDragImage, - createTrackCardDragImage, - removeDragImage, -} from '@utils/drag-image'; -import { libraryStore } from '@store/library-store'; -import { ContextMenuController } from '@utils/context-menu-controller.js'; -import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import { contextMenuStyles } from '@utils/context-menu-controller.js'; import { FavoritesController } from '@store/controllers/favorites-controller'; -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 '@components/phantom-resolver/phantom-resolver.js'; -import type { PhantomResolver } from '@components/phantom-resolver/phantom-resolver.js'; import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; import type { DuplicateTracksDialog } from '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; @@ -69,31 +46,15 @@ const SORT_OPTIONS: { id: PlaylistSortField; label: string }[] = [ interface PlaylistEntry { summary: playlist.Summary; - expanded: boolean; tracks: playlist.Track[]; } @customElement('playlist-view') -export class PlaylistView - extends LitElement - implements SelectionHost, ContextMenuHost -{ - private player = new PlayerController(this); +export class PlaylistView extends LitElement { private playlistCtrl = new PlaylistController(this); private searchCtrl = new SearchController(this); - private selection = new SelectionController(this); - private ctxMenu = new ContextMenuController(this); private favCtrl = new FavoritesController(this); - getContextMenuPopup(): WaPopup | undefined { - return this.contextMenuPopup; - } - - getPlaylistSubmenuPopup(): - | WaPopup - | undefined { - return this.playlistSubmenuPopup; - } /** Tracks the store's cached array reference to detect refreshes. */ private lastPlaylistsRef: | playlist.WithTracks[] @@ -104,14 +65,8 @@ export class PlaylistView > | null = null; private lastSearchTerm = ''; - /** - * Index of the playlist whose tracks are currently - * selectable. -1 means no active selection scope. - */ - private activePlaylistIndex = -1; - // ================================================================= - // Filtered entries (search) + // Filtered entries (search — playlist name only) // ================================================================= private get filteredEntries(): PlaylistEntry[] { @@ -120,77 +75,11 @@ export class PlaylistView if (!term) return this.entries; - return this.entries.filter( - (e) => - e.summary.Name.toLowerCase().includes( - term, - ) || - e.tracks.some( - (t) => - t.Title.toLowerCase().includes( - term, - ) || - t.Artist.toLowerCase().includes( - term, - ), - ), - ); - } - - /** - * Return the tracks to display for a playlist entry, - * preserving original indices for event handlers. - * When a search term is active, only tracks matching - * the term are shown. When there is no search term - * (or the playlist matched by name), all tracks are - * returned. - */ - private getVisibleTracks( - entry: PlaylistEntry, - ): { track: playlist.Track; trackIndex: number }[] { - const term = - this.searchCtrl.term.toLowerCase(); - - if (!term) { - return entry.tracks.map( - (track, trackIndex) => ({ - track, - trackIndex, - }), - ); - } - - // If the playlist name itself matches, show - // all tracks — the whole playlist is relevant. - if ( - entry.summary.Name.toLowerCase().includes( + return this.entries.filter((e) => + e.summary.Name.toLowerCase().includes( term, - ) - ) { - return entry.tracks.map( - (track, trackIndex) => ({ - track, - trackIndex, - }), - ); - } - - // Otherwise only show tracks whose metadata - // matches. - return entry.tracks - .map((track, trackIndex) => ({ - track, - trackIndex, - })) - .filter( - ({ track }) => - track.Title.toLowerCase().includes( - term, - ) || - track.Artist.toLowerCase().includes( - term, - ), - ); + ), + ); } @state() private entries: PlaylistEntry[] = []; @@ -239,23 +128,9 @@ export class PlaylistView */ private pendingDropPaths: string[] = []; - private dragImageEl: HTMLElement | null = null; - - @query('#context-menu') - private contextMenuPopup!: WaPopup; - - @query('#playlist-submenu') - private playlistSubmenuPopup!: WaPopup; - @query('#playlist-context-menu') private playlistContextMenuPopup!: WaPopup; - @query('track-details') - private trackDetailsDialog!: TrackDetails; - - @query('phantom-resolver') - private phantomResolver!: PhantomResolver; - @query('duplicate-tracks-dialog') private duplicateDialog!: DuplicateTracksDialog; @@ -277,18 +152,8 @@ export class PlaylistView this.closePlaylistContextMenu(); }; - private handleSelectAll = (): void => { - this.selection.selectAll(); - }; - private clearSelectionHandler = (e: MouseEvent) => { const path = e.composedPath(); - const isTrackClick = path.some( - (el) => - el instanceof HTMLElement && - el.classList.contains('track-item') && - this.shadowRoot?.contains(el), - ); const isPlaylistHeaderClick = path.some( (el) => el instanceof HTMLElement && @@ -296,105 +161,12 @@ export class PlaylistView this.shadowRoot?.contains(el), ); - if (!isTrackClick) { - this.selection.clear(); - } - - if (!isPlaylistHeaderClick && !isTrackClick) { + if (!isPlaylistHeaderClick) { this.selectedPlaylists = new Set(); this.lastSelectedPlaylistIndex = null; } }; - // ================================================================= - // SelectionHost interface - // ================================================================= - - getItemKey(index: number): string | undefined { - if (this.activePlaylistIndex < 0) return undefined; - - const entry = - this.entries[this.activePlaylistIndex]; - - if ( - !entry || - index < 0 || - index >= entry.tracks.length - ) { - return undefined; - } - - return String(index); - } - - getItemCount(): number { - if (this.activePlaylistIndex < 0) return 0; - - const entry = - this.entries[this.activePlaylistIndex]; - - return entry?.tracks.length ?? 0; - } - - onSelectionChanged(): void { - this.requestUpdate(); - } - - /** - * Return the selected playlist track IDs (database IDs) - * in order, for removal operations. - */ - private getSelectedTrackIDs(): number[] { - if (this.activePlaylistIndex < 0) return []; - - const entry = - this.entries[this.activePlaylistIndex]; - - if (!entry) return []; - - return this.selection - .getSelectedIndices() - .map((i) => entry.tracks[i]!.ID); - } - - /** - * Derive file paths from selected indices for - * operations that need file paths. - */ - private getSelectedFilePaths(): string[] { - if (this.activePlaylistIndex < 0) return []; - - const entry = - this.entries[this.activePlaylistIndex]; - - if (!entry) return []; - - return this.selection - .getSelectedIndices() - .map((i) => entry.tracks[i]!.FilePath); - } - - /** - * Ensure the selection scope matches the given playlist - * index. If switching playlists, clear the old selection. - */ - private ensureSelectionScope( - playlistIndex: number, - ): void { - // Clear playlist-level selection when entering track selection - if (this.selectedPlaylists.size > 0) { - this.selectedPlaylists = new Set(); - this.lastSelectedPlaylistIndex = null; - } - - if ( - this.activePlaylistIndex !== playlistIndex - ) { - this.selection.clear(); - this.activePlaylistIndex = playlistIndex; - } - } - static override styles = [ contextMenuStyles, css` @@ -564,17 +336,6 @@ export class PlaylistView outline-offset: -1px; } - .chevron { - font-size: 14px; - color: var(--yj-text-tertiary, #888); - flex-shrink: 0; - transition: transform 0.15s ease; - } - - .chevron.expanded { - transform: rotate(90deg); - } - .playlist-icon { font-size: 18px; color: var(--yj-text-tertiary, #888); @@ -596,151 +357,6 @@ export class PlaylistView flex-shrink: 0; } - .playlist-body { - padding: 0 16px 12px 32px; - } - - .playlist-actions { - display: flex; - align-items: center; - gap: 8px; - padding-bottom: 8px; - } - - .play-all-button { - background: none; - border: 1px solid var(--yj-border-subtle, #555); - border-radius: 4px; - color: var(--yj-text-primary, #fff); - padding: 4px 10px; - font-size: 12px; - cursor: pointer; - display: flex; - align-items: center; - gap: 5px; - font-family: inherit; - } - - .play-all-button:hover { - border-color: var(--yj-accent, #ffd43b); - color: var(--yj-accent, #ffd43b); - } - - .track-item { - padding: 6px 0; - border-bottom: 1px solid - rgba(255, 255, 255, 0.03); - cursor: default; - user-select: none; - } - - .track-item:hover { - background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05)); - } - - .track-item.selected { - background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15)); - } - - .track-item.active { - background-color: var(--yj-accent-bg, rgba(255, 212, 59, 0.1)); - color: var(--yj-accent, #ffd43b); - } - - .track-item.selected.active { - background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15)); - } - - .track-item.phantom { - cursor: pointer; - } - - .track-item.phantom:hover { - background-color: var( - --yj-hover-overlay, - rgba(255, 255, 255, 0.05) - ); - } - - .track-item.phantom.selected { - background-color: var( - --yj-selection-bg, - rgba(100, 160, 255, 0.15) - ); - } - - .phantom-row { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; - width: 100%; - } - - .phantom-caution { - flex-shrink: 0; - font-size: 14px; - color: var(--yj-warning, #e67700); - } - - .phantom-path { - flex: 1; - min-width: 0; - font-size: 12px; - color: var(--yj-text-tertiary, #888); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .phantom-actions { - display: flex; - align-items: center; - gap: 2px; - flex-shrink: 0; - } - - .phantom-icon-btn { - display: flex; - align-items: center; - justify-content: center; - background: none; - border: none; - color: var(--yj-text-tertiary, #888); - cursor: pointer; - padding: 4px; - border-radius: 3px; - font-size: 13px; - } - - .phantom-icon-btn:hover { - color: var( - --yj-text-primary, - #fff - ); - background: rgba( - 255, - 255, - 255, - 0.08 - ); - } - - .phantom-icon-btn.phantom-icon-remove:hover { - color: var(--yj-error, #e03131); - background: rgba(224, 49, 49, 0.12); - } - - .track-item:last-child { - border-bottom: none; - } - - .tracks-empty { - padding: 12px 0; - color: var(--yj-text-tertiary, #666); - font-size: 12px; - } - .loading { display: flex; justify-content: center; @@ -1200,10 +816,6 @@ export class PlaylistView 'mousedown', this.sortDropdownCloseHandler, ); - document.addEventListener( - 'shortcut:select-all', - this.handleSelectAll, - ); } override disconnectedCallback() { @@ -1234,10 +846,6 @@ export class PlaylistView 'mousedown', this.sortDropdownCloseHandler, ); - document.removeEventListener( - 'shortcut:select-all', - this.handleSelectAll, - ); } override updated() { @@ -1245,31 +853,8 @@ export class PlaylistView if (currentTerm !== this.lastSearchTerm) { this.lastSearchTerm = currentTerm; - this.selection.clear(); - this.activePlaylistIndex = -1; - - const term = currentTerm.toLowerCase(); - - this.entries = this.entries.map((e) => { - if (!term) { - return { ...e, expanded: false }; - } - - const hasTrackMatch = e.tracks.some( - (t) => - t.Title.toLowerCase().includes( - term, - ) || - t.Artist.toLowerCase().includes( - term, - ), - ); - - return { - ...e, - expanded: hasTrackMatch, - }; - }); + this.selectedPlaylists = new Set(); + this.lastSelectedPlaylistIndex = null; } // Re-fetch when the store delivers fresh @@ -1326,7 +911,6 @@ export class PlaylistView this.entries = playlists.map((p) => ({ summary: p.Summary, - expanded: false, tracks: p.Tracks ?? [], })); } catch (err) { @@ -1345,8 +929,7 @@ export class PlaylistView /** * 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. + * Shows a spinner in the header while the fetch is in-flight. */ private async refreshPlaylists() { this.refreshing = true; @@ -1355,17 +938,8 @@ export class PlaylistView 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) { @@ -1401,9 +975,6 @@ export class PlaylistView this.selectedPlaylists = next; this.lastSelectedPlaylistIndex = index; - // Clear track-level selection - this.selection.clear(); - this.activePlaylistIndex = -1; return; } @@ -1418,333 +989,26 @@ export class PlaylistView } this.selectedPlaylists = next; - // Clear track-level selection - this.selection.clear(); - this.activePlaylistIndex = -1; return; } - // Plain click: clear playlist selection, toggle expand/collapse + // Plain click: navigate to playlist details this.selectedPlaylists = new Set(); this.lastSelectedPlaylistIndex = null; - // If collapsing the active playlist, clear selection. - if ( - entry.expanded && - this.activePlaylistIndex === index - ) { - this.selection.clear(); - this.activePlaylistIndex = -1; - } - - this.entries = this.entries.map((e, i) => - i === index - ? { ...e, expanded: !e.expanded } - : e, + this.dispatchEvent( + new CustomEvent('navigate', { + bubbles: true, + composed: true, + detail: { + view: 'playlist-details', + playlistId: entry.summary.ID, + playlistName: entry.summary.Name, + }, + }), ); }; - private handlePlayAll = (index: number) => { - const entry = this.entries[index]; - - if (!entry || entry.tracks.length === 0) - return; - - const filePaths = entry.tracks - .filter((t) => !t.Phantom) - .map((t) => t.FilePath); - - if (filePaths.length === 0) return; - - queueStore.setQueue(filePaths, 0, true); - }; - - // ================================================================= - // Track selection & context menu - // ================================================================= - - private handleTrackClick( - e: MouseEvent, - _track: playlist.Track, - trackIndex: number, - playlistIndex: number, - ) { - this.ensureSelectionScope(playlistIndex); - this.selection.handleItemClick( - e, - String(trackIndex), - trackIndex, - ); - } - - private handleTrackDblClick( - _track: playlist.Track, - trackIndex: number, - playlistIndex: number, - ) { - const entry = this.entries[playlistIndex]; - - if (!entry) return; - - this.selection.clear(); - - const filePaths = entry.tracks.map( - (t) => t.FilePath, - ); - - queueStore.setQueue(filePaths, trackIndex); - } - - private handleTrackContextMenu( - e: MouseEvent, - trackIndex: number, - playlistIndex: number, - ) { - e.preventDefault(); - e.stopPropagation(); - - this.ensureSelectionScope(playlistIndex); - this.selection.handleContextMenu( - String(trackIndex), - ); - this.ctxMenu.openAt(e.clientX, e.clientY); - } - - private onContextMenuAction(action: string) { - const filePaths = - this.getSelectedFilePaths(); - - if (filePaths.length === 0) return; - - switch (action) { - case 'play': - queueStore.setQueue(filePaths, 0, true); - break; - case 'add-to-queue': - queueStore.addTracksToQueue( - filePaths, - ); - break; - case 'play-next': - queueStore.playTracksNext(filePaths); - break; - case 'remove': - void this.removeSelectedTracks(); - break; - case 'track-details': - this.openTrackDetails(filePaths[0]!); - break; - case 'phantom-locate': - if (this.activePlaylistIndex >= 0) { - this.openPhantomResolver( - this.activePlaylistIndex, - ); - } - - break; - case 'phantom-remove': - void this.removeSelectedPhantoms(); - break; - } - - this.selection.clear(); - this.ctxMenu.close(); - } - - private onContextMenuFavoriteToggle() { - const filePaths = - this.getSelectedFilePaths(); - - if (filePaths.length === 0) return; - - if (this.favCtrl.allFavorited(filePaths)) { - void this.favCtrl.removeFromFavorites( - filePaths, - ); - } else { - void this.favCtrl.addToFavorites( - filePaths, - ); - } - - this.selection.clear(); - this.ctxMenu.close(); - } - - private async removeSelectedPhantoms(): Promise { - if (this.activePlaylistIndex < 0) return; - - const entry = - this.entries[ - this.activePlaylistIndex - ]; - - if (!entry) return; - - const selectedIndices = - this.selection.getSelectedIndices(); - const phantomPaths = selectedIndices - .map((i) => entry.tracks[i]) - .filter( - (t): t is playlist.Track => - t !== undefined && - t.Phantom, - ) - .map((t) => t.FilePath); - - if (phantomPaths.length === 0) return; - - try { - await RemovePhantomTracks( - entry.summary.ID, - phantomPaths, - ); - await this.refreshPlaylists(); - } catch (err) { - console.error( - 'Failed to remove phantom tracks:', - err, - ); - } - } - - private openTrackDetails(filePath: string) { - const tracks = - libraryStore.getCachedTracks(); - const track = tracks?.find( - (t) => t.FilePath === filePath, - ); - - if (!track) return; - - const coverArt = - this.resolvePlaylistCoverArt( - track.Album, - ); - - this.trackDetailsDialog?.show( - track, - coverArt ?? undefined, - ); - } - - private resolvePlaylistCoverArt( - albumName: string, - ): CoverArtUrls | null { - if (!albumName) return null; - - const albums = - libraryStore.getCachedAlbums(); - - if (!albums) return null; - - const album = albums.find( - (a) => a.Name === albumName, - ); - - if (!album || !album.CoverArtPath) { - return null; - } - - return { - coverArtPath: album.CoverArtPath, - coverArtSmall: album.CoverArtSmall, - coverArtMedium: album.CoverArtMedium, - coverArtLarge: album.CoverArtLarge, - }; - } - - private async removeSelectedTracks() { - if (this.activePlaylistIndex < 0) return; - - const entry = - this.entries[this.activePlaylistIndex]; - - if (!entry) return; - - const trackIDs = this.getSelectedTrackIDs(); - - if (trackIDs.length === 0) return; - - try { - await RemoveTracksFromPlaylist( - entry.summary.ID, - trackIDs, - ); - - await this.refreshPlaylists(); - } catch (err) { - console.error( - 'Failed to remove tracks:', - err, - ); - } - } - - // ================================================================= - // Drag source (playlist tracks → queue or other playlist) - // ================================================================= - - private onTrackDragStart = ( - e: DragEvent, - track: playlist.Track, - trackIndex: number, - playlistIndex: number, - ) => { - this.ensureSelectionScope(playlistIndex); - - const entry = this.entries[playlistIndex]; - - if (!entry) return; - - let filePaths: string[]; - - if ( - this.activePlaylistIndex === - playlistIndex && - this.selection.isSelected( - String(trackIndex), - ) - ) { - filePaths = this.getSelectedFilePaths(); - } else { - filePaths = [track.FilePath]; - } - - if (filePaths.length === 0) return; - - setDragPayload(e, { - filePaths, - source: 'playlist', - sourcePlaylistId: entry.summary.ID, - }); - - this.dragImageEl = - filePaths.length === 1 - ? createTrackCardDragImage( - track.Title, - track.Artist, - track.FilePath, - ) - : createDragImage(filePaths.length); - e.dataTransfer?.setDragImage( - this.dragImageEl, - 0, - 0, - ); - - emitDragActive(true); - }; - - private onTrackDragEnd = () => { - if (this.dragImageEl) { - removeDragImage(this.dragImageEl); - this.dragImageEl = null; - } - - emitDragActive(false); - }; - // ================================================================= // Drop target (tracks dropped onto a specific playlist) // ================================================================= @@ -1982,18 +1246,6 @@ export class PlaylistView this.onEmptyZoneDrop(e); }; - - - private isActiveTrack( - track: playlist.Track, - ): boolean { - const currentTrack = this.player.currentTrack; - - if (!currentTrack) return false; - - return currentTrack.filePath === track.FilePath; - } - // ================================================================= // Playlist-level context menu (rename, delete) // ================================================================= @@ -2005,8 +1257,6 @@ export class PlaylistView e.preventDefault(); e.stopPropagation(); - this.ctxMenu.close(); - // If the right-clicked playlist is NOT in the current // multi-selection, replace the selection with just that one. if (!this.selectedPlaylists.has(index)) { @@ -2186,143 +1436,6 @@ export class PlaylistView } } - /** - * Check whether all currently selected tracks are phantoms. - * Returns false if nothing is selected or the active playlist - * index is unset. - */ - private isPhantomSelection(): boolean { - if (this.activePlaylistIndex < 0) return false; - - const entry = - this.entries[this.activePlaylistIndex]; - - if (!entry) return false; - - const indices = - this.selection.getSelectedIndices(); - - if (indices.length === 0) return false; - - return indices.every((i) => { - const t = entry.tracks[i]; - - return t !== undefined && t.Phantom; - }); - } - - // ================================================================= - // Phantom track interactions - // ================================================================= - - private handlePhantomClick( - e: MouseEvent, - trackIndex: number, - playlistIndex: number, - ): void { - this.ensureSelectionScope(playlistIndex); - this.selection.handleItemClick( - e, - String(trackIndex), - trackIndex, - ); - } - - private handlePhantomContextMenu( - e: MouseEvent, - trackIndex: number, - playlistIndex: number, - ): void { - e.preventDefault(); - e.stopPropagation(); - this.ensureSelectionScope(playlistIndex); - this.selection.handleContextMenu( - String(trackIndex), - ); - this.ctxMenu.openAt(e.clientX, e.clientY); - } - - private openPhantomResolver( - playlistIndex: number, - trackIndex?: number, - ): void { - const entry = - this.entries[playlistIndex]; - - if (!entry) return; - - // Collect selected phantom tracks, or just the - // one that was clicked. - let phantoms: playlist.Track[]; - - if ( - this.activePlaylistIndex === - playlistIndex - ) { - const selectedIndices = - this.selection.getSelectedIndices(); - phantoms = selectedIndices - .map( - (i) => entry.tracks[i], - ) - .filter( - (t): t is playlist.Track => - t !== undefined && - t.Phantom, - ); - } else { - phantoms = []; - } - - // Fall back to the clicked track. - if ( - phantoms.length === 0 && - trackIndex !== undefined - ) { - const track = - entry.tracks[trackIndex]; - - if (track?.Phantom) { - phantoms = [track]; - } - } - - if (phantoms.length === 0) return; - - this.phantomResolver.show( - entry.summary.ID, - phantoms, - ); - } - - private async removePhantomTrack( - playlistIndex: number, - trackIndex: number, - ): Promise { - const entry = - this.entries[playlistIndex]; - - if (!entry) return; - - const track = - entry.tracks[trackIndex]; - - if (!track?.Phantom) return; - - try { - await RemovePhantomTracks( - entry.summary.ID, - [track.FilePath], - ); - await this.refreshPlaylists(); - } catch (err) { - console.error( - 'Failed to remove phantom track:', - err, - ); - } - } - // ================================================================= // Import playlist // ================================================================= @@ -2562,199 +1675,6 @@ export class PlaylistView ` : this.renderPlaylistList()} - - ${this.ctxMenu.contextMenuOpen - ? this.isPhantomSelection() - ? html` -
- - this.onContextMenuAction( - 'phantom-locate', - )} - > - - Locate in - Library - - - this.onContextMenuAction( - 'phantom-remove', - )} - > - - Remove from - Playlist - -
- ` - : html` -
- - this.onContextMenuAction( - 'play', - )} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - Play - - - this.onContextMenuAction( - 'add-to-queue', - )} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - Add to Queue - - - this.onContextMenuAction( - 'play-next', - )} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - Play Next - - - this.onContextMenuAction( - 'remove', - )} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - Remove from - Playlist - - { - this.ctxMenu.clearSubmenuCloseTimer(); - void this.ctxMenu.showPlaylistSubmenu(this.getSelectedFilePaths()); - }} - @mouseleave=${this - .ctxMenu - .scheduleSubmenuClose} - @click=${(e: Event) => { - e.stopPropagation(); - void this.ctxMenu.showPlaylistSubmenu(this.getSelectedFilePaths()); - }} - > - - Add to Playlist - - ▶ - - - - this.onContextMenuFavoriteToggle()} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - ${this.favCtrl.allFavorited(this.getSelectedFilePaths()) ? `Remove from ${this.favCtrl.playlistName}` : `Add to ${this.favCtrl.playlistName}`} - - ${this.selection - .selectionCount === - 1 - ? html` - - this.onContextMenuAction( - 'track-details', - )} - @mouseenter=${() => - this.ctxMenu.closePlaylistSubmenu()} - > - - Track - Details - - ` - : nothing} -
- ` - : nothing} -
- - - ${this.ctxMenu.playlistSubmenuOpen && - this.selection.hasSelection - ? html` -
- this.ctxMenu.clearSubmenuCloseTimer()} - @mouseleave=${this - .ctxMenu - .scheduleSubmenuClose} - > - - e.stopPropagation()} - > -
- ` - : nothing} -
- - - - this.refreshPlaylists()} - > this.refreshPlaylists()} @@ -2969,12 +1884,6 @@ export class PlaylistView index, )} > - ${entry.summary.ID === this.favCtrl.playlistId ? html` - ${entry.expanded - ? this.renderPlaylistBody( - entry, - index, - ) - : nothing} `; } - - private renderPlaylistBody( - entry: PlaylistEntry, - playlistIndex: number, - ) { - if (entry.tracks.length === 0) { - return html` -
-
- This playlist is empty. -
-
- `; - } - - return html` -
-
- -
- ${this.getVisibleTracks(entry).map( - ({ track, trackIndex }) => { - const isPhantom = - track.Phantom; - const active = - !isPhantom && - this.isActiveTrack( - track, - ); - const selected = - this.activePlaylistIndex === - playlistIndex && - this.selection.isSelected( - String(trackIndex), - ); - - const classes = [ - 'track-item', - active ? 'active' : '', - selected - ? 'selected' - : '', - isPhantom - ? 'phantom' - : '', - ] - .filter(Boolean) - .join(' '); - - return html` -
- this.handlePhantomClick( - e, - trackIndex, - playlistIndex, - ) - : ( - e: MouseEvent, - ) => - this.handleTrackClick( - e, - track, - trackIndex, - playlistIndex, - )} - @dblclick=${isPhantom - ? nothing - : () => - this.handleTrackDblClick( - track, - trackIndex, - playlistIndex, - )} - @contextmenu=${isPhantom - ? ( - e: MouseEvent, - ) => - this.handlePhantomContextMenu( - e, - trackIndex, - playlistIndex, - ) - : ( - e: MouseEvent, - ) => - this.handleTrackContextMenu( - e, - trackIndex, - playlistIndex, - )} - @dragstart=${isPhantom - ? nothing - : ( - e: DragEvent, - ) => - this.onTrackDragStart( - e, - track, - trackIndex, - playlistIndex, - )} - @dragend=${isPhantom - ? nothing - : this - .onTrackDragEnd} - > - ${isPhantom - ? html`
- - - ${track.FilePath} - -
- - -
-
` - : html``} -
- `; - }, - )} -
- `; - } } declare global {