Fix/explore art scanner requests #21
No files matched your search
@@ -10,6 +10,7 @@ import type {
|
|||||||
VisibilityChangedEvent,
|
VisibilityChangedEvent,
|
||||||
} from '@lit-labs/virtualizer';
|
} from '@lit-labs/virtualizer';
|
||||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||||
|
import { gridSpacingFor } from '@utils/grid-spacing';
|
||||||
import {
|
import {
|
||||||
GetAlbumsByArtist,
|
GetAlbumsByArtist,
|
||||||
GetFilePathsByAlbums,
|
GetFilePathsByAlbums,
|
||||||
@@ -147,8 +148,6 @@ export class ArtistsView
|
|||||||
|
|
||||||
// ----- Grid spacing constants -----
|
// ----- Grid spacing constants -----
|
||||||
|
|
||||||
private static readonly GRID_GAP = 8;
|
|
||||||
private static readonly GRID_PADDING = 8;
|
|
||||||
private static readonly CARD_PADDING = 5;
|
private static readonly CARD_PADDING = 5;
|
||||||
|
|
||||||
private get imageSize(): number {
|
private get imageSize(): number {
|
||||||
@@ -177,20 +176,41 @@ export class ArtistsView
|
|||||||
private createGridLayout() {
|
private createGridLayout() {
|
||||||
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
||||||
const h = w + this.cardTextHeight;
|
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({
|
return grid({
|
||||||
itemSize: {
|
itemSize: {
|
||||||
width: `${w}px`,
|
width: `${w}px`,
|
||||||
height: `${h}px`,
|
height: `${h}px`,
|
||||||
},
|
},
|
||||||
gap: `${gap}px`,
|
gap: `${spacing}px`,
|
||||||
padding: `${pad}px`,
|
padding: `${spacing}px`,
|
||||||
justify: 'center',
|
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.
|
/** Sort direction for the artist grid.
|
||||||
*
|
*
|
||||||
* There is only one key to sort by: `library.Artist` carries a
|
* There is only one key to sort by: `library.Artist` carries a
|
||||||
@@ -478,6 +498,8 @@ export class ArtistsView
|
|||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.detachWheelListener();
|
this.detachWheelListener();
|
||||||
|
this.gridResizeObserver?.disconnect();
|
||||||
|
this.gridResizeObserver = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The wheel listener and the scroll debounce belong to the grid
|
/** The wheel listener and the scroll debounce belong to the grid
|
||||||
@@ -730,10 +752,34 @@ export class ArtistsView
|
|||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|
||||||
private lastLayoutWidth = 0;
|
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() {
|
private updateGridLayout() {
|
||||||
|
this.observeGridWidth();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.cardSize === this.lastLayoutWidth
|
this.cardSize === this.lastLayoutWidth &&
|
||||||
|
this.lastLayoutSpacing ===
|
||||||
|
this.spacingFor(this.containerWidth)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { LibraryController } from '@store/controllers/library-controller';
|
|||||||
import { SearchController } from '@store/controllers/search-controller';
|
import { SearchController } from '@store/controllers/search-controller';
|
||||||
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
import { ViewLifecycleMixin } from '@utils/view-lifecycle';
|
||||||
import { RovingGridController } from '@utils/roving-grid';
|
import { RovingGridController } from '@utils/roving-grid';
|
||||||
|
import { gridColumnsFor, gridSpacingFor } from '@utils/grid-spacing';
|
||||||
import { queueStore } from '@store/queue-store';
|
import { queueStore } from '@store/queue-store';
|
||||||
import type { QueueSource } from '@store/queue-store';
|
import type { QueueSource } from '@store/queue-store';
|
||||||
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
import '@awesome.me/webawesome/dist/components/popup/popup.js';
|
||||||
@@ -97,19 +98,36 @@ export class CoverGrid
|
|||||||
private lastAlbumsRef: library.Album[] | null =
|
private lastAlbumsRef: library.Album[] | null =
|
||||||
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 static readonly CARD_PADDING = 5;
|
||||||
|
|
||||||
private ctxMenu = new ContextMenuController(this);
|
private ctxMenu = new ContextMenuController(this);
|
||||||
private favCtrl = new FavoritesController(this);
|
private favCtrl = new FavoritesController(this);
|
||||||
private selMgr = new AlbumSelectionManager();
|
private selMgr = new AlbumSelectionManager();
|
||||||
private scrollMgr = new ScrollManager(this, {
|
private scrollMgr = new ScrollManager(this, {
|
||||||
GRID_GAP: CoverGrid.GRID_GAP,
|
columnsFor: (width: number) => this.columnsFor(width),
|
||||||
GRID_PADDING: CoverGrid.GRID_PADDING,
|
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 lastSelectedAlbumIndex: number | null = null;
|
||||||
private lastSelectedTrackIndex: number | null = null;
|
private lastSelectedTrackIndex: number | null = null;
|
||||||
|
|
||||||
@@ -148,10 +166,30 @@ export class CoverGrid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Virtualizer grid layout instance — recreated when
|
// Virtualizer grid layout instance — recreated when
|
||||||
// the card size changes.
|
// the card size or the container width changes.
|
||||||
private gridLayout = this.createGridLayout();
|
private gridLayout = this.createGridLayout();
|
||||||
private gridLayoutWidth = 0;
|
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
|
* Secondary layout for the "after" virtualizer in
|
||||||
* split mode. Uses zero top padding so there is no
|
* split mode. Uses zero top padding so there is no
|
||||||
@@ -169,22 +207,49 @@ export class CoverGrid
|
|||||||
}
|
}
|
||||||
|
|
||||||
const h = w + this.cardTextHeight;
|
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({
|
return grid({
|
||||||
itemSize: {
|
itemSize: {
|
||||||
width: `${w}px`,
|
width: `${w}px`,
|
||||||
height: `${h}px`,
|
height: `${h}px`,
|
||||||
},
|
},
|
||||||
gap: `${gap}px`,
|
gap: `${spacing}px`,
|
||||||
padding: noTopPad
|
padding: noTopPad
|
||||||
? `0 ${pad}px ${pad}px`
|
? `0 ${spacing}px ${spacing}px`
|
||||||
: `${pad}px`,
|
: `${spacing}px`,
|
||||||
justify: 'center',
|
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;
|
private dragImageEl: HTMLElement | null = null;
|
||||||
|
|
||||||
// -- Memoisation caches for filtered albums --
|
// -- Memoisation caches for filtered albums --
|
||||||
@@ -466,6 +531,9 @@ export class CoverGrid
|
|||||||
);
|
);
|
||||||
this.wheelListenerAttached = false;
|
this.wheelListenerAttached = false;
|
||||||
|
|
||||||
|
this.gridResizeObserver?.disconnect();
|
||||||
|
this.gridResizeObserver = null;
|
||||||
|
|
||||||
this.scrollMgr.teardown();
|
this.scrollMgr.teardown();
|
||||||
this.scrollMgr.revealContainer(
|
this.scrollMgr.revealContainer(
|
||||||
this.scrollContainer,
|
this.scrollContainer,
|
||||||
@@ -603,10 +671,18 @@ export class CoverGrid
|
|||||||
this.wheelListenerAttached = true;
|
this.wheelListenerAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recreate the virtualizer grid layout when
|
this.observeGridWidth();
|
||||||
// the card size changes.
|
|
||||||
|
// 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 =
|
const cardSizeChanged =
|
||||||
this.gridLayoutWidth !== this.cardWidth;
|
this.gridLayoutWidth !== this.cardWidth ||
|
||||||
|
this.gridLayoutSpacing !==
|
||||||
|
this.spacingFor(this.containerWidth);
|
||||||
|
|
||||||
if (cardSizeChanged) {
|
if (cardSizeChanged) {
|
||||||
this.gridLayout = this.createGridLayout();
|
this.gridLayout = this.createGridLayout();
|
||||||
|
|||||||
@@ -6,12 +6,21 @@ import type { LibraryController } from '@store/controllers/library-controller';
|
|||||||
import type { GridEntry } from './cover-grid-types.js';
|
import type { GridEntry } from './cover-grid-types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Grid spacing constants shared between the scroll
|
* Grid geometry, asked of the host rather than written down.
|
||||||
* manager and the host component.
|
*
|
||||||
|
* 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 {
|
export interface GridConstants {
|
||||||
readonly GRID_GAP: number;
|
/** Columns that fit across `width`. */
|
||||||
readonly GRID_PADDING: number;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
const pad = this.gc.GRID_PADDING;
|
const pad = gap;
|
||||||
const rowStep =
|
const rowStep =
|
||||||
this.host.cardHeight + gap;
|
this.host.cardHeight + gap;
|
||||||
|
|
||||||
@@ -293,7 +302,7 @@ export class ScrollManager {
|
|||||||
() => {
|
() => {
|
||||||
const rowStep =
|
const rowStep =
|
||||||
this.host.cardHeight +
|
this.host.cardHeight +
|
||||||
this.gc.GRID_GAP;
|
this.spacing(container);
|
||||||
|
|
||||||
if (this.pendingFocus === null) {
|
if (this.pendingFocus === null) {
|
||||||
this.isResizing = true;
|
this.isResizing = true;
|
||||||
@@ -351,7 +360,7 @@ export class ScrollManager {
|
|||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
rowStep: number,
|
rowStep: number,
|
||||||
): void {
|
): void {
|
||||||
const pad = this.gc.GRID_PADDING;
|
const pad = this.spacing(container);
|
||||||
const cols = this.currentColumnCount;
|
const cols = this.currentColumnCount;
|
||||||
const filtered =
|
const filtered =
|
||||||
this.host.cachedFilteredAlbums;
|
this.host.cachedFilteredAlbums;
|
||||||
@@ -410,17 +419,15 @@ export class ScrollManager {
|
|||||||
): number {
|
): number {
|
||||||
if (!container) return 1;
|
if (!container) return 1;
|
||||||
|
|
||||||
const gap = this.gc.GRID_GAP;
|
return this.gc.columnsFor(
|
||||||
const pad = this.gc.GRID_PADDING;
|
container.clientWidth,
|
||||||
const availableWidth =
|
);
|
||||||
container.clientWidth - pad * 2;
|
}
|
||||||
|
|
||||||
return Math.max(
|
/** The grid's current spacing, which is also its padding. */
|
||||||
1,
|
private spacing(container?: HTMLElement): number {
|
||||||
Math.floor(
|
return this.gc.spacingFor(
|
||||||
(availableWidth + gap) /
|
container?.clientWidth ?? 800,
|
||||||
(this.host.cardWidth + gap),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +446,7 @@ export class ScrollManager {
|
|||||||
container?: HTMLElement,
|
container?: HTMLElement,
|
||||||
): number {
|
): number {
|
||||||
const cols = this.getColumnCount(container);
|
const cols = this.getColumnCount(container);
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
cols * this.host.cardWidth +
|
cols * this.host.cardWidth +
|
||||||
@@ -460,7 +467,7 @@ export class ScrollManager {
|
|||||||
|
|
||||||
const cols = this.getColumnCount(container);
|
const cols = this.getColumnCount(container);
|
||||||
const colIndex = idx % cols;
|
const colIndex = idx % cols;
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
colIndex *
|
colIndex *
|
||||||
@@ -597,8 +604,8 @@ export class ScrollManager {
|
|||||||
|
|
||||||
if (!this.host.splitMode) return raw;
|
if (!this.host.splitMode) return raw;
|
||||||
|
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
const pad = this.gc.GRID_PADDING;
|
const pad = gap;
|
||||||
const columns =
|
const columns =
|
||||||
this.getColumnCount(container);
|
this.getColumnCount(container);
|
||||||
const rowStep = this.host.cardHeight + gap;
|
const rowStep = this.host.cardHeight + gap;
|
||||||
@@ -678,8 +685,8 @@ export class ScrollManager {
|
|||||||
|
|
||||||
if (expandedIndex < 0) return;
|
if (expandedIndex < 0) return;
|
||||||
|
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
const pad = this.gc.GRID_PADDING;
|
const pad = gap;
|
||||||
const columns =
|
const columns =
|
||||||
this.getColumnCount(container);
|
this.getColumnCount(container);
|
||||||
const rowStep = this.host.cardHeight + gap;
|
const rowStep = this.host.cardHeight + gap;
|
||||||
@@ -772,8 +779,8 @@ export class ScrollManager {
|
|||||||
|
|
||||||
if (idx < 0) return;
|
if (idx < 0) return;
|
||||||
|
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
const pad = this.gc.GRID_PADDING;
|
const pad = gap;
|
||||||
const cols =
|
const cols =
|
||||||
this.getColumnCount(container);
|
this.getColumnCount(container);
|
||||||
const rowStep = this.host.cardHeight + gap;
|
const rowStep = this.host.cardHeight + gap;
|
||||||
@@ -854,9 +861,8 @@ export class ScrollManager {
|
|||||||
this.getExpandedAlbumIndex();
|
this.getExpandedAlbumIndex();
|
||||||
|
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
const gap = this.gc.GRID_GAP;
|
const gap = this.spacing(container);
|
||||||
const pad =
|
const pad = gap;
|
||||||
this.gc.GRID_PADDING;
|
|
||||||
const cols =
|
const cols =
|
||||||
this.getColumnCount(
|
this.getColumnCount(
|
||||||
container,
|
container,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
VisibilityChangedEvent,
|
VisibilityChangedEvent,
|
||||||
} from '@lit-labs/virtualizer';
|
} from '@lit-labs/virtualizer';
|
||||||
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
|
||||||
|
import { gridSpacingFor } from '@utils/grid-spacing';
|
||||||
import {
|
import {
|
||||||
GetFilePathsByGenres,
|
GetFilePathsByGenres,
|
||||||
} from '@go/library/library.js';
|
} from '@go/library/library.js';
|
||||||
@@ -155,8 +156,6 @@ export class GenresView
|
|||||||
|
|
||||||
// ----- Grid spacing constants -----
|
// ----- Grid spacing constants -----
|
||||||
|
|
||||||
private static readonly GRID_GAP = 8;
|
|
||||||
private static readonly GRID_PADDING = 8;
|
|
||||||
private static readonly CARD_PADDING = 5;
|
private static readonly CARD_PADDING = 5;
|
||||||
|
|
||||||
private get imageSize(): number {
|
private get imageSize(): number {
|
||||||
@@ -185,20 +184,41 @@ export class GenresView
|
|||||||
private createGridLayout() {
|
private createGridLayout() {
|
||||||
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
const w = this.cardSize ?? CARD_SIZE_DEFAULT;
|
||||||
const h = w + this.cardTextHeight;
|
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({
|
return grid({
|
||||||
itemSize: {
|
itemSize: {
|
||||||
width: `${w}px`,
|
width: `${w}px`,
|
||||||
height: `${h}px`,
|
height: `${h}px`,
|
||||||
},
|
},
|
||||||
gap: `${gap}px`,
|
gap: `${spacing}px`,
|
||||||
padding: `${pad}px`,
|
padding: `${spacing}px`,
|
||||||
justify: 'center',
|
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). */
|
/** Sort key and direction for the genre grid (H-19: it had none). */
|
||||||
@state()
|
@state()
|
||||||
private sortField: 'name' | 'tracks' = 'name';
|
private sortField: 'name' | 'tracks' = 'name';
|
||||||
@@ -483,6 +503,8 @@ export class GenresView
|
|||||||
override disconnectedCallback() {
|
override disconnectedCallback() {
|
||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
this.detachWheelListener();
|
this.detachWheelListener();
|
||||||
|
this.gridResizeObserver?.disconnect();
|
||||||
|
this.gridResizeObserver = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** See artists-view: off-screen the grid cannot be scrolled, and
|
/** See artists-view: off-screen the grid cannot be scrolled, and
|
||||||
@@ -737,10 +759,34 @@ export class GenresView
|
|||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|
||||||
private lastLayoutWidth = 0;
|
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() {
|
private updateGridLayout() {
|
||||||
|
this.observeGridWidth();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.cardSize === this.lastLayoutWidth
|
this.cardSize === this.lastLayoutWidth &&
|
||||||
|
this.lastLayoutSpacing ===
|
||||||
|
this.spacingFor(this.containerWidth)
|
||||||
) {
|
) {
|
||||||
return;
|
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