${album.CoverArtPath
@@ -537,120 +1125,168 @@ export class CoverGrid extends LitElement {
src="${album.CoverArtThumbnailPath || album.CoverArtPath}"
alt="${album.Name} cover"
loading="lazy"
- @error=${(e: Event) => {
- const img = e.target as HTMLImageElement;
- if (img.src !== album.CoverArtPath) {
- img.src = album.CoverArtPath;
- }
- }}
/>`
- : html`
+ : html`
${this.getAlbumInitial(album.Name)}
`}
-
${album.Name}
-
- ${album.ArtistName}${album.Year ? ` - ${album.Year}` : ''}
+
+ ${album.Name}
+
+
+ ${album.ArtistName}${album.Year
+ ? ` - ${album.Year}`
+ : ''}
`;
+ }
+
+ /* ====================================================================
+ * Render: main grid with interleaved dropdown
+ *
+ * Builds a union array of GridItem entries and
+ * renders them via repeat() so Lit can diff by
+ * stable key rather than positional index.
+ * ==================================================================== */
+
+ private buildGridItems(): GridItem[] {
+ const columns = this.getColumnCount();
+
+ // Return cached result when inputs are unchanged.
+ if (
+ this.gridItemsCacheAlbums ===
+ this.albums &&
+ this.gridItemsCacheExpandedId ===
+ this.expandedAlbumId &&
+ this.gridItemsCacheColumns === columns
+ ) {
+ return this.gridItemsCache;
+ }
+
+ const expandedIndex =
+ this.expandedAlbumId !== null
+ ? this.albums.findIndex(
+ (a) =>
+ a.ID ===
+ this.expandedAlbumId,
+ )
+ : -1;
+
+ let dropdownAfterIndex = -1;
+
+ if (expandedIndex >= 0) {
+ const row = Math.floor(
+ expandedIndex / columns,
+ );
+ dropdownAfterIndex = Math.min(
+ (row + 1) * columns - 1,
+ this.albums.length - 1,
+ );
+ }
+
+ const items: GridItem[] = [];
+
+ for (let i = 0; i < this.albums.length; i++) {
+ const album = this.albums[i]!;
+ items.push({
+ kind: 'album',
+ key: `a-${album.ID}`,
+ album,
+ index: i,
+ });
+
+ if (i === dropdownAfterIndex) {
+ items.push({
+ kind: 'dropdown',
+ key: 'dropdown',
+ });
+ }
+ }
+
+ // Cache the result and inputs.
+ this.gridItemsCache = items;
+ this.gridItemsCacheAlbums = this.albums;
+ this.gridItemsCacheExpandedId =
+ this.expandedAlbumId;
+ this.gridItemsCacheColumns = columns;
+
+ return items;
+ }
+
+ private renderGridItem = (item: GridItem) => {
+ if (item.kind === 'dropdown') {
+ return html`
+
+ `;
+ }
+
+ return this.renderAlbumCard(
+ item.album,
+ item.index,
+ );
};
- private getAlbumInitial(name: string): string {
- return name.charAt(0).toUpperCase();
- }
-
- private onGridClick(e: MouseEvent) {
- const clickedCard = e.composedPath().some(
- (el) =>
- el instanceof HTMLElement &&
- el.classList.contains('album-card'),
- );
-
- if (!clickedCard) {
- this.selectedAlbums = new Set();
- this.lastSelectedIndex = null;
- }
- }
-
- private onAlbumClick(
- e: MouseEvent,
- album: library.Album,
- index: number,
- ) {
- const isCtrl = e.ctrlKey || e.metaKey;
- const isShift = e.shiftKey;
-
- if (isShift && this.lastSelectedIndex !== null) {
- const range = this.selectRange(
- this.lastSelectedIndex,
- index,
- );
- const next = new Set(this.selectedAlbums);
-
- for (const id of range) {
- next.add(id);
- }
-
- this.selectedAlbums = next;
- } else if (isCtrl) {
- const next = new Set(this.selectedAlbums);
-
- if (next.has(album.ID)) {
- next.delete(album.ID);
- } else {
- next.add(album.ID);
- }
-
- this.selectedAlbums = next;
- this.lastSelectedIndex = index;
- } else {
- this.selectedAlbums = new Set([album.ID]);
- this.lastSelectedIndex = index;
- }
- }
-
- private onAlbumKeydown(
- e: KeyboardEvent,
- album: library.Album,
- index: number,
- ) {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- this.selectedAlbums = new Set([album.ID]);
- this.lastSelectedIndex = index;
- }
- }
+ /* ====================================================================
+ * Render: main
+ * ==================================================================== */
override render() {
if (this.loading) {
- return html`
Loading albums...
`;
+ return html`
+ Loading albums...
+
`;
}
if (this.albums.length === 0) {
return html`
No albums found
-
Add music to your library to see album covers here.
+
+ Add music to your library to see
+ album covers here.
+
`;
}
return html`
-
this.onGridClick(e)}
- .layout=${grid({
- itemSize: { width: '176px', height: '230px' },
- gap: '16px',
- padding: '16px',
- })}
- >
+
@@ -702,12 +1363,13 @@ export class CoverGrid extends LitElement {
>
${this.playlistSubmenuOpen
? html`
-
e.stopPropagation()}
- >
- `
+
+ e.stopPropagation()}
+ >
+ `
: nothing}
`;
diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts
index 8e5a86a..6b76f4b 100644
--- a/frontend/src/components/track-list/track-list.ts
+++ b/frontend/src/components/track-list/track-list.ts
@@ -50,6 +50,7 @@ export class TrackList extends LitElement {
private virtualizer!: LitVirtualizer;
private lastSelectedIndex: number | null = null;
+ private lastActiveTrackPath: string | null = null;
private closeHandler = () => this.closeContextMenu();
@@ -289,7 +290,7 @@ export class TrackList extends LitElement {
background-color: rgba(255, 212, 59, 0.1);
}
- .track-row.active .track-name {
+ .track-row.active {
color: #ffd43b;
}
@@ -392,6 +393,14 @@ export class TrackList extends LitElement {
if (changed.has('columnWidths')) {
this.virtualizer?.requestUpdate();
}
+
+ const currentPath =
+ this.player.currentTrack?.filePath ?? null;
+
+ if (currentPath !== this.lastActiveTrackPath) {
+ this.lastActiveTrackPath = currentPath;
+ this.virtualizer?.requestUpdate();
+ }
}
private previousHostWidth = 0;
diff --git a/frontend/src/pages/config/config.ts b/frontend/src/pages/config/config.ts
index 4c8f370..e4a611b 100644
--- a/frontend/src/pages/config/config.ts
+++ b/frontend/src/pages/config/config.ts
@@ -1,8 +1,42 @@
import 'htmx.org/dist/htmx.js'
import { DirectoryPicker } from '@go/frontendutil/FrontendUtil';
+import { Scan } from '@go/library/Library';
declare global {
- interface Window { DirectoryPicker: any; }
+ interface Window {
+ DirectoryPicker: typeof DirectoryPicker;
+ Scan: typeof Scan;
+ selectLibraryDirectory: (pElement: HTMLInputElement) => void;
+ scanLibrary: (button: HTMLButtonElement) => void;
+ }
}
-window.DirectoryPicker = DirectoryPicker
+window.DirectoryPicker = DirectoryPicker;
+window.Scan = Scan;
+
+window.selectLibraryDirectory = (pElement: HTMLInputElement) => {
+ window.DirectoryPicker()
+ .then((result) => {
+ if (result.length !== 0) {
+ pElement.value = result;
+ }
+ })
+ .catch((err: unknown) => {
+ console.error('error with directory picker: ' + err);
+ });
+};
+
+window.scanLibrary = (button: HTMLButtonElement) => {
+ button.disabled = true;
+ button.textContent = 'Scanning...';
+ window.Scan()
+ .then(() => {
+ button.textContent = 'Scan Library';
+ button.disabled = false;
+ })
+ .catch((err: unknown) => {
+ console.error('error scanning library: ' + err);
+ button.textContent = 'Scan Library';
+ button.disabled = false;
+ });
+};
diff --git a/frontend/src/pages/config/config.html b/frontend/src/pages/config/index.html
similarity index 100%
rename from frontend/src/pages/config/config.html
rename to frontend/src/pages/config/index.html
diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts
index 78acb66..4319cb7 100644
--- a/frontend/vite.config.mts
+++ b/frontend/vite.config.mts
@@ -17,7 +17,7 @@ export default defineConfig({
rollupOptions: {
input: {
main: "index.html",
- config: "src/pages/config/config.html",
+ config: "src/pages/config/index.html",
},
},
},
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index 2cf94f5..5942ea6 100755
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -27,6 +27,8 @@ export namespace library {
ArtistName: string;
TrackLength: string;
FilePath: string;
+ TrackNumber: number;
+ DiscNumber: number;
static createFrom(source: any = {}) {
return new Track(source);
@@ -38,6 +40,8 @@ export namespace library {
this.ArtistName = source["ArtistName"];
this.TrackLength = source["TrackLength"];
this.FilePath = source["FilePath"];
+ this.TrackNumber = source["TrackNumber"];
+ this.DiscNumber = source["DiscNumber"];
}
}
diff --git a/main.go b/main.go
index 0a4d910..77f83f4 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,7 @@ import (
"github.com/golang-cz/devslog"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
+ "github.com/wailsapp/wails/v2/pkg/options/linux"
"yellowjacket/backend"
"yellowjacket/backend/assets"
@@ -78,6 +79,9 @@ func main() {
MinHeight: 384,
MaxWidth: 0,
MaxHeight: 0,
+ Linux: &linux.Options{
+ WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
+ },
})
if err != nil {
sLogger.Error("application error", "err", err.Error())