diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts
index d44c9b1..a3cfd24 100644
--- a/frontend/src/components/artists-view/artists-view.ts
+++ b/frontend/src/components/artists-view/artists-view.ts
@@ -6,6 +6,10 @@ import {
} from 'lit/decorators.js';
import { EventsOn } from '@runtime/runtime';
import '@lit-labs/virtualizer';
+import type {
+ LitVirtualizer,
+ VisibilityChangedEvent,
+} from '@lit-labs/virtualizer';
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
import {
GetAlbumsByArtist,
@@ -33,6 +37,9 @@ const CARD_SIZE_MIN = 100;
const CARD_SIZE_MAX = 350;
const CARD_SIZE_DEFAULT = 176;
+/** Debounce delay for saving scroll position. */
+const SCROLL_DEBOUNCE_MS = 100;
+
/**
* Grid entry for the virtualized artist grid.
*/
@@ -48,6 +55,9 @@ export class ArtistsView extends LitElement {
private cancelScanComplete?: () => void;
private wheelListenerAttached = false;
private lastSearchTerm = '';
+ private scrollDebounceTimer: ReturnType<
+ typeof setTimeout
+ > | null = null;
@state()
private artists: library.Artist[] = [];
@@ -55,6 +65,9 @@ export class ArtistsView extends LitElement {
@state()
private loading = true;
+ @state()
+ private restoringScroll = false;
+
@state()
private cardSize: number = CARD_SIZE_DEFAULT;
@@ -398,6 +411,11 @@ export class ArtistsView extends LitElement {
super.disconnectedCallback();
this.cancelScanComplete?.();
this.detachWheelListener();
+
+ if (this.scrollDebounceTimer !== null) {
+ clearTimeout(this.scrollDebounceTimer);
+ }
+
document.removeEventListener(
'click',
this.closeHandler,
@@ -445,8 +463,85 @@ export class ArtistsView extends LitElement {
);
this.artists = [];
} finally {
+ const saved =
+ this.libraryCtrl.getScrollPosition(
+ 'artists',
+ );
+
+ this.restoringScroll = saved > 0;
this.loading = false;
}
+
+ await this.updateComplete;
+ this.restoreScrollPosition();
+ }
+
+ /* ================================================================
+ * Scroll position persistence
+ * ================================================================ */
+
+ /**
+ * Save the first visible item index on scroll.
+ */
+ private onVisibilityChanged = (
+ e: VisibilityChangedEvent,
+ ) => {
+ if (this.restoringScroll) return;
+
+ if (this.scrollDebounceTimer !== null) {
+ clearTimeout(this.scrollDebounceTimer);
+ }
+
+ this.scrollDebounceTimer = setTimeout(
+ () => {
+ this.libraryCtrl.setScrollPosition(
+ 'artists',
+ e.first,
+ );
+ },
+ SCROLL_DEBOUNCE_MS,
+ );
+ };
+
+ /**
+ * Restore scroll position from the store.
+ */
+ private restoreScrollPosition(): void {
+ const saved =
+ this.libraryCtrl.getScrollPosition(
+ 'artists',
+ );
+
+ if (saved <= 0) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ const virt =
+ this.shadowRoot?.querySelector(
+ 'lit-virtualizer',
+ ) as LitVirtualizer | null;
+
+ if (!virt) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ const safeIndex = Math.min(
+ saved,
+ this.filteredArtists.length - 1,
+ );
+
+ if (safeIndex <= 0) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ virt.scrollToIndex(safeIndex, 'start');
+ this.restoringScroll = false;
}
/* ================================================================
@@ -1182,6 +1277,9 @@ export class ArtistsView extends LitElement {
: nothing}
${this.renderContextMenu()}
diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts
index 7ec358e..109dbe1 100644
--- a/frontend/src/components/genres-view/genres-view.ts
+++ b/frontend/src/components/genres-view/genres-view.ts
@@ -6,6 +6,10 @@ import {
} from 'lit/decorators.js';
import { EventsOn } from '@runtime/runtime';
import '@lit-labs/virtualizer';
+import type {
+ LitVirtualizer,
+ VisibilityChangedEvent,
+} from '@lit-labs/virtualizer';
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
import { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
@@ -29,6 +33,9 @@ const CARD_SIZE_MIN = 100;
const CARD_SIZE_MAX = 350;
const CARD_SIZE_DEFAULT = 176;
+/** Debounce delay for saving scroll position. */
+const SCROLL_DEBOUNCE_MS = 100;
+
/** A genre extracted from the track library. */
interface Genre {
name: string;
@@ -48,6 +55,9 @@ export class GenresView extends LitElement {
private cancelScanComplete?: () => void;
private wheelListenerAttached = false;
private lastSearchTerm = '';
+ private scrollDebounceTimer: ReturnType<
+ typeof setTimeout
+ > | null = null;
/** All tracks from the library (used to derive genres). */
private allTracks: library.Track[] = [];
@@ -58,6 +68,9 @@ export class GenresView extends LitElement {
@state()
private loading = true;
+ @state()
+ private restoringScroll = false;
+
@state()
private cardSize: number = CARD_SIZE_DEFAULT;
@@ -404,6 +417,11 @@ export class GenresView extends LitElement {
super.disconnectedCallback();
this.cancelScanComplete?.();
this.detachWheelListener();
+
+ if (this.scrollDebounceTimer !== null) {
+ clearTimeout(this.scrollDebounceTimer);
+ }
+
document.removeEventListener(
'click',
this.closeHandler,
@@ -454,8 +472,17 @@ export class GenresView extends LitElement {
this.allTracks = [];
this.genres = [];
} finally {
+ const saved =
+ this.libraryCtrl.getScrollPosition(
+ 'genres',
+ );
+
+ this.restoringScroll = saved > 0;
this.loading = false;
}
+
+ await this.updateComplete;
+ this.restoreScrollPosition();
}
/**
@@ -493,6 +520,74 @@ export class GenresView extends LitElement {
return result;
}
+ /* ================================================================
+ * Scroll position persistence
+ * ================================================================ */
+
+ /**
+ * Save the first visible item index on scroll.
+ */
+ private onVisibilityChanged = (
+ e: VisibilityChangedEvent,
+ ) => {
+ if (this.restoringScroll) return;
+
+ if (this.scrollDebounceTimer !== null) {
+ clearTimeout(this.scrollDebounceTimer);
+ }
+
+ this.scrollDebounceTimer = setTimeout(
+ () => {
+ this.libraryCtrl.setScrollPosition(
+ 'genres',
+ e.first,
+ );
+ },
+ SCROLL_DEBOUNCE_MS,
+ );
+ };
+
+ /**
+ * Restore scroll position from the store.
+ */
+ private restoreScrollPosition(): void {
+ const saved =
+ this.libraryCtrl.getScrollPosition(
+ 'genres',
+ );
+
+ if (saved <= 0) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ const virt =
+ this.shadowRoot?.querySelector(
+ 'lit-virtualizer',
+ ) as LitVirtualizer | null;
+
+ if (!virt) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ const safeIndex = Math.min(
+ saved,
+ this.filteredGenres.length - 1,
+ );
+
+ if (safeIndex <= 0) {
+ this.restoringScroll = false;
+
+ return;
+ }
+
+ virt.scrollToIndex(safeIndex, 'start');
+ this.restoringScroll = false;
+ }
+
/* ================================================================
* Card size (zoom)
* ================================================================ */
@@ -1187,6 +1282,9 @@ export class GenresView extends LitElement {
: nothing}
this.renderGenreCard(entry)}
.layout=${this.gridLayout}
+ @visibilityChanged=${this.onVisibilityChanged}
>
${this.renderContextMenu()}
diff --git a/frontend/src/store/controllers/library-controller.ts b/frontend/src/store/controllers/library-controller.ts
index 5fa52bf..ea2a38f 100644
--- a/frontend/src/store/controllers/library-controller.ts
+++ b/frontend/src/store/controllers/library-controller.ts
@@ -2,7 +2,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
import type { library } from '@go/models';
import { libraryStore } from '../library-store';
-type ViewName = 'tracks' | 'albums';
+type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
/**
* LibraryController connects a Lit component to the LibraryStore.
diff --git a/frontend/src/store/library-store.ts b/frontend/src/store/library-store.ts
index 0ee7d7a..a888e82 100644
--- a/frontend/src/store/library-store.ts
+++ b/frontend/src/store/library-store.ts
@@ -8,7 +8,7 @@ import {
import type { library } from '@go/models';
import { Events } from '../events';
-type ViewName = 'tracks' | 'albums';
+type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
type Subscriber = () => void;
@@ -38,6 +38,8 @@ class LibraryStore {
private scrollPositions: Record = {
tracks: 0,
albums: 0,
+ artists: 0,
+ genres: 0,
};
private subscribers = new Set();
@@ -247,7 +249,7 @@ class LibraryStore {
this.tracks = null;
this.albums = null;
this.artists = null;
- this.scrollPositions = { tracks: 0, albums: 0 };
+ this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 };
this.notify();
}