audio file info added, fixed right click selecting.
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user