scroll restore on artists and genres pages
This commit is contained in:
@@ -6,6 +6,10 @@ import {
|
|||||||
} from 'lit/decorators.js';
|
} from 'lit/decorators.js';
|
||||||
import { EventsOn } from '@runtime/runtime';
|
import { EventsOn } from '@runtime/runtime';
|
||||||
import '@lit-labs/virtualizer';
|
import '@lit-labs/virtualizer';
|
||||||
|
import type {
|
||||||
|
LitVirtualizer,
|
||||||
|
VisibilityChangedEvent,
|
||||||
|
} from '@lit-labs/virtualizer';
|
||||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||||
import {
|
import {
|
||||||
GetAlbumsByArtist,
|
GetAlbumsByArtist,
|
||||||
@@ -33,6 +37,9 @@ const CARD_SIZE_MIN = 100;
|
|||||||
const CARD_SIZE_MAX = 350;
|
const CARD_SIZE_MAX = 350;
|
||||||
const CARD_SIZE_DEFAULT = 176;
|
const CARD_SIZE_DEFAULT = 176;
|
||||||
|
|
||||||
|
/** Debounce delay for saving scroll position. */
|
||||||
|
const SCROLL_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid entry for the virtualized artist grid.
|
* Grid entry for the virtualized artist grid.
|
||||||
*/
|
*/
|
||||||
@@ -48,6 +55,9 @@ export class ArtistsView extends LitElement {
|
|||||||
private cancelScanComplete?: () => void;
|
private cancelScanComplete?: () => void;
|
||||||
private wheelListenerAttached = false;
|
private wheelListenerAttached = false;
|
||||||
private lastSearchTerm = '';
|
private lastSearchTerm = '';
|
||||||
|
private scrollDebounceTimer: ReturnType<
|
||||||
|
typeof setTimeout
|
||||||
|
> | null = null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private artists: library.Artist[] = [];
|
private artists: library.Artist[] = [];
|
||||||
@@ -55,6 +65,9 @@ export class ArtistsView extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private loading = true;
|
private loading = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private restoringScroll = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private cardSize: number = CARD_SIZE_DEFAULT;
|
private cardSize: number = CARD_SIZE_DEFAULT;
|
||||||
|
|
||||||
@@ -398,6 +411,11 @@ export class ArtistsView extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.cancelScanComplete?.();
|
this.cancelScanComplete?.();
|
||||||
this.detachWheelListener();
|
this.detachWheelListener();
|
||||||
|
|
||||||
|
if (this.scrollDebounceTimer !== null) {
|
||||||
|
clearTimeout(this.scrollDebounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
'click',
|
'click',
|
||||||
this.closeHandler,
|
this.closeHandler,
|
||||||
@@ -445,8 +463,85 @@ export class ArtistsView extends LitElement {
|
|||||||
);
|
);
|
||||||
this.artists = [];
|
this.artists = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
const saved =
|
||||||
|
this.libraryCtrl.getScrollPosition(
|
||||||
|
'artists',
|
||||||
|
);
|
||||||
|
|
||||||
|
this.restoringScroll = saved > 0;
|
||||||
this.loading = false;
|
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}
|
: nothing}
|
||||||
<div
|
<div
|
||||||
class="grid-scroll-container"
|
class="grid-scroll-container"
|
||||||
|
style=${this.restoringScroll
|
||||||
|
? 'visibility: hidden'
|
||||||
|
: ''}
|
||||||
@click=${this.onGridClick}
|
@click=${this.onGridClick}
|
||||||
>
|
>
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
@@ -1193,6 +1291,7 @@ export class ArtistsView extends LitElement {
|
|||||||
entry,
|
entry,
|
||||||
)}
|
)}
|
||||||
.layout=${this.gridLayout}
|
.layout=${this.gridLayout}
|
||||||
|
@visibilityChanged=${this.onVisibilityChanged}
|
||||||
></lit-virtualizer>
|
></lit-virtualizer>
|
||||||
</div>
|
</div>
|
||||||
${this.renderContextMenu()}
|
${this.renderContextMenu()}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import {
|
|||||||
} from 'lit/decorators.js';
|
} from 'lit/decorators.js';
|
||||||
import { EventsOn } from '@runtime/runtime';
|
import { EventsOn } from '@runtime/runtime';
|
||||||
import '@lit-labs/virtualizer';
|
import '@lit-labs/virtualizer';
|
||||||
|
import type {
|
||||||
|
LitVirtualizer,
|
||||||
|
VisibilityChangedEvent,
|
||||||
|
} from '@lit-labs/virtualizer';
|
||||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||||
import { library } from '@go/models';
|
import { library } from '@go/models';
|
||||||
import { LibraryController } from '@store/controllers/library-controller';
|
import { LibraryController } from '@store/controllers/library-controller';
|
||||||
@@ -29,6 +33,9 @@ const CARD_SIZE_MIN = 100;
|
|||||||
const CARD_SIZE_MAX = 350;
|
const CARD_SIZE_MAX = 350;
|
||||||
const CARD_SIZE_DEFAULT = 176;
|
const CARD_SIZE_DEFAULT = 176;
|
||||||
|
|
||||||
|
/** Debounce delay for saving scroll position. */
|
||||||
|
const SCROLL_DEBOUNCE_MS = 100;
|
||||||
|
|
||||||
/** A genre extracted from the track library. */
|
/** A genre extracted from the track library. */
|
||||||
interface Genre {
|
interface Genre {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -48,6 +55,9 @@ export class GenresView extends LitElement {
|
|||||||
private cancelScanComplete?: () => void;
|
private cancelScanComplete?: () => void;
|
||||||
private wheelListenerAttached = false;
|
private wheelListenerAttached = false;
|
||||||
private lastSearchTerm = '';
|
private lastSearchTerm = '';
|
||||||
|
private scrollDebounceTimer: ReturnType<
|
||||||
|
typeof setTimeout
|
||||||
|
> | null = null;
|
||||||
|
|
||||||
/** All tracks from the library (used to derive genres). */
|
/** All tracks from the library (used to derive genres). */
|
||||||
private allTracks: library.Track[] = [];
|
private allTracks: library.Track[] = [];
|
||||||
@@ -58,6 +68,9 @@ export class GenresView extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
private loading = true;
|
private loading = true;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private restoringScroll = false;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private cardSize: number = CARD_SIZE_DEFAULT;
|
private cardSize: number = CARD_SIZE_DEFAULT;
|
||||||
|
|
||||||
@@ -404,6 +417,11 @@ export class GenresView extends LitElement {
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.cancelScanComplete?.();
|
this.cancelScanComplete?.();
|
||||||
this.detachWheelListener();
|
this.detachWheelListener();
|
||||||
|
|
||||||
|
if (this.scrollDebounceTimer !== null) {
|
||||||
|
clearTimeout(this.scrollDebounceTimer);
|
||||||
|
}
|
||||||
|
|
||||||
document.removeEventListener(
|
document.removeEventListener(
|
||||||
'click',
|
'click',
|
||||||
this.closeHandler,
|
this.closeHandler,
|
||||||
@@ -454,8 +472,17 @@ export class GenresView extends LitElement {
|
|||||||
this.allTracks = [];
|
this.allTracks = [];
|
||||||
this.genres = [];
|
this.genres = [];
|
||||||
} finally {
|
} finally {
|
||||||
|
const saved =
|
||||||
|
this.libraryCtrl.getScrollPosition(
|
||||||
|
'genres',
|
||||||
|
);
|
||||||
|
|
||||||
|
this.restoringScroll = saved > 0;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.updateComplete;
|
||||||
|
this.restoreScrollPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -493,6 +520,74 @@ export class GenresView extends LitElement {
|
|||||||
return result;
|
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)
|
* Card size (zoom)
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
@@ -1187,6 +1282,9 @@ export class GenresView extends LitElement {
|
|||||||
: nothing}
|
: nothing}
|
||||||
<div
|
<div
|
||||||
class="grid-scroll-container"
|
class="grid-scroll-container"
|
||||||
|
style=${this.restoringScroll
|
||||||
|
? 'visibility: hidden'
|
||||||
|
: ''}
|
||||||
@click=${this.onGridClick}
|
@click=${this.onGridClick}
|
||||||
>
|
>
|
||||||
<lit-virtualizer
|
<lit-virtualizer
|
||||||
@@ -1196,6 +1294,7 @@ export class GenresView extends LitElement {
|
|||||||
) =>
|
) =>
|
||||||
this.renderGenreCard(entry)}
|
this.renderGenreCard(entry)}
|
||||||
.layout=${this.gridLayout}
|
.layout=${this.gridLayout}
|
||||||
|
@visibilityChanged=${this.onVisibilityChanged}
|
||||||
></lit-virtualizer>
|
></lit-virtualizer>
|
||||||
</div>
|
</div>
|
||||||
${this.renderContextMenu()}
|
${this.renderContextMenu()}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|||||||
import type { library } from '@go/models';
|
import type { library } from '@go/models';
|
||||||
import { libraryStore } from '../library-store';
|
import { libraryStore } from '../library-store';
|
||||||
|
|
||||||
type ViewName = 'tracks' | 'albums';
|
type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LibraryController connects a Lit component to the LibraryStore.
|
* LibraryController connects a Lit component to the LibraryStore.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import type { library } from '@go/models';
|
import type { library } from '@go/models';
|
||||||
import { Events } from '../events';
|
import { Events } from '../events';
|
||||||
|
|
||||||
type ViewName = 'tracks' | 'albums';
|
type ViewName = 'tracks' | 'albums' | 'artists' | 'genres';
|
||||||
|
|
||||||
type Subscriber = () => void;
|
type Subscriber = () => void;
|
||||||
|
|
||||||
@@ -38,6 +38,8 @@ class LibraryStore {
|
|||||||
private scrollPositions: Record<ViewName, number> = {
|
private scrollPositions: Record<ViewName, number> = {
|
||||||
tracks: 0,
|
tracks: 0,
|
||||||
albums: 0,
|
albums: 0,
|
||||||
|
artists: 0,
|
||||||
|
genres: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
private subscribers = new Set<Subscriber>();
|
private subscribers = new Set<Subscriber>();
|
||||||
@@ -247,7 +249,7 @@ class LibraryStore {
|
|||||||
this.tracks = null;
|
this.tracks = null;
|
||||||
this.albums = null;
|
this.albums = null;
|
||||||
this.artists = null;
|
this.artists = null;
|
||||||
this.scrollPositions = { tracks: 0, albums: 0 };
|
this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 };
|
||||||
this.notify();
|
this.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user