Fix/explore art scanner requests #21
@@ -10,6 +10,7 @@ import type {
|
||||
VisibilityChangedEvent,
|
||||
} from '@lit-labs/virtualizer';
|
||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import { gridSpacingFor } from '@utils/grid-spacing';
|
||||
import {
|
||||
GetAlbumsByArtist,
|
||||
GetFilePathsByAlbums,
|
||||
@@ -147,8 +148,6 @@ export class ArtistsView
|
||||
|
||||
// ----- Grid spacing constants -----
|
||||
|
||||
private static readonly GRID_GAP = 8;
|
||||
private static readonly GRID_PADDING = 8;
|
||||
private static readonly CARD_PADDING = 5;
|
||||
|
||||
private get imageSize(): number {
|
||||
@@ -177,20 +176,41 @@ export class ArtistsView
|
||||
private createGridLayout() {
|
||||
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
||||
const h = w + this.cardTextHeight;
|
||||
const gap = ArtistsView.GRID_GAP;
|
||||
const pad = ArtistsView.GRID_PADDING;
|
||||
|
||||
// One number for the gap, the row gap and the padding: whatever
|
||||
// a row could not spend on another card, shared out equally, so
|
||||
// the outside is never wider than the inside. See
|
||||
// `utils/grid-spacing.ts`.
|
||||
const spacing = this.spacingFor(this.containerWidth);
|
||||
|
||||
this.lastLayoutSpacing = spacing;
|
||||
|
||||
return grid({
|
||||
itemSize: {
|
||||
width: `${w}px`,
|
||||
height: `${h}px`,
|
||||
},
|
||||
gap: `${gap}px`,
|
||||
padding: `${pad}px`,
|
||||
justify: 'center',
|
||||
gap: `${spacing}px`,
|
||||
padding: `${spacing}px`,
|
||||
justify: 'start',
|
||||
});
|
||||
}
|
||||
|
||||
/** The width the grid lays itself out in. */
|
||||
private get containerWidth(): number {
|
||||
return (
|
||||
this.renderRoot?.querySelector<HTMLElement>(
|
||||
'.grid-scroll-container',
|
||||
)?.clientWidth ||
|
||||
this.clientWidth ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private spacingFor(width: number): number {
|
||||
return gridSpacingFor(width, this.cardSize);
|
||||
}
|
||||
|
||||
/** Sort direction for the artist grid.
|
||||
*
|
||||
* There is only one key to sort by: `library.Artist` carries a
|
||||
@@ -478,6 +498,8 @@ export class ArtistsView
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.detachWheelListener();
|
||||
this.gridResizeObserver?.disconnect();
|
||||
this.gridResizeObserver = null;
|
||||
}
|
||||
|
||||
/** The wheel listener and the scroll debounce belong to the grid
|
||||
@@ -730,10 +752,34 @@ export class ArtistsView
|
||||
* ================================================================ */
|
||||
|
||||
private lastLayoutWidth = 0;
|
||||
private lastLayoutSpacing = 0;
|
||||
|
||||
/** Watches the scroller so a window resize rebuilds the layout:
|
||||
* the spacing is derived from its width, and nothing else asks
|
||||
* this view to update when only that changes. */
|
||||
private gridResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
private observeGridWidth() {
|
||||
const container =
|
||||
this.renderRoot?.querySelector<HTMLElement>(
|
||||
'.grid-scroll-container',
|
||||
);
|
||||
|
||||
if (!container || this.gridResizeObserver) return;
|
||||
|
||||
this.gridResizeObserver = new ResizeObserver(() =>
|
||||
this.requestUpdate(),
|
||||
);
|
||||
this.gridResizeObserver.observe(container);
|
||||
}
|
||||
|
||||
private updateGridLayout() {
|
||||
this.observeGridWidth();
|
||||
|
||||
if (
|
||||
this.cardSize === this.lastLayoutWidth
|
||||
this.cardSize === this.lastLayoutWidth &&
|
||||
this.lastLayoutSpacing ===
|
||||
this.spacingFor(this.containerWidth)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { LibraryController } from '@store/controllers/library-controller';
|
||||
import { SearchController } from '@store/controllers/search-controller';
|
||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||
import { RovingGridController } from '@utils/roving-grid';
|
||||
import { gridColumnsFor, gridSpacingFor } from '@utils/grid-spacing';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
import type { QueueSource } from '@store/queue-store';
|
||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||
@@ -97,19 +98,36 @@ export class CoverGrid
|
||||
private lastAlbumsRef: library.Album[] | null =
|
||||
null;
|
||||
|
||||
// Fixed grid spacing constants.
|
||||
private static readonly GRID_GAP = 8;
|
||||
private static readonly GRID_PADDING = 8;
|
||||
private static readonly CARD_PADDING = 5;
|
||||
|
||||
private ctxMenu = new ContextMenuController(this);
|
||||
private favCtrl = new FavoritesController(this);
|
||||
private selMgr = new AlbumSelectionManager();
|
||||
private scrollMgr = new ScrollManager(this, {
|
||||
GRID_GAP: CoverGrid.GRID_GAP,
|
||||
GRID_PADDING: CoverGrid.GRID_PADDING,
|
||||
columnsFor: (width: number) => this.columnsFor(width),
|
||||
spacingFor: (width: number) => this.spacingFor(width),
|
||||
});
|
||||
|
||||
/**
|
||||
* How many cards fit across `width`, by the same arithmetic the
|
||||
* virtualizer's `space-evenly` grid uses — no gap and no padding
|
||||
* are reserved, because both come out of what is left over.
|
||||
*
|
||||
* The scroll manager restores a position by rebuilding the grid's
|
||||
* geometry, so this and `spacingFor` must agree with the layout
|
||||
* rather than approximate it; they were two constants that no
|
||||
* longer describe anything once the spacing became elastic.
|
||||
*/
|
||||
columnsFor(width: number): number {
|
||||
return gridColumnsFor(width, this.cardWidth);
|
||||
}
|
||||
|
||||
/** The spacing that width produces: between columns, between rows,
|
||||
* and around the outside, all the same number. */
|
||||
spacingFor(width: number): number {
|
||||
return gridSpacingFor(width, this.cardWidth);
|
||||
}
|
||||
|
||||
private lastSelectedAlbumIndex: number | null = null;
|
||||
private lastSelectedTrackIndex: number | null = null;
|
||||
|
||||
@@ -148,10 +166,30 @@ export class CoverGrid
|
||||
}
|
||||
|
||||
// Virtualizer grid layout instance — recreated when
|
||||
// the card size changes.
|
||||
// the card size or the container width changes.
|
||||
private gridLayout = this.createGridLayout();
|
||||
private gridLayoutWidth = 0;
|
||||
|
||||
/** The spacing the current layouts were built with. */
|
||||
private gridLayoutSpacing = 0;
|
||||
|
||||
/** Watches the scroll container so a window resize rebuilds the
|
||||
* layout: the spacing is derived from its width, and nothing else
|
||||
* asks this component to update when only that changes. */
|
||||
private gridResizeObserver: ResizeObserver | null =
|
||||
null;
|
||||
|
||||
private observeGridWidth(): void {
|
||||
const container = this.scrollContainer;
|
||||
|
||||
if (!container || this.gridResizeObserver) return;
|
||||
|
||||
this.gridResizeObserver = new ResizeObserver(
|
||||
() => this.requestUpdate(),
|
||||
);
|
||||
this.gridResizeObserver.observe(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondary layout for the "after" virtualizer in
|
||||
* split mode. Uses zero top padding so there is no
|
||||
@@ -169,22 +207,49 @@ export class CoverGrid
|
||||
}
|
||||
|
||||
const h = w + this.cardTextHeight;
|
||||
const gap = CoverGrid.GRID_GAP;
|
||||
const pad = CoverGrid.GRID_PADDING;
|
||||
|
||||
// The spacing is whatever the row could not spend on another
|
||||
// card, shared out equally — so it is the same number between
|
||||
// two cards, between two rows, and down each outside edge.
|
||||
// See `utils/grid-spacing.ts` for why it is computed rather
|
||||
// than handed to the virtualizer as `space-evenly`.
|
||||
const spacing = this.spacingFor(
|
||||
this.containerWidth,
|
||||
);
|
||||
|
||||
if (!noTopPad) {
|
||||
this.gridLayoutSpacing = spacing;
|
||||
}
|
||||
|
||||
return grid({
|
||||
itemSize: {
|
||||
width: `${w}px`,
|
||||
height: `${h}px`,
|
||||
},
|
||||
gap: `${gap}px`,
|
||||
gap: `${spacing}px`,
|
||||
padding: noTopPad
|
||||
? `0 ${pad}px ${pad}px`
|
||||
: `${pad}px`,
|
||||
justify: 'center',
|
||||
? `0 ${spacing}px ${spacing}px`
|
||||
: `${spacing}px`,
|
||||
justify: 'start',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The width the grid lays itself out in.
|
||||
*
|
||||
* Read from the scroll container when there is one; before the
|
||||
* first render there is not, and the fallback only has to be
|
||||
* plausible — the layout is rebuilt from the real width as soon as
|
||||
* one exists.
|
||||
*/
|
||||
private get containerWidth(): number {
|
||||
return (
|
||||
this.scrollContainer?.clientWidth ||
|
||||
this.clientWidth ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private dragImageEl: HTMLElement | null = null;
|
||||
|
||||
// -- Memoisation caches for filtered albums --
|
||||
@@ -466,6 +531,9 @@ export class CoverGrid
|
||||
);
|
||||
this.wheelListenerAttached = false;
|
||||
|
||||
this.gridResizeObserver?.disconnect();
|
||||
this.gridResizeObserver = null;
|
||||
|
||||
this.scrollMgr.teardown();
|
||||
this.scrollMgr.revealContainer(
|
||||
this.scrollContainer,
|
||||
@@ -603,10 +671,18 @@ export class CoverGrid
|
||||
this.wheelListenerAttached = true;
|
||||
}
|
||||
|
||||
// Recreate the virtualizer grid layout when
|
||||
// the card size changes.
|
||||
this.observeGridWidth();
|
||||
|
||||
// Recreate the virtualizer grid layout when the card size
|
||||
// changes — or when the spacing the container width produces
|
||||
// does, since that is now a derived number rather than a
|
||||
// constant. Keyed on the spacing rather than on the width, or
|
||||
// every pixel of a drag rebuilds a layout that would come out
|
||||
// the same.
|
||||
const cardSizeChanged =
|
||||
this.gridLayoutWidth !== this.cardWidth;
|
||||
this.gridLayoutWidth !== this.cardWidth ||
|
||||
this.gridLayoutSpacing !==
|
||||
this.spacingFor(this.containerWidth);
|
||||
|
||||
if (cardSizeChanged) {
|
||||
this.gridLayout = this.createGridLayout();
|
||||
|
||||
@@ -6,12 +6,21 @@ import type { LibraryController } from '@store/controllers/library-controller';
|
||||
import type { GridEntry } from './cover-grid-types.js';
|
||||
|
||||
/**
|
||||
* Grid spacing constants shared between the scroll
|
||||
* manager and the host component.
|
||||
* Grid geometry, asked of the host rather than written down.
|
||||
*
|
||||
* These were two constants, `GRID_GAP` and `GRID_PADDING`, which stopped
|
||||
* describing anything the moment the grid's spacing became elastic: the
|
||||
* gap, the padding and the column count are all derived from the
|
||||
* container width now, and a scroll position rebuilt from a stale 8px
|
||||
* lands in the wrong row.
|
||||
*/
|
||||
export interface GridConstants {
|
||||
readonly GRID_GAP: number;
|
||||
readonly GRID_PADDING: number;
|
||||
/** Columns that fit across `width`. */
|
||||
columnsFor(width: number): number;
|
||||
|
||||
/** The spacing `width` produces — between columns, between rows,
|
||||
* and around the outside, all the same number. */
|
||||
spacingFor(width: number): number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -275,8 +284,8 @@ export class ScrollManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const gap = this.spacing(container);
|
||||
const pad = gap;
|
||||
const rowStep =
|
||||
this.host.cardHeight + gap;
|
||||
|
||||
@@ -293,7 +302,7 @@ export class ScrollManager {
|
||||
() => {
|
||||
const rowStep =
|
||||
this.host.cardHeight +
|
||||
this.gc.GRID_GAP;
|
||||
this.spacing(container);
|
||||
|
||||
if (this.pendingFocus === null) {
|
||||
this.isResizing = true;
|
||||
@@ -351,7 +360,7 @@ export class ScrollManager {
|
||||
container: HTMLElement,
|
||||
rowStep: number,
|
||||
): void {
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const pad = this.spacing(container);
|
||||
const cols = this.currentColumnCount;
|
||||
const filtered =
|
||||
this.host.cachedFilteredAlbums;
|
||||
@@ -410,17 +419,15 @@ export class ScrollManager {
|
||||
): number {
|
||||
if (!container) return 1;
|
||||
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const availableWidth =
|
||||
container.clientWidth - pad * 2;
|
||||
return this.gc.columnsFor(
|
||||
container.clientWidth,
|
||||
);
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
1,
|
||||
Math.floor(
|
||||
(availableWidth + gap) /
|
||||
(this.host.cardWidth + gap),
|
||||
),
|
||||
/** The grid's current spacing, which is also its padding. */
|
||||
private spacing(container?: HTMLElement): number {
|
||||
return this.gc.spacingFor(
|
||||
container?.clientWidth ?? 800,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -439,7 +446,7 @@ export class ScrollManager {
|
||||
container?: HTMLElement,
|
||||
): number {
|
||||
const cols = this.getColumnCount(container);
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const gap = this.spacing(container);
|
||||
|
||||
return (
|
||||
cols * this.host.cardWidth +
|
||||
@@ -460,7 +467,7 @@ export class ScrollManager {
|
||||
|
||||
const cols = this.getColumnCount(container);
|
||||
const colIndex = idx % cols;
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const gap = this.spacing(container);
|
||||
|
||||
return (
|
||||
colIndex *
|
||||
@@ -597,8 +604,8 @@ export class ScrollManager {
|
||||
|
||||
if (!this.host.splitMode) return raw;
|
||||
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const gap = this.spacing(container);
|
||||
const pad = gap;
|
||||
const columns =
|
||||
this.getColumnCount(container);
|
||||
const rowStep = this.host.cardHeight + gap;
|
||||
@@ -678,8 +685,8 @@ export class ScrollManager {
|
||||
|
||||
if (expandedIndex < 0) return;
|
||||
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const gap = this.spacing(container);
|
||||
const pad = gap;
|
||||
const columns =
|
||||
this.getColumnCount(container);
|
||||
const rowStep = this.host.cardHeight + gap;
|
||||
@@ -772,8 +779,8 @@ export class ScrollManager {
|
||||
|
||||
if (idx < 0) return;
|
||||
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad = this.gc.GRID_PADDING;
|
||||
const gap = this.spacing(container);
|
||||
const pad = gap;
|
||||
const cols =
|
||||
this.getColumnCount(container);
|
||||
const rowStep = this.host.cardHeight + gap;
|
||||
@@ -854,9 +861,8 @@ export class ScrollManager {
|
||||
this.getExpandedAlbumIndex();
|
||||
|
||||
if (idx >= 0) {
|
||||
const gap = this.gc.GRID_GAP;
|
||||
const pad =
|
||||
this.gc.GRID_PADDING;
|
||||
const gap = this.spacing(container);
|
||||
const pad = gap;
|
||||
const cols =
|
||||
this.getColumnCount(
|
||||
container,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
VisibilityChangedEvent,
|
||||
} from '@lit-labs/virtualizer';
|
||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||
import { gridSpacingFor } from '@utils/grid-spacing';
|
||||
import {
|
||||
GetFilePathsByGenres,
|
||||
} from '@go/library/library.js';
|
||||
@@ -155,8 +156,6 @@ export class GenresView
|
||||
|
||||
// ----- Grid spacing constants -----
|
||||
|
||||
private static readonly GRID_GAP = 8;
|
||||
private static readonly GRID_PADDING = 8;
|
||||
private static readonly CARD_PADDING = 5;
|
||||
|
||||
private get imageSize(): number {
|
||||
@@ -185,20 +184,41 @@ export class GenresView
|
||||
private createGridLayout() {
|
||||
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
||||
const h = w + this.cardTextHeight;
|
||||
const gap = GenresView.GRID_GAP;
|
||||
const pad = GenresView.GRID_PADDING;
|
||||
|
||||
// One number for the gap, the row gap and the padding: whatever
|
||||
// a row could not spend on another card, shared out equally, so
|
||||
// the outside is never wider than the inside. See
|
||||
// `utils/grid-spacing.ts`.
|
||||
const spacing = this.spacingFor(this.containerWidth);
|
||||
|
||||
this.lastLayoutSpacing = spacing;
|
||||
|
||||
return grid({
|
||||
itemSize: {
|
||||
width: `${w}px`,
|
||||
height: `${h}px`,
|
||||
},
|
||||
gap: `${gap}px`,
|
||||
padding: `${pad}px`,
|
||||
justify: 'center',
|
||||
gap: `${spacing}px`,
|
||||
padding: `${spacing}px`,
|
||||
justify: 'start',
|
||||
});
|
||||
}
|
||||
|
||||
/** The width the grid lays itself out in. */
|
||||
private get containerWidth(): number {
|
||||
return (
|
||||
this.renderRoot?.querySelector<HTMLElement>(
|
||||
'.grid-scroll-container',
|
||||
)?.clientWidth ||
|
||||
this.clientWidth ||
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
private spacingFor(width: number): number {
|
||||
return gridSpacingFor(width, this.cardSize);
|
||||
}
|
||||
|
||||
/** Sort key and direction for the genre grid (H-19: it had none). */
|
||||
@state()
|
||||
private sortField: 'name' | 'tracks' = 'name';
|
||||
@@ -483,6 +503,8 @@ export class GenresView
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.detachWheelListener();
|
||||
this.gridResizeObserver?.disconnect();
|
||||
this.gridResizeObserver = null;
|
||||
}
|
||||
|
||||
/** See artists-view: off-screen the grid cannot be scrolled, and
|
||||
@@ -737,10 +759,34 @@ export class GenresView
|
||||
* ================================================================ */
|
||||
|
||||
private lastLayoutWidth = 0;
|
||||
private lastLayoutSpacing = 0;
|
||||
|
||||
/** Watches the scroller so a window resize rebuilds the layout:
|
||||
* the spacing is derived from its width, and nothing else asks
|
||||
* this view to update when only that changes. */
|
||||
private gridResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
private observeGridWidth() {
|
||||
const container =
|
||||
this.renderRoot?.querySelector<HTMLElement>(
|
||||
'.grid-scroll-container',
|
||||
);
|
||||
|
||||
if (!container || this.gridResizeObserver) return;
|
||||
|
||||
this.gridResizeObserver = new ResizeObserver(() =>
|
||||
this.requestUpdate(),
|
||||
);
|
||||
this.gridResizeObserver.observe(container);
|
||||
}
|
||||
|
||||
private updateGridLayout() {
|
||||
this.observeGridWidth();
|
||||
|
||||
if (
|
||||
this.cardSize === this.lastLayoutWidth
|
||||
this.cardSize === this.lastLayoutWidth &&
|
||||
this.lastLayoutSpacing ===
|
||||
this.spacingFor(this.containerWidth)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Even spacing for the three card grids — albums, artists, genres.
|
||||
*
|
||||
* All three used `justify: 'center'` with a fixed 8px gap and 8px
|
||||
* padding, which gives the row a fixed width and pushes everything left
|
||||
* over to the two margins: on a 1440px window the albums grid drew its
|
||||
* cards 16px apart inside 78px of nothing down each side. The outside
|
||||
* was five times the inside.
|
||||
*
|
||||
* The fix is to spend the leftover on the spacing instead, so there is
|
||||
* one number: between two cards, between two rows, and down each edge.
|
||||
* The virtualizer has a word for that — `justify: 'space-evenly'` with
|
||||
* `gap: 'auto'` — and it cannot be used, because it fits
|
||||
* `floor(width / cardWidth)` columns without reserving the gap it is
|
||||
* about to need: a width one card short of exact fits seven cards a
|
||||
* pixel apart. Deciding the column count here is what puts a floor
|
||||
* under the spacing, and the grid is then given plain numbers.
|
||||
*/
|
||||
|
||||
/** The narrowest the spacing is allowed to get. */
|
||||
export const MIN_GRID_SPACING = 8;
|
||||
|
||||
/**
|
||||
* How many cards of `cardWidth` fit across `width`.
|
||||
*
|
||||
* A row of c cards spends c×cardWidth on cards and (c+1)×spacing on the
|
||||
* spaces between and beside them, so c is bounded by
|
||||
* (width − spacing) / (cardWidth + spacing) at the minimum spacing.
|
||||
*/
|
||||
export function gridColumnsFor(
|
||||
width: number,
|
||||
cardWidth: number,
|
||||
): number {
|
||||
if (cardWidth <= 0) return 1;
|
||||
|
||||
const fit = Math.floor(
|
||||
(width - MIN_GRID_SPACING) / (cardWidth + MIN_GRID_SPACING),
|
||||
);
|
||||
|
||||
return Math.max(1, fit);
|
||||
}
|
||||
|
||||
/**
|
||||
* The spacing `width` produces — the gap, the row gap and the padding,
|
||||
* which are all the same number.
|
||||
*/
|
||||
export function gridSpacingFor(
|
||||
width: number,
|
||||
cardWidth: number,
|
||||
): number {
|
||||
const columns = gridColumnsFor(width, cardWidth);
|
||||
const leftover = width - columns * cardWidth;
|
||||
|
||||
return Math.max(
|
||||
MIN_GRID_SPACING,
|
||||
Math.floor(leftover / (columns + 1)),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user