fix(frontend): reposition search indicator into toolbar and fix album cover art lookup

Move search indicator from absolute-positioned overlay into sort toolbar
(or dedicated search-bar-row for artists/genres views). Shows indicator
on empty-state screens. Fix cover art not displaying for expanded album
tracks by checking expandedAlbumId before the albumName guard. Add
ScanWarning model bindings.
This commit is contained in:
2026-03-05 16:07:54 -05:00
parent 6977ba8bc1
commit a29137b2ba
8 changed files with 131 additions and 63 deletions
@@ -327,12 +327,22 @@ export class ArtistsView
line-height: 1.3; line-height: 1.3;
} }
.search-bar-row {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 30px;
border-bottom: 1px solid
var(--yj-border-subtle, #333);
flex-shrink: 0;
user-select: none;
}
.search-indicator { .search-indicator {
position: absolute; position: absolute;
top: 8px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 5;
pointer-events: none; pointer-events: none;
background: var( background: var(
--yj-bg-overlay, --yj-bg-overlay,
@@ -343,7 +353,7 @@ export class ArtistsView
#b3b3b3 #b3b3b3
); );
font-size: 12px; font-size: 12px;
padding: 4px 14px; padding: 2px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid border: 1px solid
var(--yj-border-subtle, #555); var(--yj-border-subtle, #555);
@@ -1186,9 +1196,19 @@ export class ArtistsView
} }
const entries = this.cachedGridEntries; const entries = this.cachedGridEntries;
const searchBar = this.searchCtrl.term
? html`<div class="search-bar-row">
<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl
.term}&rdquo;
</div>
</div>`
: nothing;
if (entries.length === 0) { if (entries.length === 0) {
return html` return html`
${searchBar}
<div class="empty-message"> <div class="empty-message">
${this.searchCtrl.term ${this.searchCtrl.term
? 'No artists match your search.' ? 'No artists match your search.'
@@ -1198,15 +1218,7 @@ export class ArtistsView
} }
return html` return html`
${this.searchCtrl.term ${searchBar}
? html`<div
class="search-indicator"
>
Showing results for
&ldquo;${this.searchCtrl
.term}&rdquo;
</div>`
: nothing}
<div <div
class="grid-scroll-container" class="grid-scroll-container"
style=${this.restoringScroll style=${this.restoringScroll
@@ -313,10 +313,12 @@ export class AlbumSelectionManager {
albumName: string, albumName: string,
expandedAlbumId: number | null, expandedAlbumId: number | null,
): CoverArtUrls | null { ): CoverArtUrls | null {
if (!albumName) return null;
// Prefer the expanded album (we know the track // Prefer the expanded album (we know the track
// belongs to it) for an O(1) lookup. // belongs to it) for an O(1) lookup. This must
// run before the albumName guard because
// GetAlbumTracks returns tracks without an Album
// field, so albumName may be empty even when the
// album ID is known.
if (expandedAlbumId !== null) { if (expandedAlbumId !== null) {
const album = this.albumById.get( const album = this.albumById.get(
expandedAlbumId, expandedAlbumId,
@@ -333,6 +335,8 @@ export class AlbumSelectionManager {
} }
} }
if (!albumName) return null;
// Fallback: name-based search across all albums. // Fallback: name-based search across all albums.
for (const album of this.albumById.values()) { for (const album of this.albumById.values()) {
if ( if (
@@ -243,17 +243,19 @@ const gridStyles = css`
color: var(--yj-text-secondary, #b3b3b3); color: var(--yj-text-secondary, #b3b3b3);
} }
.sort-toolbar {
position: relative;
}
.search-indicator { .search-indicator {
position: absolute; position: absolute;
top: 8px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 5;
pointer-events: none; pointer-events: none;
background: var(--yj-bg-overlay, #495057); background: var(--yj-bg-overlay, #495057);
color: var(--yj-text-secondary, #b3b3b3); color: var(--yj-text-secondary, #b3b3b3);
font-size: var(--yj-text-sm); font-size: var(--yj-text-sm);
padding: 4px 14px; padding: 2px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid border: 1px solid
var(--yj-border-subtle, #555); var(--yj-border-subtle, #555);
@@ -1613,6 +1613,12 @@ export class CoverGrid
name=${dirIcon} name=${dirIcon}
></wa-icon> ></wa-icon>
</button> </button>
${this.searchCtrl.term
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
</div> </div>
${this.renderSortDropdownPopup()} ${this.renderSortDropdownPopup()}
`; `;
@@ -1812,6 +1818,7 @@ export class CoverGrid
if (this.cachedFilteredAlbums.length === 0) { if (this.cachedFilteredAlbums.length === 0) {
return html` return html`
${this.renderSortToolbar()}
<div class="empty-state"> <div class="empty-state">
<p>No albums match your search.</p> <p>No albums match your search.</p>
</div> </div>
@@ -1824,12 +1831,6 @@ export class CoverGrid
return html` return html`
${this.renderSortToolbar()} ${this.renderSortToolbar()}
${this.searchCtrl.term
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
<div <div
class="grid-scroll-container" class="grid-scroll-container"
@click=${this.onGridClick} @click=${this.onGridClick}
@@ -326,13 +326,23 @@ export class GenresView
line-height: 1.3; line-height: 1.3;
} }
.search-indicator { .search-bar-row {
position: absolute; position: relative;
top: 8px; display: flex;
left: 50%; align-items: center;
transform: translateX(-50%); justify-content: center;
z-index: 5; min-height: 30px;
pointer-events: none; border-bottom: 1px solid
var(--yj-border-subtle, #333);
flex-shrink: 0;
user-select: none;
}
.search-indicator {
position: absolute;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
background: var( background: var(
--yj-bg-overlay, --yj-bg-overlay,
#495057 #495057
@@ -342,7 +352,7 @@ export class GenresView
#b3b3b3 #b3b3b3
); );
font-size: 12px; font-size: 12px;
padding: 4px 14px; padding: 2px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid border: 1px solid
var(--yj-border-subtle, #555); var(--yj-border-subtle, #555);
@@ -1138,9 +1148,19 @@ export class GenresView
} }
const entries = this.cachedGridEntries; const entries = this.cachedGridEntries;
const searchBar = this.searchCtrl.term
? html`<div class="search-bar-row">
<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl
.term}&rdquo;
</div>
</div>`
: nothing;
if (entries.length === 0) { if (entries.length === 0) {
return html` return html`
${searchBar}
<div class="empty-message"> <div class="empty-message">
${this.searchCtrl.term ${this.searchCtrl.term
? 'No genres match your search.' ? 'No genres match your search.'
@@ -1150,15 +1170,7 @@ export class GenresView
} }
return html` return html`
${this.searchCtrl.term ${searchBar}
? html`<div
class="search-indicator"
>
Showing results for
&ldquo;${this.searchCtrl
.term}&rdquo;
</div>`
: nothing}
<div <div
class="grid-scroll-container" class="grid-scroll-container"
style=${this.restoringScroll style=${this.restoringScroll
@@ -745,12 +745,14 @@ export class PlaylistView
color: var(--yj-text-secondary, #b3b3b3); color: var(--yj-text-secondary, #b3b3b3);
} }
.sort-toolbar {
position: relative;
}
.search-indicator { .search-indicator {
position: absolute; position: absolute;
top: 8px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 5;
pointer-events: none; pointer-events: none;
background: var(--yj-bg-overlay, #495057); background: var(--yj-bg-overlay, #495057);
color: var( color: var(
@@ -758,7 +760,7 @@ export class PlaylistView
#b3b3b3 #b3b3b3
); );
font-size: 12px; font-size: 12px;
padding: 4px 14px; padding: 2px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid border: 1px solid
var(--yj-border-subtle, #555); var(--yj-border-subtle, #555);
@@ -2442,6 +2444,12 @@ export class PlaylistView
name=${dirIcon} name=${dirIcon}
></wa-icon> ></wa-icon>
</button> </button>
${this.searchCtrl.term
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
</div> </div>
${this.renderSortDropdownPopup()} ${this.renderSortDropdownPopup()}
`; `;
@@ -2532,15 +2540,6 @@ export class PlaylistView
${this.renderSortToolbar()} ${this.renderSortToolbar()}
${this.searchCtrl.term &&
this.filteredEntries.length > 0
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl
.term}&rdquo;
</div>`
: nothing}
${this.creating ${this.creating
? this.renderCreateForm() ? this.renderCreateForm()
: nothing} : nothing}
@@ -897,17 +897,19 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
background-color: var(--yj-text-tertiary, #6c757d); background-color: var(--yj-text-tertiary, #6c757d);
} }
.sort-toolbar {
position: relative;
}
.search-indicator { .search-indicator {
position: absolute; position: absolute;
top: 8px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 5;
pointer-events: none; pointer-events: none;
background: var(--yj-bg-overlay, #495057); background: var(--yj-bg-overlay, #495057);
color: var(--yj-text-secondary, #b3b3b3); color: var(--yj-text-secondary, #b3b3b3);
font-size: var(--yj-text-sm); font-size: var(--yj-text-sm);
padding: 4px 14px; padding: 2px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid var(--yj-border-subtle, #555); border: 1px solid var(--yj-border-subtle, #555);
white-space: nowrap; white-space: nowrap;
@@ -1640,6 +1642,12 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
</button> </button>
` `
: nothing} : nothing}
${this.searchCtrl.term
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
</div> </div>
${this.renderSortDropdownPopup()} ${this.renderSortDropdownPopup()}
`; `;
@@ -1743,13 +1751,6 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
></lit-virtualizer> ></lit-virtualizer>
`} `}
${this.searchCtrl.term && visibleTracks.length > 0
? html`<div class="search-indicator">
Showing results for
&ldquo;${this.searchCtrl.term}&rdquo;
</div>`
: nothing}
<div class="resize-overlay"> <div class="resize-overlay">
${this.colBoundaryPositions.map( ${this.colBoundaryPositions.map(
(pos, i) => html` (pos, i) => html`
+37
View File
@@ -66,6 +66,22 @@ export namespace library {
} }
} }
export class ScanWarning {
filePath: string;
phase: string;
err: any;
static createFrom(source: any = {}) {
return new ScanWarning(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.filePath = source["filePath"];
this.phase = source["phase"];
this.err = source["err"];
}
}
export class ScanMetrics { export class ScanMetrics {
total: number; total: number;
loadExisting: number; loadExisting: number;
@@ -92,6 +108,7 @@ export namespace library {
updated: number; updated: number;
skipped: number; skipped: number;
removed: number; removed: number;
warnings: ScanWarning[];
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new ScanMetrics(source); return new ScanMetrics(source);
@@ -124,8 +141,28 @@ export namespace library {
this.updated = source["updated"]; this.updated = source["updated"];
this.skipped = source["skipped"]; this.skipped = source["skipped"];
this.removed = source["removed"]; this.removed = source["removed"];
this.warnings = this.convertValues(source["warnings"], ScanWarning);
} }
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
} }
export class Track { export class Track {
TrackName: string; TrackName: string;
ArtistName: string; ArtistName: string;