audio file info added, fixed right click selecting.

This commit is contained in:
2026-02-23 19:05:57 -05:00
parent a6b476399e
commit 16060023bb
23 changed files with 798 additions and 134 deletions
@@ -84,6 +84,14 @@ export class ArtistsView extends LitElement {
@state()
private contextMenuOpen = false;
/**
* Artist ID that was right-clicked to open the
* context menu. Used as fallback when the
* right-clicked artist is not in the current
* visual selection.
*/
private contextMenuArtistId: number | null = null;
@state()
private playlistSubmenuOpen = false;
@@ -755,6 +763,40 @@ export class ArtistsView extends LitElement {
return allPaths;
}
/**
* Return file paths for the context menu target.
* If the right-clicked artist is part of the
* current selection, return paths for all selected
* artists. Otherwise return paths for the
* right-clicked artist only.
*/
private async getContextMenuArtistFilePaths(): Promise<
string[]
> {
if (
this.contextMenuArtistId !== null &&
!this.selectedArtists.has(
this.contextMenuArtistId,
)
) {
const artist = this.artists.find(
(a) =>
a.ID ===
this.contextMenuArtistId,
);
if (artist) {
return this.getArtistFilePaths(
artist,
);
}
return [];
}
return this.getSelectedArtistFilePaths();
}
/** Clear the current artist selection. */
private clearSelection() {
this.selectedArtists = new Set();
@@ -831,21 +873,7 @@ export class ArtistsView extends LitElement {
e.preventDefault();
e.stopPropagation();
// If right-clicked artist is not in the
// current selection, replace the selection
// with just this artist.
if (
!this.selectedArtists.has(artist.ID)
) {
const idx =
this.filteredArtists.indexOf(artist);
this.selectedArtists = new Set([
artist.ID,
]);
this.lastSelectedArtistIndex =
idx >= 0 ? idx : null;
}
this.contextMenuArtistId = artist.ID;
this.openContextMenuAt(
e.clientX,
@@ -888,6 +916,7 @@ export class ArtistsView extends LitElement {
this.closePlaylistSubmenu();
this.contextMenuOpen = false;
this.playlistFilePaths = [];
this.contextMenuArtistId = null;
const popup = this.contextMenuPopup;
@@ -899,10 +928,8 @@ export class ArtistsView extends LitElement {
private async onContextMenuAction(
action: string,
) {
if (this.selectedArtists.size === 0) return;
const filePaths =
await this.getSelectedArtistFilePaths();
await this.getContextMenuArtistFilePaths();
if (filePaths.length === 0) return;
@@ -949,10 +976,12 @@ export class ArtistsView extends LitElement {
if (this.playlistSubmenuOpen) return;
if (this.selectedArtists.size === 0) return;
this.playlistFilePaths =
await this.getSelectedArtistFilePaths();
await this.getContextMenuArtistFilePaths();
if (this.playlistFilePaths.length === 0) {
return;
}
this.playlistSubmenuOpen = true;
@@ -1,5 +1,6 @@
import { LitElement, html, css, nothing } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { EventsOn } from '@runtime/runtime';
import { Scan, FullRescan } from '@go/library/Library';
import {
@@ -980,7 +981,7 @@ export class ConfigPage extends LitElement {
description="Choose which columns are visible and set their display order."
>
<ul class="column-list">
${order.map((id, idx) => {
${repeat(order, (id) => id, (id, idx) => {
const checked =
enabledIds.includes(id);
const onlyOne =
@@ -646,6 +646,14 @@ export class CoverGrid extends LitElement {
kind: 'album',
};
/**
* Album ID that was right-clicked to open the
* context menu. Used as fallback when the
* right-clicked album is not part of the current
* visual selection.
*/
private contextMenuAlbumId: number | null = null;
@state()
private selectedAlbums: Set<number> = new Set();
@@ -2371,6 +2379,40 @@ export class CoverGrid extends LitElement {
return allPaths;
}
/**
* Return file paths for the context menu target.
* If the right-clicked album is part of the current
* selection, return paths for all selected albums.
* Otherwise return paths for the right-clicked
* album only.
*/
private async getContextMenuAlbumFilePaths(): Promise<
string[]
> {
if (
this.contextMenuAlbumId !== null &&
!this.selectedAlbums.has(
this.contextMenuAlbumId,
)
) {
const album = this.albums.find(
(a) =>
a.ID ===
this.contextMenuAlbumId,
);
if (album) {
return this.getAlbumFilePaths(
album,
);
}
return [];
}
return this.getSelectedAlbumFilePaths();
}
private async getAlbumFilePaths(
album: library.Album,
): Promise<string[]> {
@@ -2704,14 +2746,7 @@ export class CoverGrid extends LitElement {
e.preventDefault();
e.stopPropagation();
if (!this.selectedAlbums.has(hit.album.ID)) {
this.selectedAlbums = new Set([
hit.album.ID,
]);
this.syncDropdownToSelection();
void this.warmAlbumFilePathCache();
}
this.contextMenuAlbumId = hit.album.ID;
this.contextMenuTarget = { kind: 'album' };
this.openContextMenuAt(e.clientX, e.clientY);
};
@@ -3059,10 +3094,15 @@ export class CoverGrid extends LitElement {
}
private async onContextMenuAction(action: string) {
const filePaths =
this.contextMenuTarget.kind === 'track'
? this.getSelectedTrackFilePaths()
: await this.getSelectedAlbumFilePaths();
let filePaths: string[];
if (this.contextMenuTarget.kind === 'track') {
filePaths =
this.getSelectedTrackFilePaths();
} else {
filePaths =
await this.getContextMenuAlbumFilePaths();
}
if (filePaths.length === 0) return;
@@ -3125,6 +3165,7 @@ export class CoverGrid extends LitElement {
this.closePlaylistSubmenu();
this.contextMenuOpen = false;
this.playlistFilePaths = [];
this.contextMenuAlbumId = null;
if (clearSelection) {
if (
@@ -3166,9 +3207,9 @@ export class CoverGrid extends LitElement {
if (this.contextMenuTarget.kind === 'track') {
this.playlistFilePaths =
this.getSelectedTrackFilePaths();
} else if (this.selectedAlbums.size > 0) {
} else {
this.playlistFilePaths =
await this.getSelectedAlbumFilePaths();
await this.getContextMenuAlbumFilePaths();
}
this.playlistSubmenuOpen = true;
@@ -87,6 +87,14 @@ export class GenresView extends LitElement {
@state()
private contextMenuOpen = false;
/**
* Genre name that was right-clicked to open the
* context menu. Used as fallback when the
* right-clicked genre is not in the current
* visual selection.
*/
private contextMenuGenreName: string | null = null;
@state()
private playlistSubmenuOpen = false;
@@ -801,6 +809,45 @@ export class GenresView extends LitElement {
return allPaths;
}
/**
* Return file paths for the context menu target.
* If the right-clicked genre is part of the
* current selection, return paths for all selected
* genres. Otherwise return paths for the
* right-clicked genre only.
*/
private getContextMenuGenreFilePaths(): string[] {
if (
this.contextMenuGenreName !== null &&
!this.selectedGenres.has(
this.contextMenuGenreName,
)
) {
const paths: string[] = [];
const seen = new Set<string>();
for (const track of this.allTracks) {
if (seen.has(track.FilePath)) {
continue;
}
const genres = track.Genre ?? [];
const match = genres.includes(
this.contextMenuGenreName,
);
if (match) {
paths.push(track.FilePath);
seen.add(track.FilePath);
}
}
return paths;
}
return this.getSelectedGenreFilePaths();
}
/** Clear the current genre selection. */
private clearSelection() {
this.selectedGenres = new Set();
@@ -876,19 +923,7 @@ export class GenresView extends LitElement {
e.preventDefault();
e.stopPropagation();
// If right-clicked genre is not in the
// current selection, replace the selection
// with just this genre.
if (!this.selectedGenres.has(genre.name)) {
const idx =
this.filteredGenres.indexOf(genre);
this.selectedGenres = new Set([
genre.name,
]);
this.lastSelectedGenreIndex =
idx >= 0 ? idx : null;
}
this.contextMenuGenreName = genre.name;
this.openContextMenuAt(
e.clientX,
@@ -931,6 +966,7 @@ export class GenresView extends LitElement {
this.closePlaylistSubmenu();
this.contextMenuOpen = false;
this.playlistFilePaths = [];
this.contextMenuGenreName = null;
const popup = this.contextMenuPopup;
@@ -940,10 +976,8 @@ export class GenresView extends LitElement {
}
private onContextMenuAction(action: string) {
if (this.selectedGenres.size === 0) return;
const filePaths =
this.getSelectedGenreFilePaths();
this.getContextMenuGenreFilePaths();
if (filePaths.length === 0) return;
@@ -990,10 +1024,12 @@ export class GenresView extends LitElement {
if (this.playlistSubmenuOpen) return;
if (this.selectedGenres.size === 0) return;
this.playlistFilePaths =
this.getSelectedGenreFilePaths();
this.getContextMenuGenreFilePaths();
if (this.playlistFilePaths.length === 0) {
return;
}
this.playlistSubmenuOpen = true;
@@ -5,6 +5,13 @@ import {
query,
} from 'lit/decorators.js';
import type { library } from '@go/models';
import {
formatSampleRate,
formatBitDepth,
formatChannels,
formatBitrate,
formatFileSize,
} from '@utils/format';
import { formatMilliseconds } from '@utils/time';
import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
@@ -182,6 +189,15 @@ export class TrackDetails extends LitElement {
margin-bottom: 16px;
}
.section-label {
font-size: 11px;
font-weight: 600;
color: var(--yj-text-tertiary, #888);
text-transform: uppercase;
letter-spacing: 0.8px;
margin-bottom: 10px;
}
.metadata-grid {
display: grid;
grid-template-columns: 120px 1fr;
@@ -330,6 +346,13 @@ export class TrackDetails extends LitElement {
<div class="metadata-grid">
${this.renderDetailFields(t)}
</div>
<div class="divider"></div>
<div class="section-label">
Audio Properties
</div>
<div class="metadata-grid">
${this.renderAudioProperties(t)}
</div>
<div class="action-bar">
${this.renderActions()}
</div>
@@ -511,6 +534,45 @@ export class TrackDetails extends LitElement {
return fields.map((f) => this.renderField(f));
}
private renderAudioProperties(t: library.Track) {
const props: { label: string; value: string }[] = [
{
label: 'Sample Rate',
value: formatSampleRate(t.SampleRate),
},
{
label: 'Bit Depth',
value: formatBitDepth(t.BitDepth),
},
{
label: 'Channels',
value: formatChannels(t.Channels),
},
{
label: 'Bitrate',
value: formatBitrate(t.Bitrate),
},
{
label: 'File Size',
value: formatFileSize(t.FileSize),
},
];
return props.map(
(p) => html`
<span class="meta-label">${p.label}</span>
<span
class="meta-value ${p.value ===
'\u2014'
? 'empty'
: ''}"
>
${p.value}
</span>
`,
);
}
private renderField(f: MetadataField) {
const display =
this.getEditValue(f.key, f.value) || f.value;
@@ -1,4 +1,11 @@
import type { library } from '@go/models';
import {
formatSampleRate,
formatBitDepth,
formatChannels,
formatBitrate,
formatFileSize,
} from '@utils/format';
import { formatMilliseconds } from '@utils/time';
/** Compares two strings using locale-aware ordering. */
@@ -135,6 +142,50 @@ export const COLUMN_DEFS: Record<string, ColumnDef> = {
comparator: (a, b) =>
compareStr(a.FileType, b.FileType),
},
sampleRate: {
id: 'sampleRate',
label: 'Sample Rate',
accessor: (t) => formatSampleRate(t.SampleRate),
defaultWidth: '100px',
align: 'right',
comparator: (a, b) =>
compareNum(a.SampleRate, b.SampleRate),
},
bitDepth: {
id: 'bitDepth',
label: 'Bit Depth',
accessor: (t) => formatBitDepth(t.BitDepth),
defaultWidth: '80px',
align: 'right',
comparator: (a, b) =>
compareNum(a.BitDepth, b.BitDepth),
},
channels: {
id: 'channels',
label: 'Channels',
accessor: (t) => formatChannels(t.Channels),
defaultWidth: '80px',
comparator: (a, b) =>
compareNum(a.Channels, b.Channels),
},
bitrate: {
id: 'bitrate',
label: 'Bitrate',
accessor: (t) => formatBitrate(t.Bitrate),
defaultWidth: '100px',
align: 'right',
comparator: (a, b) =>
compareNum(a.Bitrate, b.Bitrate),
},
fileSize: {
id: 'fileSize',
label: 'File Size',
accessor: (t) => formatFileSize(t.FileSize),
defaultWidth: '80px',
align: 'right',
comparator: (a, b) =>
compareNum(a.FileSize, b.FileSize),
},
};
/**
@@ -894,6 +894,10 @@ export class TrackList extends LitElement implements SelectionHost {
text-align: right;
}
.cell-center {
text-align: center;
}
#context-menu {
z-index: 200;
}
@@ -1540,13 +1544,21 @@ export class TrackList extends LitElement implements SelectionHost {
this.onTrackDragStart(e, track)}
@dragend=${this.onTrackDragEnd}
>
${cols.map(
(col) => html`
<div class="cell ${col.align === 'right' ? 'cell-right' : ''}">
${col.accessor(track)}
</div>
`,
)}
${cols.map((col) => {
const val = col.accessor(track);
const centered = val === '\u2014';
const align = centered
? 'cell-center'
: col.align === 'right'
? 'cell-right'
: '';
return html`
<div class="cell ${align}">
${val}
</div>
`;
})}
</div>
`;
};