feat(settings): library card grid max columns in Appearance (4–12, default 6)

Persist libraryGridMaxColumns, wire useCardGridMetrics and card grid layout,
add i18n and settings search index; document performance hint in copy.
This commit is contained in:
Maxim Isaev
2026-05-15 02:19:20 +03:00
parent 7c6d3694d4
commit 0f5ece6d03
21 changed files with 169 additions and 16 deletions
+45 -1
View File
@@ -1,8 +1,12 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { invoke } from '@tauri-apps/api/core';
import { Clock, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
import { Clock, LayoutGrid, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
import { useAuthStore } from '../../store/authStore';
import {
LIBRARY_GRID_MAX_COLUMNS_MAX,
LIBRARY_GRID_MAX_COLUMNS_MIN,
} from '../../store/authStoreDefaults';
import type { SeekbarStyle } from '../../store/authStoreTypes';
import { useFontStore, FontId } from '../../store/fontStore';
import { useThemeStore } from '../../store/themeStore';
@@ -108,6 +112,46 @@ export function AppearanceTab() {
</div>
</SettingsSubSection>
<SettingsSubSection
title={t('settings.libraryGridMaxColumnsTitle')}
icon={<LayoutGrid size={16} />}
>
<div className="settings-card">
<div className="settings-hint settings-hint-info" style={{ marginBottom: '0.75rem' }}>
{t('settings.libraryGridMaxColumnsPerfHint')}
</div>
<div className="form-group">
<label className="settings-label" htmlFor="library-grid-max-cols">
{t('settings.libraryGridMaxColumnsRangeLabel', {
min: LIBRARY_GRID_MAX_COLUMNS_MIN,
max: LIBRARY_GRID_MAX_COLUMNS_MAX,
})}
</label>
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', marginTop: 8 }}>
<input
id="library-grid-max-cols"
type="range"
min={LIBRARY_GRID_MAX_COLUMNS_MIN}
max={LIBRARY_GRID_MAX_COLUMNS_MAX}
step={1}
value={auth.libraryGridMaxColumns}
onChange={e => auth.setLibraryGridMaxColumns(Number(e.target.value))}
style={{ flex: 1, maxWidth: 360 }}
aria-valuemin={LIBRARY_GRID_MAX_COLUMNS_MIN}
aria-valuemax={LIBRARY_GRID_MAX_COLUMNS_MAX}
aria-valuenow={auth.libraryGridMaxColumns}
/>
<span style={{ minWidth: 28, fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>
{auth.libraryGridMaxColumns}
</span>
</div>
</div>
<p style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: '0.75rem', lineHeight: 1.45 }}>
{t('settings.libraryGridMaxColumnsDesc')}
</p>
</div>
</SettingsSubSection>
<SettingsSubSection
title={t('settings.visualOptionsTitle')}
icon={<Palette size={16} />}
+1
View File
@@ -46,6 +46,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
{ tab: 'personalisation',titleKey: 'settings.artistLayoutTitle', keywords: 'artist page layout sections order' },
{ tab: 'personalisation',titleKey: 'settings.homeCustomizerTitle', keywords: 'home page customize sections' },
{ tab: 'personalisation',titleKey: 'settings.queueToolbarTitle', keywords: 'queue toolbar buttons reorder customize shuffle save load' },
{ tab: 'appearance', titleKey: 'settings.libraryGridMaxColumnsTitle', keywords: 'grid columns album artist playlist cards layout appearance performance scroll paint' },
{ tab: 'library', titleKey: 'settings.randomMixTitle', keywords: 'random mix blacklist genre keywords filter audiobook' },
{ tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' },
{ tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' },
+11 -6
View File
@@ -1,6 +1,7 @@
import { useLayoutEffect, useState, type RefObject } from 'react';
import { useAuthStore } from '../store/authStore';
import { clampLibraryGridMaxColumns } from '../store/authStoreHelpers';
import {
CARD_GRID_MAX_COLS,
type CardGridRowHeightVariant,
computeCardGridColumnCount,
computeCellWidthPx,
@@ -8,8 +9,8 @@ import {
} from '../utils/cardGridLayout';
/**
* ResizeObserver-driven column count (max six) and virtual row height estimate
* from the measured cell width.
* ResizeObserver-driven column count (capped by Settings → Library) and
* virtual row height estimate from the measured cell width.
*/
export function useCardGridMetrics(
measureRef: RefObject<HTMLElement | null>,
@@ -17,9 +18,13 @@ export function useCardGridMetrics(
variant: CardGridRowHeightVariant,
layoutSignal: number,
): { gridCols: number; rowHeightEst: number } {
const maxCols = useAuthStore(s => clampLibraryGridMaxColumns(s.libraryGridMaxColumns));
const [gridCols, setGridCols] = useState(4);
const [rowHeightEst, setRowHeightEst] = useState(() =>
estimateRowHeightPx(computeCellWidthPx(960, CARD_GRID_MAX_COLS), variant),
estimateRowHeightPx(
computeCellWidthPx(960, clampLibraryGridMaxColumns(useAuthStore.getState().libraryGridMaxColumns)),
variant,
),
);
useLayoutEffect(() => {
@@ -28,7 +33,7 @@ export function useCardGridMetrics(
if (!el) return;
const onResize = () => {
const w = el.clientWidth;
const cols = computeCardGridColumnCount(w);
const cols = computeCardGridColumnCount(w, maxCols);
setGridCols(cols);
setRowHeightEst(estimateRowHeightPx(computeCellWidthPx(w, cols), variant));
};
@@ -36,7 +41,7 @@ export function useCardGridMetrics(
const ro = new ResizeObserver(onResize);
ro.observe(el);
return () => ro.disconnect();
}, [observerEnabled, variant, layoutSignal]);
}, [observerEnabled, variant, layoutSignal, maxCols]);
return { gridCols, rowHeightEst };
}
+4
View File
@@ -228,6 +228,10 @@ export const settings = {
aboutContributorsLabel: 'Mitwirkende',
showChangelogOnUpdate: "'Was ist neu' bei Update anzeigen",
showChangelogOnUpdateDesc: 'Blendet nach einem Update einen dezenten Changelog-Banner über Now Playing ein. Klick öffnet die Release Notes, X blendet ihn aus.',
libraryGridMaxColumnsTitle: 'Kartenraster in der Bibliothek',
libraryGridMaxColumnsPerfHint: 'Mehr Spalten bedeuten mehr Kacheln pro Zeile und oft mehr Layout- und Zeichenaufwand — bei sehr großen Bibliotheken oder langsamerer Hardware spürbarer.',
libraryGridMaxColumnsRangeLabel: 'Maximale Spalten ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Gilt für Album-, Künstler-, Wiedergabelisten-, Radio-, Offline- und andere Kartenansichten. Weniger Spalten = größere Kacheln und meist weniger CPU-Last.',
randomMixTitle: 'Zufallsmix-Blacklist',
luckyMixMenuTitle: 'Glücks-Mix im Menü anzeigen',
luckyMixMenuDesc: 'Aktiviert Glücks-Mix im "Mix erstellen"-Hub und als separaten Menüeintrag bei getrennter Navigation. Sichtbar nur bei aktiviertem AudioMuse auf dem aktiven Server.',
+4
View File
@@ -231,6 +231,10 @@ export const settings = {
aboutContributorsLabel: 'Contributors',
showChangelogOnUpdate: "Show 'What's New' on update",
showChangelogOnUpdateDesc: "Show a discreet changelog banner above Now Playing after an update. Click opens the release notes; X dismisses it.",
libraryGridMaxColumnsTitle: 'Library card grids',
libraryGridMaxColumnsPerfHint: 'Higher values pack more tiles per row and can increase layout and painting work—noticeable on very large libraries or slower devices.',
libraryGridMaxColumnsRangeLabel: 'Maximum columns ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Applies to album, artist, playlist, radio, offline, and other card-based library views. Lower values use larger tiles and are usually easier on the CPU.',
randomMixTitle: 'Random Mix Blacklist',
luckyMixMenuTitle: 'Show Lucky Mix in menu',
luckyMixMenuDesc: 'Enables Lucky Mix in Build a Mix and as a separate menu item when split navigation is on. Visible only when AudioMuse is enabled on the active server.',
+4
View File
@@ -227,6 +227,10 @@ export const settings = {
aboutContributorsLabel: 'Contribuidores',
showChangelogOnUpdate: "Mostrar 'Novedades' al actualizar",
showChangelogOnUpdateDesc: 'Muestra un discreto banner de changelog encima de Now Playing tras una actualización. Clic abre las notas de la versión; X lo oculta.',
libraryGridMaxColumnsTitle: 'Rejillas de tarjetas en la biblioteca',
libraryGridMaxColumnsPerfHint: 'Más columnas caben más mosaicos por fila y pueden aumentar el trabajo de diseño y pintado; se nota más en bibliotecas muy grandes o equipos lentos.',
libraryGridMaxColumnsRangeLabel: 'Máximo de columnas ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Se aplica a vistas de álbum, artista, lista de reproducción, radio, sin conexión y otras con tarjetas. Menos columnas = mosaicos más grandes y suele aligerar la CPU.',
randomMixTitle: 'Lista negra de Mezcla Aleatoria',
luckyMixMenuTitle: 'Mostrar Mezcla Suerte en el menú',
luckyMixMenuDesc: 'Activa Mezcla Suerte en "Crear Mezcla" y como elemento de menú separado cuando la navegación dividida está activa. Solo visible cuando AudioMuse está activo en el servidor actual.',
+4
View File
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: 'Contributeurs',
showChangelogOnUpdate: "Afficher 'Quoi de neuf' lors des mises à jour",
showChangelogOnUpdateDesc: "Affiche une bannière discrète du changelog au-dessus de Now Playing après une mise à jour. Un clic ouvre les notes de version, le X la masque.",
libraryGridMaxColumnsTitle: 'Grilles de cartes dans la bibliothèque',
libraryGridMaxColumnsPerfHint: 'Plus de colonnes signifie plus de tuiles par ligne et davantage de travail de mise en page et de peinture — surtout sur de grosses bibliothèques ou du matériel lent.',
libraryGridMaxColumnsRangeLabel: 'Nombre maximal de colonnes ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Sapplique aux vues album, artiste, liste de lecture, radio, hors ligne et autres pages en cartes. Moins de colonnes = tuiles plus grandes et en général moins de charge CPU.',
randomMixTitle: 'Liste noire du mix aléatoire',
luckyMixMenuTitle: 'Afficher Mix Chance dans le menu',
luckyMixMenuDesc: 'Active Mix Chance dans "Créer un mix" et comme entrée séparée quand la navigation est scindée. Visible uniquement si AudioMuse est actif sur le serveur courant.',
+4
View File
@@ -224,6 +224,10 @@ export const settings = {
aboutContributorsLabel: 'Bidragsytere',
showChangelogOnUpdate: "Vis 'Hva er nytt' ved oppdatering til ny versjon",
showChangelogOnUpdateDesc: "Viser et diskret changelog-banner over Now Playing etter en oppdatering. Klikk åpner versjonsnotatene; X skjuler det.",
libraryGridMaxColumnsTitle: 'Kortrutenett i biblioteket',
libraryGridMaxColumnsPerfHint: 'Flere kolonner gir flere fliser per rad og kan øke layout- og tegnearbeid — merkbart på veldig store biblioteker eller tregere maskiner.',
libraryGridMaxColumnsRangeLabel: 'Maksimalt antall kolonner ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Gjelder album-, artist-, spilleliste-, radio-, offline- og andre kortbaserte bibliotekvisninger. Færre kolonner = større fliser og vanligvis mindre CPU-belastning.',
randomMixTitle: 'Svarteliste for tilfeldig miks',
luckyMixMenuTitle: 'Vis Lykkemiks i menyen',
luckyMixMenuDesc: 'Aktiverer Lykkemiks i "Lag en miks" og som eget menypunkt når delt navigasjon er aktiv. Vises bare når AudioMuse er aktiv på gjeldende server.',
+4
View File
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: 'Bijdragers',
showChangelogOnUpdate: "'Wat is nieuw' tonen bij update",
showChangelogOnUpdateDesc: 'Toont een discrete changelog-banner boven Now Playing na een update. Klik opent de release-notities; X verbergt hem.',
libraryGridMaxColumnsTitle: 'Kaartenrasters in de bibliotheek',
libraryGridMaxColumnsPerfHint: 'Meer kolommen betekent meer tegels per rij en vaak meer lay-out- en schilderwerk — merkbaar bij zeer grote bibliotheken of tragere hardware.',
libraryGridMaxColumnsRangeLabel: 'Maximum aantal kolommen ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Geldt voor album-, artiest-, afspeellijst-, radio-, offline- en andere kaartweergaven. Minder kolommen = grotere tegels en meestal minder CPU-belasting.',
randomMixTitle: 'Willekeurige mix-blacklist',
luckyMixMenuTitle: 'Toon Geluksmix in menu',
luckyMixMenuDesc: 'Schakelt Geluksmix in bij "Mix samenstellen" en als apart menu-item bij gesplitste navigatie. Alleen zichtbaar wanneer AudioMuse actief is op de huidige server.',
+4
View File
@@ -231,6 +231,10 @@ export const settings = {
aboutContributorsLabel: 'Contribuabili',
showChangelogOnUpdate: "Arată 'Ce este nou' în update",
showChangelogOnUpdateDesc: "Arată un banner discret cu lista modificărilor deasupra Now Playing după un update. Apasă pentru a deschide lista modificărilor; X îl respinge.",
libraryGridMaxColumnsTitle: 'Grile cu carduri în bibliotecă',
libraryGridMaxColumnsPerfHint: 'Mai multe coloane înseamnă mai multe plăcuțe pe rând și mai multă muncă de layout și desen — se simte pe biblioteci foarte mari sau hardware lent.',
libraryGridMaxColumnsRangeLabel: 'Număr maxim de coloane ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Se aplică la albume, artiști, liste de redare, radio, offline și alte ecrane cu carduri. Mai puține coloane = plăcuțe mai mari și de obicei mai puțină încărcare pe CPU.',
randomMixTitle: 'Lista neagră Mix Aleatoriu',
luckyMixMenuTitle: 'Arată Mixul Norocos în meniu',
luckyMixMenuDesc: 'Activează Mixul Norocos în Construiește un Mix și ca un element separat în meniu când navigarea împărțită este pornită. Vizibil doar când AudioMuse este activat pe serverul activ.',
+4
View File
@@ -237,6 +237,10 @@ export const settings = {
aboutContributorsLabel: 'Участники',
showChangelogOnUpdate: 'Показывать «Что нового» после обновления',
showChangelogOnUpdateDesc: 'После обновления над «Сейчас играет» появится ненавязчивый баннер журнала изменений. Клик открывает заметки о выпуске, X скрывает.',
libraryGridMaxColumnsTitle: 'Сетки карточек в библиотеке',
libraryGridMaxColumnsPerfHint: 'Больше колонок — больше плиток в ряд и больше работы по вёрстке и отрисовке; на очень больших библиотеках или слабом железе это заметнее.',
libraryGridMaxColumnsRangeLabel: 'Максимум колонок ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Действует для альбомов, исполнителей, плейлистов, радио, офлайн-библиотеки и других экранов с карточками. Меньше колонок — крупнее плитки и обычно меньше нагрузка на CPU.',
randomMixTitle: 'Чёрный список случайного микса',
luckyMixMenuTitle: 'Показывать «Мне повезёт» в меню',
luckyMixMenuDesc:
+4
View File
@@ -225,6 +225,10 @@ export const settings = {
aboutContributorsLabel: '贡献者',
showChangelogOnUpdate: '更新时显示"新功能"',
showChangelogOnUpdateDesc: '更新后在「正在播放」上方显示一个低调的更新日志横幅。点击打开发行说明,X 按钮关闭。',
libraryGridMaxColumnsTitle: '资料库卡片网格',
libraryGridMaxColumnsPerfHint: '列数越多,每行显示的卡片越多,布局与绘制开销通常也越大;在超大曲库或较慢设备上更明显。',
libraryGridMaxColumnsRangeLabel: '最大列数({{min}}{{max}}',
libraryGridMaxColumnsDesc: '适用于专辑、艺人、播放列表、电台、离线及其他卡片式资料库视图。列数越少卡片越大,通常对 CPU 更友好。',
randomMixTitle: '随机混音黑名单',
luckyMixMenuTitle: '在菜单中显示“好运混音”',
luckyMixMenuDesc: '在“创建混音”中启用“好运混音”,并在分离导航时作为独立菜单项显示。仅当当前服务器启用 AudioMuse 时可见。',
+15
View File
@@ -30,6 +30,10 @@ vi.mock('@/api/subsonic', () => ({
import { useAuthStore } from './authStore';
import { resetAuthStore, resetPlayerStore } from '@/test/helpers/storeReset';
import { onInvoke } from '@/test/mocks/tauri';
import {
LIBRARY_GRID_MAX_COLUMNS_MAX,
LIBRARY_GRID_MAX_COLUMNS_MIN,
} from './authStoreDefaults';
beforeEach(() => {
resetAuthStore();
@@ -120,6 +124,17 @@ describe('setters with validation / clamping', () => {
expect(useAuthStore.getState().trackPreviewDurationSec).toBe(31);
});
it('setLibraryGridMaxColumns clamps to the allowed column range', () => {
useAuthStore.getState().setLibraryGridMaxColumns(99);
expect(useAuthStore.getState().libraryGridMaxColumns).toBe(LIBRARY_GRID_MAX_COLUMNS_MAX);
useAuthStore.getState().setLibraryGridMaxColumns(1);
expect(useAuthStore.getState().libraryGridMaxColumns).toBe(LIBRARY_GRID_MAX_COLUMNS_MIN);
useAuthStore.getState().setLibraryGridMaxColumns(6);
expect(useAuthStore.getState().libraryGridMaxColumns).toBe(6);
});
it('setTrackPreviewsEnabled coerces truthy/falsy to boolean', () => {
useAuthStore.getState().setTrackPreviewsEnabled('yes' as unknown as boolean);
expect(useAuthStore.getState().trackPreviewsEnabled).toBe(true);
+2
View File
@@ -17,6 +17,7 @@ import {
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
DEFAULT_LYRICS_SOURCES,
DEFAULT_TRACK_PREVIEW_LOCATIONS,
DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
} from './authStoreDefaults';
import { computeAuthStoreRehydration } from './authStoreRehydrate';
import type { AuthState } from './authStoreTypes';
@@ -58,6 +59,7 @@ export const useAuthStore = create<AuthState>()(
infiniteQueueEnabled: false,
preservePlayNextOrder: false,
showArtistImages: false,
libraryGridMaxColumns: DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
showTrayIcon: true,
minimizeToTray: false,
showOrbitTrigger: true,
+5
View File
@@ -38,3 +38,8 @@ export const DEFAULT_LYRICS_SOURCES: LyricsSourceConfig[] = [
export const MIX_MIN_RATING_FILTER_MAX_STARS = 3;
export const RANDOM_MIX_SIZE_OPTIONS: readonly number[] = [50, 75, 100, 125, 150];
/** Default max columns for album/artist/playlist card grids (Settings → Library). */
export const DEFAULT_LIBRARY_GRID_MAX_COLUMNS = 6;
export const LIBRARY_GRID_MAX_COLUMNS_MIN = 4;
export const LIBRARY_GRID_MAX_COLUMNS_MAX = 12;
+10
View File
@@ -1,6 +1,9 @@
import { clampStoredLoudnessPreAnalysisAttenuationRefDb } from '../utils/audio/loudnessPreAnalysisSlider';
import {
DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB,
DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
LIBRARY_GRID_MAX_COLUMNS_MAX,
LIBRARY_GRID_MAX_COLUMNS_MIN,
LOUDNESS_LUFS_PRESETS,
MIX_MIN_RATING_FILTER_MAX_STARS,
RANDOM_MIX_SIZE_OPTIONS,
@@ -41,6 +44,13 @@ export function clampRandomMixSize(v: number): number {
return nearest;
}
/** Persisted max columns for library card grids (albums, artists, playlists, …). */
export function clampLibraryGridMaxColumns(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v);
if (!Number.isFinite(n)) return DEFAULT_LIBRARY_GRID_MAX_COLUMNS;
return Math.max(LIBRARY_GRID_MAX_COLUMNS_MIN, Math.min(LIBRARY_GRID_MAX_COLUMNS_MAX, Math.round(n)));
}
export function clampSkipStarThreshold(v: number): number {
if (!Number.isFinite(v)) return 3;
return Math.max(1, Math.min(99, Math.round(v)));
+4
View File
@@ -7,6 +7,7 @@ import { DEFAULT_LOUDNESS_PRE_ANALYSIS_ATTENUATION_DB } from './authStoreDefault
import {
clampMixFilterMinStars,
clampRandomMixSize,
clampLibraryGridMaxColumns,
sanitizeLoudnessLufsPreset,
sanitizeLoudnessPreAnalysisFromStorage,
sanitizeSkipStarCounts,
@@ -116,6 +117,9 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
mixMinRatingAlbum: clampMixFilterMinStars(state.mixMinRatingAlbum as number),
mixMinRatingArtist: clampMixFilterMinStars(state.mixMinRatingArtist as number),
randomMixSize: clampRandomMixSize(state.randomMixSize as number),
libraryGridMaxColumns: clampLibraryGridMaxColumns(
(state as { libraryGridMaxColumns?: unknown }).libraryGridMaxColumns,
),
skipStarManualSkipCountsByKey: sanitizeSkipStarCounts(
(state as { skipStarManualSkipCountsByKey?: unknown }).skipStarManualSkipCountsByKey,
),
+6
View File
@@ -80,6 +80,11 @@ export interface AuthState {
infiniteQueueEnabled: boolean;
preservePlayNextOrder: boolean;
showArtistImages: boolean;
/**
* Max columns for album/artist/playlist-style card grids (Settings Library).
* Clamped 212; higher values mean more tiles per row and more layout/paint work.
*/
libraryGridMaxColumns: number;
showTrayIcon: boolean;
minimizeToTray: boolean;
/** Whether the "Orbit" topbar trigger is rendered. Users who never
@@ -252,6 +257,7 @@ export interface AuthState {
setInfiniteQueueEnabled: (v: boolean) => void;
setPreservePlayNextOrder: (v: boolean) => void;
setShowArtistImages: (v: boolean) => void;
setLibraryGridMaxColumns: (v: number) => void;
setShowTrayIcon: (v: boolean) => void;
setMinimizeToTray: (v: boolean) => void;
setShowOrbitTrigger: (v: boolean) => void;
+3
View File
@@ -1,4 +1,5 @@
import type { AuthState } from './authStoreTypes';
import { clampLibraryGridMaxColumns } from './authStoreHelpers';
type SetState = (
partial: Partial<AuthState> | ((state: AuthState) => Partial<AuthState>),
@@ -12,6 +13,7 @@ type SetState = (
export function createUiAppearanceActions(set: SetState): Pick<
AuthState,
| 'setShowArtistImages'
| 'setLibraryGridMaxColumns'
| 'setShowTrayIcon'
| 'setMinimizeToTray'
| 'setShowOrbitTrigger'
@@ -30,6 +32,7 @@ export function createUiAppearanceActions(set: SetState): Pick<
> {
return {
setShowArtistImages: (v) => set({ showArtistImages: v }),
setLibraryGridMaxColumns: (v) => set({ libraryGridMaxColumns: clampLibraryGridMaxColumns(v) }),
setShowTrayIcon: (v) => set({ showTrayIcon: v }),
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
setShowOrbitTrigger: (v) => set({ showOrbitTrigger: v }),
+16 -6
View File
@@ -1,16 +1,26 @@
import { describe, expect, it } from 'vitest';
import { computeCardGridColumnCount, CARD_GRID_MAX_COLS } from './cardGridLayout';
import { computeCardGridColumnCount } from './cardGridLayout';
import { LIBRARY_GRID_MAX_COLUMNS_MAX, LIBRARY_GRID_MAX_COLUMNS_MIN } from '../store/authStoreDefaults';
describe('computeCardGridColumnCount', () => {
it('never exceeds CARD_GRID_MAX_COLS', () => {
expect(computeCardGridColumnCount(20_000)).toBe(CARD_GRID_MAX_COLS);
it('never exceeds the configured max', () => {
expect(computeCardGridColumnCount(20_000, 6)).toBe(6);
expect(computeCardGridColumnCount(20_000, 4)).toBe(4);
});
it('clamps requested max to store-wide upper bound', () => {
expect(computeCardGridColumnCount(20_000, 99)).toBe(LIBRARY_GRID_MAX_COLUMNS_MAX);
});
it('clamps requested max to store-wide lower bound', () => {
expect(computeCardGridColumnCount(20_000, 2)).toBe(LIBRARY_GRID_MAX_COLUMNS_MIN);
});
it('returns at least one column', () => {
expect(computeCardGridColumnCount(50)).toBe(1);
expect(computeCardGridColumnCount(50, 6)).toBe(1);
});
it('uses six columns on wide desktop widths', () => {
expect(computeCardGridColumnCount(1200)).toBe(6);
it('uses six columns on wide desktop when max allows', () => {
expect(computeCardGridColumnCount(1200, 6)).toBe(6);
});
});
+15 -3
View File
@@ -3,15 +3,27 @@
* and row-height estimates derived from measured cell width (TanStack virtual rows).
*/
import {
DEFAULT_LIBRARY_GRID_MAX_COLUMNS,
LIBRARY_GRID_MAX_COLUMNS_MAX,
LIBRARY_GRID_MAX_COLUMNS_MIN,
} from '../store/authStoreDefaults';
export const CARD_GRID_GAP_PX = 16;
export const CARD_GRID_MIN_TILE_PX = 140;
export const CARD_GRID_MAX_COLS = 6;
export function computeCardGridColumnCount(containerWidthPx: number): number {
/** @deprecated use `DEFAULT_LIBRARY_GRID_MAX_COLUMNS` from `authStoreDefaults` */
export const CARD_GRID_MAX_COLS = DEFAULT_LIBRARY_GRID_MAX_COLUMNS;
export function computeCardGridColumnCount(containerWidthPx: number, maxColumns: number): number {
const cap = Math.max(
LIBRARY_GRID_MAX_COLUMNS_MIN,
Math.min(LIBRARY_GRID_MAX_COLUMNS_MAX, Math.round(maxColumns)),
);
const raw = Math.floor(
(containerWidthPx + CARD_GRID_GAP_PX) / (CARD_GRID_MIN_TILE_PX + CARD_GRID_GAP_PX),
);
return Math.min(CARD_GRID_MAX_COLS, Math.max(1, raw));
return Math.min(cap, Math.max(1, raw));
}
export function computeCellWidthPx(containerWidthPx: number, columnCount: number): number {