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:
@@ -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
|
||||||
|
“${this.searchCtrl
|
||||||
|
.term}”
|
||||||
|
</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
|
|
||||||
“${this.searchCtrl
|
|
||||||
.term}”
|
|
||||||
</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
|
||||||
|
“${this.searchCtrl.term}”
|
||||||
|
</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
|
|
||||||
“${this.searchCtrl.term}”
|
|
||||||
</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
|
||||||
|
“${this.searchCtrl
|
||||||
|
.term}”
|
||||||
|
</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
|
|
||||||
“${this.searchCtrl
|
|
||||||
.term}”
|
|
||||||
</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
|
||||||
|
“${this.searchCtrl.term}”
|
||||||
|
</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
|
|
||||||
“${this.searchCtrl
|
|
||||||
.term}”
|
|
||||||
</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
|
||||||
|
“${this.searchCtrl.term}”
|
||||||
|
</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
|
|
||||||
“${this.searchCtrl.term}”
|
|
||||||
</div>`
|
|
||||||
: nothing}
|
|
||||||
|
|
||||||
<div class="resize-overlay">
|
<div class="resize-overlay">
|
||||||
${this.colBoundaryPositions.map(
|
${this.colBoundaryPositions.map(
|
||||||
(pos, i) => html`
|
(pos, i) => html`
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user