grid and list view customizability

This commit is contained in:
Boof2015
2026-07-28 13:55:04 -04:00
parent 8d78278852
commit 4c40af970b
9 changed files with 257 additions and 25 deletions
+1
View File
@@ -83,6 +83,7 @@
"test:now-playing-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/player/nowPlayingLayout.test.mts src/components/player/nowPlayingPreferences.test.mts src/components/player/nowPlayingDismiss.test.mts src/playback/playbackTargetPresentation.test.mts", "test:now-playing-layout": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/player/nowPlayingLayout.test.mts src/components/player/nowPlayingPreferences.test.mts src/components/player/nowPlayingDismiss.test.mts src/playback/playbackTargetPresentation.test.mts",
"test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs", "test:memory-lifecycle": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/components/delayedPresence.test.mts scripts/android-memory-profile.test.mjs",
"test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/homeLibraryNavigation.test.mts src/navigation/tabTransition.test.mts", "test:ui-navigation": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/stores/playerPresence.test.mts src/navigation/tabsAnchor.test.mts src/navigation/libraryDetailBack.test.mts src/navigation/homeLibraryNavigation.test.mts src/navigation/tabTransition.test.mts",
"test:library-layout": "node --experimental-strip-types --test src/library/libraryLayout.test.mts",
"test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts", "test:haptics": "node --experimental-strip-types --experimental-specifier-resolution=node --test src/lib/haptics.test.mts",
"test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts", "test:app-dialog": "node --experimental-strip-types --test src/components/dialogs/dialogQueue.test.mts",
"test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts", "test:home-greeting": "node --experimental-strip-types --test src/home/homeGreeting.test.mts",
+1
View File
@@ -20,6 +20,7 @@ const TEST_SCRIPTS = [
'test:now-playing-layout', 'test:now-playing-layout',
'test:memory-lifecycle', 'test:memory-lifecycle',
'test:ui-navigation', 'test:ui-navigation',
'test:library-layout',
'test:haptics', 'test:haptics',
'test:home-greeting', 'test:home-greeting',
'test:session', 'test:session',
+117 -21
View File
@@ -20,6 +20,8 @@ import { Text } from '@/components/Text';
import { ViewModeSwitcher } from '@/components/library/ViewModeSwitcher'; import { ViewModeSwitcher } from '@/components/library/ViewModeSwitcher';
import { AlbumGridItem } from '@/components/library/AlbumGridItem'; import { AlbumGridItem } from '@/components/library/AlbumGridItem';
import { ArtistGridItem } from '@/components/library/ArtistGridItem'; import { ArtistGridItem } from '@/components/library/ArtistGridItem';
import { AlbumRow } from '@/components/library/AlbumRow';
import { ArtistRow } from '@/components/library/ArtistRow';
import { TrackRow } from '@/components/library/TrackRow'; import { TrackRow } from '@/components/library/TrackRow';
import { FoldersView } from '@/components/library/FoldersView'; import { FoldersView } from '@/components/library/FoldersView';
import { PlaylistsView } from '@/components/library/PlaylistsView'; import { PlaylistsView } from '@/components/library/PlaylistsView';
@@ -64,6 +66,12 @@ import {
ARTIST_SORT_LABELS, ARTIST_SORT_LABELS,
type ArtistSort type ArtistSort
} from '@/lib/artistSort'; } from '@/lib/artistSort';
import {
LIBRARY_LAYOUT_OPTIONS,
libraryLayoutColumns,
libraryLayoutLabel,
type LibraryLayout,
} from '@/library/libraryLayout';
import type { import type {
DbTrack DbTrack
} from '@/types/library'; } from '@/types/library';
@@ -95,6 +103,10 @@ export default function LibraryScreen() {
const setAlbumSort = useLibraryStore((s) => s.setAlbumSort); const setAlbumSort = useLibraryStore((s) => s.setAlbumSort);
const artistSort = useLibraryStore((s) => s.artistSort); const artistSort = useLibraryStore((s) => s.artistSort);
const setArtistSort = useLibraryStore((s) => s.setArtistSort); const setArtistSort = useLibraryStore((s) => s.setArtistSort);
const albumLayout = useLibraryStore((s) => s.albumLayout);
const setAlbumLayout = useLibraryStore((s) => s.setAlbumLayout);
const artistLayout = useLibraryStore((s) => s.artistLayout);
const setArtistLayout = useLibraryStore((s) => s.setArtistLayout);
const loadNextTracks = useLibraryStore((s) => s.loadNextTracks); const loadNextTracks = useLibraryStore((s) => s.loadNextTracks);
const loadNextAlbums = useLibraryStore((s) => s.loadNextAlbums); const loadNextAlbums = useLibraryStore((s) => s.loadNextAlbums);
const loadNextArtists = useLibraryStore((s) => s.loadNextArtists); const loadNextArtists = useLibraryStore((s) => s.loadNextArtists);
@@ -117,6 +129,7 @@ export default function LibraryScreen() {
const [actionTrack, setActionTrack] = useState<DbTrack | null>(null); const [actionTrack, setActionTrack] = useState<DbTrack | null>(null);
const [sortSheetOpen, setSortSheetOpen] = useState(false); const [sortSheetOpen, setSortSheetOpen] = useState(false);
const [layoutSheetOpen, setLayoutSheetOpen] = useState(false);
const [selectMode, setSelectMode] = useState(false); const [selectMode, setSelectMode] = useState(false);
const [selectedIds, setSelectedIds] = useState<Set<number>>(() => new Set()); const [selectedIds, setSelectedIds] = useState<Set<number>>(() => new Set());
const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false); const [playlistPickerOpen, setPlaylistPickerOpen] = useState(false);
@@ -290,6 +303,20 @@ export default function LibraryScreen() {
selected: option === artistSort, selected: option === artistSort,
onSelect: () => setArtistSort(option), onSelect: () => setArtistSort(option),
})); }));
const activeLayout =
viewMode === 'albums'
? albumLayout
: viewMode === 'artists'
? artistLayout
: null;
const activeLayoutLabel = activeLayout ? libraryLayoutLabel(activeLayout) : null;
const layoutSheetLabel = viewMode === 'albums' ? 'ALBUM LAYOUT' : 'ARTIST LAYOUT';
const setActiveLayout = (layout: LibraryLayout) => {
scrollTop.setScrollAtTop(true);
if (viewMode === 'albums') setAlbumLayout(layout);
if (viewMode === 'artists') setArtistLayout(layout);
};
return ( return (
<Screen> <Screen>
@@ -319,6 +346,7 @@ export default function LibraryScreen() {
value={viewMode} value={viewMode}
onChange={(mode) => { onChange={(mode) => {
if (selectMode) exitSelection(); if (selectMode) exitSelection();
setLayoutSheetOpen(false);
scrollTop.setScrollAtTop(true); scrollTop.setScrollAtTop(true);
setViewMode(mode); setViewMode(mode);
}} }}
@@ -343,23 +371,41 @@ export default function LibraryScreen() {
</Pressable> </Pressable>
</View> </View>
) : sortable ? ( ) : sortable ? (
<Pressable android_ripple={ripple.bounded} <View style={styles.controlsRow}>
style={styles.sortTrigger} <Pressable android_ripple={ripple.bounded}
onPress={() => setSortSheetOpen(true)} style={styles.sortTrigger}
accessibilityRole="button" onPress={() => setSortSheetOpen(true)}
accessibilityLabel={`Sort by ${sortLabel}`} accessibilityRole="button"
> accessibilityLabel={`Sort by ${sortLabel}`}
<Ionicons name="swap-vertical" size={14} color={colors.textSecondary} /> >
<Text variant="label">{sortLabel}</Text> <Ionicons name="swap-vertical" size={14} color={colors.textSecondary} />
</Pressable> <Text variant="label">{sortLabel}</Text>
</Pressable>
{activeLayout && activeLayoutLabel ? (
<Pressable
android_ripple={ripple.bounded}
style={styles.layoutTrigger}
hitSlop={8}
onPress={() => setLayoutSheetOpen(true)}
accessibilityRole="button"
accessibilityLabel={`Change ${viewMode} layout. Current layout: ${activeLayoutLabel}`}
>
<Ionicons
name={activeLayout === 'list' ? 'list-outline' : 'grid-outline'}
size={18}
color={colors.textSecondary}
/>
</Pressable>
) : null}
</View>
) : null} ) : null}
<View style={styles.listArea}> <View style={styles.listArea}>
{viewMode === 'albums' ? ( {viewMode === 'albums' ? (
<FlashList <FlashList
key={`albums-${albumSort}-${sectionJumpRevision}`} key={`albums-${albumSort}-${albumLayout}-${sectionJumpRevision}`}
data={sortedAlbums} data={sortedAlbums}
numColumns={3} numColumns={libraryLayoutColumns(albumLayout)}
keyExtractor={(album) => album.identity_key} keyExtractor={(album) => album.identity_key}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
overScrollMode="never" overScrollMode="never"
@@ -373,8 +419,8 @@ export default function LibraryScreen() {
onStartReachedThreshold={START_REACHED_THRESHOLD} onStartReachedThreshold={START_REACHED_THRESHOLD}
ListFooterComponent={albumNextCursor ? listFooter : null} ListFooterComponent={albumNextCursor ? listFooter : null}
renderItem={({ item }) => ( renderItem={({ item }) => (
<View style={styles.gridCell}> albumLayout === 'list' ? (
<AlbumGridItem <AlbumRow
album={item} album={item}
onPress={() => onPress={() =>
router.push({ router.push({
@@ -383,16 +429,28 @@ export default function LibraryScreen() {
}) })
} }
/> />
</View> ) : (
<View style={styles.gridCell}>
<AlbumGridItem
album={item}
onPress={() =>
router.push({
pathname: '/library/album/[key]',
params: { key: item.identity_key },
})
}
/>
</View>
)
)} )}
/> />
) : null} ) : null}
{viewMode === 'artists' ? ( {viewMode === 'artists' ? (
<FlashList <FlashList
key={`artists-${artistSort}-${sectionJumpRevision}`} key={`artists-${artistSort}-${artistLayout}-${sectionJumpRevision}`}
data={sortedArtists} data={sortedArtists}
numColumns={3} numColumns={libraryLayoutColumns(artistLayout)}
keyExtractor={(artist) => artist.artist} keyExtractor={(artist) => artist.artist}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
overScrollMode="never" overScrollMode="never"
@@ -406,8 +464,8 @@ export default function LibraryScreen() {
onStartReachedThreshold={START_REACHED_THRESHOLD} onStartReachedThreshold={START_REACHED_THRESHOLD}
ListFooterComponent={artistNextCursor ? listFooter : null} ListFooterComponent={artistNextCursor ? listFooter : null}
renderItem={({ item }) => ( renderItem={({ item }) => (
<View style={styles.gridCell}> artistLayout === 'list' ? (
<ArtistGridItem <ArtistRow
artist={item} artist={item}
onPress={() => onPress={() =>
router.push({ router.push({
@@ -416,7 +474,19 @@ export default function LibraryScreen() {
}) })
} }
/> />
</View> ) : (
<View style={styles.gridCell}>
<ArtistGridItem
artist={item}
onPress={() =>
router.push({
pathname: '/library/artist/[name]',
params: { name: item.artist },
})
}
/>
</View>
)
)} )}
/> />
) : null} ) : null}
@@ -516,6 +586,24 @@ export default function LibraryScreen() {
))} ))}
</AppSheet> </AppSheet>
) : null} ) : null}
{layoutSheetOpen && activeLayout ? (
<AppSheet onClose={() => setLayoutSheetOpen(false)}>
<AppSheetSection label={layoutSheetLabel} />
{LIBRARY_LAYOUT_OPTIONS.map((option) => (
<AppSheetItem
key={option.value}
label={option.label}
subtitle={option.subtitle}
icon={option.icon}
selected={option.value === activeLayout}
onPress={() => {
setActiveLayout(option.value);
setLayoutSheetOpen(false);
}}
/>
))}
</AppSheet>
) : null}
</Screen> </Screen>
); );
} }
@@ -537,13 +625,21 @@ const styles = StyleSheet.create({
error: { error: {
marginBottom: spacing.md, marginBottom: spacing.md,
}, },
controlsRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-end',
gap: spacing.md,
marginBottom: spacing.xs,
},
sortTrigger: { sortTrigger: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: spacing.xs, gap: spacing.xs,
alignSelf: 'flex-end',
paddingVertical: spacing.xs, paddingVertical: spacing.xs,
marginBottom: spacing.xs, },
layoutTrigger: {
padding: spacing.xs,
}, },
// Same vertical rhythm as sortTrigger so entering selection doesn't shift the list. // Same vertical rhythm as sortTrigger so entering selection doesn't shift the list.
selectionHeader: { selectionHeader: {
+3 -1
View File
@@ -15,7 +15,7 @@ import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { albumArtworkSource } from '@/library/artwork'; import { albumArtworkSource } from '@/library/artwork';
import type { Album } from '@/types/library'; import type { Album } from '@/types/library';
/** Compact album list row (search results) — the grid uses AlbumGridItem. */ /** Compact album list row for library browsing and search results. */
export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void }) { export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void }) {
const styles = useStyles(); const styles = useStyles();
const colors = useColors(); const colors = useColors();
@@ -29,6 +29,8 @@ export function AlbumRow({ album, onPress }: { album: Album; onPress: () => void
source={{ uri: artUri }} source={{ uri: artUri }}
style={styles.artImage} style={styles.artImage}
contentFit="cover" contentFit="cover"
recyclingKey={album.identity_key}
transition={null}
/> />
) : ( ) : (
<Ionicons name="disc-outline" size={20} color={colors.textTertiary} /> <Ionicons name="disc-outline" size={20} color={colors.textTertiary} />
+1 -1
View File
@@ -15,7 +15,7 @@ import { SCROLL_PRESS_DELAY, useRipple } from '@/theme/ripple';
import { artworkUri } from '@/library/artwork'; import { artworkUri } from '@/library/artwork';
import type { Artist } from '@/types/library'; import type { Artist } from '@/types/library';
/** 2-column grid cell: square art (2x2 album mosaic when available) + counts, matching the album grid. */ /** Resizable grid cell: square art (2x2 album mosaic when available) + counts. */
export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: () => void }) { export function ArtistGridItem({ artist, onPress }: { artist: Artist; onPress: () => void }) {
const styles = useStyles(); const styles = useStyles();
const colors = useColors(); const colors = useColors();
+6 -2
View File
@@ -19,6 +19,8 @@ export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () =>
const styles = useStyles(); const styles = useStyles();
const colors = useColors(); const colors = useColors();
const ripple = useRipple(); const ripple = useRipple();
const albums = `${artist.album_count} ${artist.album_count === 1 ? 'album' : 'albums'}`;
const tracks = `${artist.track_count} ${artist.track_count === 1 ? 'track' : 'tracks'}`;
return ( return (
<Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button"> <Pressable android_ripple={ripple.bounded} unstable_pressDelay={SCROLL_PRESS_DELAY} style={styles.row} onPress={onPress} accessibilityRole="button">
<View style={styles.art}> <View style={styles.art}>
@@ -27,6 +29,8 @@ export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () =>
source={{ uri: artworkUri(artist.artwork_hash) }} source={{ uri: artworkUri(artist.artwork_hash) }}
style={styles.artImage} style={styles.artImage}
contentFit="cover" contentFit="cover"
recyclingKey={artist.artist}
transition={null}
/> />
) : ( ) : (
<Ionicons name="person" size={20} color={colors.textTertiary} /> <Ionicons name="person" size={20} color={colors.textTertiary} />
@@ -36,8 +40,8 @@ export function ArtistRow({ artist, onPress }: { artist: Artist; onPress: () =>
<Text variant="body" numberOfLines={1}> <Text variant="body" numberOfLines={1}>
{artist.artist} {artist.artist}
</Text> </Text>
<Text variant="label"> <Text variant="label" numberOfLines={1}>
{artist.track_count} {artist.track_count === 1 ? 'track' : 'tracks'} {albums} · {tracks}
</Text> </Text>
</View> </View>
<Ionicons name="chevron-forward" size={16} color={colors.textTertiary} /> <Ionicons name="chevron-forward" size={16} color={colors.textTertiary} />
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
DEFAULT_LIBRARY_LAYOUT,
LIBRARY_LAYOUT_OPTIONS,
libraryLayoutColumns,
libraryLayoutLabel,
parseLibraryLayout,
type LibraryLayout,
} from './libraryLayout.ts';
test('defaults missing and invalid library layouts to the current three-column grid', () => {
assert.equal(parseLibraryLayout(null), DEFAULT_LIBRARY_LAYOUT);
assert.equal(parseLibraryLayout(''), DEFAULT_LIBRARY_LAYOUT);
assert.equal(parseLibraryLayout('grid'), DEFAULT_LIBRARY_LAYOUT);
assert.equal(parseLibraryLayout('grid-5'), DEFAULT_LIBRARY_LAYOUT);
});
test('restores every supported persisted library layout', () => {
for (const option of LIBRARY_LAYOUT_OPTIONS) {
assert.equal(parseLibraryLayout(option.value), option.value);
}
});
test('maps list and grid layouts to their visible column counts and labels', () => {
const expectations: Array<[LibraryLayout, number, string]> = [
['list', 1, 'List'],
['grid-2', 2, 'Large grid'],
['grid-3', 3, 'Medium grid'],
['grid-4', 4, 'Compact grid'],
];
for (const [layout, columns, label] of expectations) {
assert.equal(libraryLayoutColumns(layout), columns);
assert.equal(libraryLayoutLabel(layout), label);
}
});
+62
View File
@@ -0,0 +1,62 @@
export type LibraryLayout = 'list' | 'grid-2' | 'grid-3' | 'grid-4';
export interface LibraryLayoutOption {
value: LibraryLayout;
label: string;
subtitle: string;
icon: 'list-outline' | 'grid-outline';
columns: number;
}
export const DEFAULT_LIBRARY_LAYOUT: LibraryLayout = 'grid-3';
export const LIBRARY_LAYOUT_OPTIONS: readonly LibraryLayoutOption[] = [
{
value: 'list',
label: 'List',
subtitle: 'Compact rows',
icon: 'list-outline',
columns: 1,
},
{
value: 'grid-2',
label: 'Large grid',
subtitle: '2 columns',
icon: 'grid-outline',
columns: 2,
},
{
value: 'grid-3',
label: 'Medium grid',
subtitle: '3 columns',
icon: 'grid-outline',
columns: 3,
},
{
value: 'grid-4',
label: 'Compact grid',
subtitle: '4 columns',
icon: 'grid-outline',
columns: 4,
},
] as const;
const LIBRARY_LAYOUTS = new Set<LibraryLayout>(
LIBRARY_LAYOUT_OPTIONS.map((option) => option.value)
);
export function parseLibraryLayout(value: string | null): LibraryLayout {
return value !== null && LIBRARY_LAYOUTS.has(value as LibraryLayout)
? (value as LibraryLayout)
: DEFAULT_LIBRARY_LAYOUT;
}
export function libraryLayoutColumns(layout: LibraryLayout): number {
return LIBRARY_LAYOUT_OPTIONS.find((option) => option.value === layout)?.columns
?? LIBRARY_LAYOUT_OPTIONS.find((option) => option.value === DEFAULT_LIBRARY_LAYOUT)!.columns;
}
export function libraryLayoutLabel(layout: LibraryLayout): string {
return LIBRARY_LAYOUT_OPTIONS.find((option) => option.value === layout)?.label
?? LIBRARY_LAYOUT_OPTIONS.find((option) => option.value === DEFAULT_LIBRARY_LAYOUT)!.label;
}
+29
View File
@@ -20,6 +20,11 @@ import { endScanService, reportScanProgress } from '@/library/scanService';
import { ALBUM_SORT_LABELS, type AlbumSort } from '@/lib/albumSort'; import { ALBUM_SORT_LABELS, type AlbumSort } from '@/lib/albumSort';
import { ARTIST_SORT_LABELS, type ArtistSort } from '@/lib/artistSort'; import { ARTIST_SORT_LABELS, type ArtistSort } from '@/lib/artistSort';
import { TRACK_SORT_LABELS, type TrackSort } from '@/lib/trackSort'; import { TRACK_SORT_LABELS, type TrackSort } from '@/lib/trackSort';
import {
DEFAULT_LIBRARY_LAYOUT,
parseLibraryLayout,
type LibraryLayout,
} from '@/library/libraryLayout';
import { useSettingsStore } from './settingsStore'; import { useSettingsStore } from './settingsStore';
type ViewMode = 'tracks' | 'albums' | 'artists' | 'playlists' | 'folders'; type ViewMode = 'tracks' | 'albums' | 'artists' | 'playlists' | 'folders';
@@ -28,6 +33,8 @@ const VIEW_MODE_KEY = 'library_view_mode';
const TRACK_SORT_KEY = 'library_track_sort'; const TRACK_SORT_KEY = 'library_track_sort';
const ALBUM_SORT_KEY = 'library_album_sort'; const ALBUM_SORT_KEY = 'library_album_sort';
const ARTIST_SORT_KEY = 'library_artist_sort'; const ARTIST_SORT_KEY = 'library_artist_sort';
const ALBUM_LAYOUT_KEY = 'library_album_layout';
const ARTIST_LAYOUT_KEY = 'library_artist_layout';
const INCLUDE_COLLAB_ARTISTS_KEY = 'library_include_collab_artists'; const INCLUDE_COLLAB_ARTISTS_KEY = 'library_include_collab_artists';
const PAGE_SIZE = 200; const PAGE_SIZE = 200;
@@ -83,6 +90,8 @@ interface LibraryStore {
trackSort: TrackSort; trackSort: TrackSort;
albumSort: AlbumSort; albumSort: AlbumSort;
artistSort: ArtistSort; artistSort: ArtistSort;
albumLayout: LibraryLayout;
artistLayout: LibraryLayout;
includeCollabArtists: boolean; includeCollabArtists: boolean;
isScanning: boolean; isScanning: boolean;
isCancelling: boolean; isCancelling: boolean;
@@ -120,6 +129,8 @@ interface LibraryStore {
setTrackSort: (sort: TrackSort) => void; setTrackSort: (sort: TrackSort) => void;
setAlbumSort: (sort: AlbumSort) => void; setAlbumSort: (sort: AlbumSort) => void;
setArtistSort: (sort: ArtistSort) => void; setArtistSort: (sort: ArtistSort) => void;
setAlbumLayout: (layout: LibraryLayout) => void;
setArtistLayout: (layout: LibraryLayout) => void;
setIncludeCollabArtists: (include: boolean) => void; setIncludeCollabArtists: (include: boolean) => void;
addFolder: () => Promise<void>; addFolder: () => Promise<void>;
removeFolder: (folderId: number) => Promise<void>; removeFolder: (folderId: number) => Promise<void>;
@@ -407,6 +418,8 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
trackSort: 'title', trackSort: 'title',
albumSort: 'name', albumSort: 'name',
artistSort: 'name', artistSort: 'name',
albumLayout: DEFAULT_LIBRARY_LAYOUT,
artistLayout: DEFAULT_LIBRARY_LAYOUT,
includeCollabArtists: false, includeCollabArtists: false,
isScanning: false, isScanning: false,
isCancelling: false, isCancelling: false,
@@ -441,6 +454,8 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
TRACK_SORT_KEY, TRACK_SORT_KEY,
ALBUM_SORT_KEY, ALBUM_SORT_KEY,
ARTIST_SORT_KEY, ARTIST_SORT_KEY,
ALBUM_LAYOUT_KEY,
ARTIST_LAYOUT_KEY,
INCLUDE_COLLAB_ARTISTS_KEY, INCLUDE_COLLAB_ARTISTS_KEY,
]); ]);
const viewMode = parseViewMode(values[VIEW_MODE_KEY] ?? null); const viewMode = parseViewMode(values[VIEW_MODE_KEY] ?? null);
@@ -452,6 +467,8 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
...(trackSort ? { trackSort } : {}), ...(trackSort ? { trackSort } : {}),
...(albumSort ? { albumSort } : {}), ...(albumSort ? { albumSort } : {}),
...(artistSort ? { artistSort } : {}), ...(artistSort ? { artistSort } : {}),
albumLayout: parseLibraryLayout(values[ALBUM_LAYOUT_KEY] ?? null),
artistLayout: parseLibraryLayout(values[ARTIST_LAYOUT_KEY] ?? null),
includeCollabArtists: values[INCLUDE_COLLAB_ARTISTS_KEY] === 'true', includeCollabArtists: values[INCLUDE_COLLAB_ARTISTS_KEY] === 'true',
}); });
@@ -953,6 +970,18 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
void resetSectionAnchors(); void resetSectionAnchors();
}, },
setAlbumLayout: (albumLayout) => {
if (get().albumLayout === albumLayout) return;
set({ albumLayout, jumpAnchorIndex: 0 });
persistSetting(ALBUM_LAYOUT_KEY, albumLayout);
},
setArtistLayout: (artistLayout) => {
if (get().artistLayout === artistLayout) return;
set({ artistLayout, jumpAnchorIndex: 0 });
persistSetting(ARTIST_LAYOUT_KEY, artistLayout);
},
setIncludeCollabArtists: (includeCollabArtists) => { setIncludeCollabArtists: (includeCollabArtists) => {
anchorGeneration += 1; anchorGeneration += 1;
set({ set({