mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-20 12:40:15 +02:00
initial db rewrite
This commit is contained in:
+319
-130
@@ -1,19 +1,10 @@
|
||||
import { create } from 'zustand';
|
||||
import type { Album, Artist, DbTrack, LibraryFolder } from '@/types/library';
|
||||
import { openLibraryDb } from '@/db/database';
|
||||
import {
|
||||
getAllTracks,
|
||||
getRecentlyPlayedTracks,
|
||||
getSetting,
|
||||
getTrackCount,
|
||||
markTrackPlayed,
|
||||
setSetting,
|
||||
} from '@/db/queries';
|
||||
import { markLocalTracksStaleForRebuild } from '@/db/libraryMaintenance';
|
||||
import { recomputeAlbumIdentity } from '@/library/albumIdentity';
|
||||
import { buildAlbumList } from '@/library/albumSummary';
|
||||
import { ensureArtworkThumbnails } from '@/library/artwork';
|
||||
import { buildArtistList } from '@/library/artistGrouping';
|
||||
AstraLibraryData,
|
||||
type LibrarySectionAnchor,
|
||||
type LibraryStatus,
|
||||
} from '../../modules/astra-library-scanner';
|
||||
import type { Album, Artist, DbTrack, LibraryFolder } from '@/types/library';
|
||||
import {
|
||||
addFolderViaPicker,
|
||||
loadFolders,
|
||||
@@ -26,13 +17,8 @@ import { endScanService, reportScanProgress } from '@/library/scanService';
|
||||
import { ALBUM_SORT_LABELS, type AlbumSort } from '@/lib/albumSort';
|
||||
import { ARTIST_SORT_LABELS, type ArtistSort } from '@/lib/artistSort';
|
||||
import { TRACK_SORT_LABELS, type TrackSort } from '@/lib/trackSort';
|
||||
import { usePlaylistStore } from './playlistStore';
|
||||
import { useSettingsStore } from './settingsStore';
|
||||
|
||||
/**
|
||||
* Library state — SQLite is the source of truth (no persist middleware);
|
||||
* this store mirrors it in memory for the UI plus scan/UI state.
|
||||
*/
|
||||
type ViewMode = 'tracks' | 'albums' | 'artists' | 'playlists' | 'folders';
|
||||
|
||||
const VIEW_MODE_KEY = 'library_view_mode';
|
||||
@@ -40,11 +26,8 @@ const TRACK_SORT_KEY = 'library_track_sort';
|
||||
const ALBUM_SORT_KEY = 'library_album_sort';
|
||||
const ARTIST_SORT_KEY = 'library_artist_sort';
|
||||
const INCLUDE_COLLAB_ARTISTS_KEY = 'library_include_collab_artists';
|
||||
|
||||
// Bump when the album-identity algorithm changes to re-run the whole-library
|
||||
// recompute at startup. '2' = the desktop three-tier grouping port (v15 schema).
|
||||
const ALBUM_GROUPING_VERSION_KEY = 'album_grouping_version';
|
||||
const ALBUM_GROUPING_VERSION = '2';
|
||||
const PAGE_SIZE = 100;
|
||||
const MAX_WINDOW_ITEMS = PAGE_SIZE * 5;
|
||||
|
||||
const VIEW_MODES: readonly ViewMode[] = ['tracks', 'albums', 'artists', 'playlists', 'folders'];
|
||||
|
||||
@@ -64,13 +47,8 @@ function parseArtistSort(value: string | null): ArtistSort | null {
|
||||
return value !== null && value in ARTIST_SORT_LABELS ? (value as ArtistSort) : null;
|
||||
}
|
||||
|
||||
/** Fire-and-forget settings write so view/sort switching stays synchronous. */
|
||||
function persistSetting(key: string, value: string) {
|
||||
void openLibraryDb()
|
||||
.then((db) => setSetting(db, key, value))
|
||||
.catch(() => {
|
||||
// Losing a view preference write is harmless; never surface it.
|
||||
});
|
||||
void AstraLibraryData.setSettings({ [key]: value });
|
||||
}
|
||||
|
||||
export type FolderWithCount = LibraryFolder & { track_count: number };
|
||||
@@ -86,10 +64,14 @@ const IDLE_PROGRESS: ScanProgressState = { phase: 'idle', processed: 0, total: 0
|
||||
|
||||
interface LibraryStore {
|
||||
initialized: boolean;
|
||||
status: LibraryStatus;
|
||||
recoveryNotice: string | null;
|
||||
tracks: DbTrack[];
|
||||
recentlyPlayedTracks: DbTrack[];
|
||||
albums: Album[];
|
||||
artists: Artist[];
|
||||
homeAlbums: Album[];
|
||||
homeArtists: Artist[];
|
||||
folders: FolderWithCount[];
|
||||
totalTrackCount: number;
|
||||
viewMode: ViewMode;
|
||||
@@ -98,11 +80,20 @@ interface LibraryStore {
|
||||
artistSort: ArtistSort;
|
||||
includeCollabArtists: boolean;
|
||||
isScanning: boolean;
|
||||
isPageLoading: boolean;
|
||||
scanProgress: ScanProgressState;
|
||||
scanError: string | null;
|
||||
trackNextCursor: string | null;
|
||||
albumNextCursor: string | null;
|
||||
artistNextCursor: string | null;
|
||||
sectionAnchors: LibrarySectionAnchor[];
|
||||
|
||||
initialize: () => Promise<void>;
|
||||
refresh: () => Promise<void>;
|
||||
loadNextTracks: () => Promise<void>;
|
||||
loadNextAlbums: () => Promise<void>;
|
||||
loadNextArtists: () => Promise<void>;
|
||||
jumpToSection: (cursor: string) => Promise<void>;
|
||||
recordTrackPlayed: (path: string) => Promise<void>;
|
||||
recomputeArtists: () => void;
|
||||
recomputeAlbums: () => void;
|
||||
@@ -118,36 +109,117 @@ interface LibraryStore {
|
||||
}
|
||||
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let nativeSubscriptionsInstalled = false;
|
||||
|
||||
function appendWindow<T>(
|
||||
current: T[],
|
||||
incoming: T[],
|
||||
key: (item: T) => string
|
||||
): T[] {
|
||||
const known = new Set(current.map(key));
|
||||
const merged = [...current, ...incoming.filter((item) => !known.has(key(item)))];
|
||||
return merged.length > MAX_WINDOW_ITEMS ? merged.slice(merged.length - MAX_WINDOW_ITEMS) : merged;
|
||||
}
|
||||
|
||||
export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
const onProgress = (progress: ScanProgress) => {
|
||||
set({ scanProgress: progress });
|
||||
// Mirror progress into the foreground-service notification (starts it on the
|
||||
// first tick) so a big scan keeps running + stays visible when backgrounded.
|
||||
void reportScanProgress(progress);
|
||||
};
|
||||
|
||||
/** Shared scan wrapper: progress/error state + refresh, scans never overlap. */
|
||||
const readTrackPage = (cursor: string | null) =>
|
||||
AstraLibraryData.getTrackPage<DbTrack>(get().trackSort, cursor, PAGE_SIZE);
|
||||
|
||||
const readAlbumPage = (cursor: string | null) =>
|
||||
AstraLibraryData.getAlbumPage<Album>(
|
||||
get().albumSort,
|
||||
useSettingsStore.getState().includeSingles,
|
||||
cursor,
|
||||
PAGE_SIZE
|
||||
);
|
||||
|
||||
const readArtistPage = (cursor: string | null) =>
|
||||
AstraLibraryData.getArtistPage<Artist>(
|
||||
get().artistSort,
|
||||
useSettingsStore.getState().artistGroupingMode,
|
||||
get().includeCollabArtists,
|
||||
cursor,
|
||||
PAGE_SIZE
|
||||
);
|
||||
|
||||
const resetTracks = async () => {
|
||||
const page = await readTrackPage(null);
|
||||
set({
|
||||
tracks: page.items ?? [],
|
||||
trackNextCursor: page.nextCursor ?? null,
|
||||
totalTrackCount: page.totalCount ?? 0,
|
||||
});
|
||||
};
|
||||
|
||||
const resetAlbums = async () => {
|
||||
const page = await readAlbumPage(null);
|
||||
set({ albums: page.items ?? [], albumNextCursor: page.nextCursor ?? null });
|
||||
};
|
||||
|
||||
const resetArtists = async () => {
|
||||
const page = await readArtistPage(null);
|
||||
set({ artists: page.items ?? [], artistNextCursor: page.nextCursor ?? null });
|
||||
};
|
||||
|
||||
const resetSectionAnchors = async () => {
|
||||
const state = get();
|
||||
const sortable =
|
||||
(state.viewMode === 'tracks' && (state.trackSort === 'artist' || state.trackSort === 'title')) ||
|
||||
(state.viewMode === 'albums' && (state.albumSort === 'artist' || state.albumSort === 'name')) ||
|
||||
(state.viewMode === 'artists' && state.artistSort === 'name');
|
||||
if (!sortable) {
|
||||
set({ sectionAnchors: [] });
|
||||
return;
|
||||
}
|
||||
const sort =
|
||||
state.viewMode === 'tracks'
|
||||
? state.trackSort as 'artist' | 'title'
|
||||
: state.viewMode === 'albums'
|
||||
? state.albumSort as 'artist' | 'name'
|
||||
: 'name';
|
||||
set({
|
||||
sectionAnchors: await AstraLibraryData.getSectionAnchors(
|
||||
state.viewMode as 'tracks' | 'albums' | 'artists',
|
||||
sort,
|
||||
useSettingsStore.getState().includeSingles,
|
||||
useSettingsStore.getState().artistGroupingMode,
|
||||
state.includeCollabArtists
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const runScan = async (scan: () => Promise<ScanResult | null>) => {
|
||||
if (get().isScanning) return;
|
||||
set({ isScanning: true, scanError: null, scanProgress: { ...IDLE_PROGRESS } });
|
||||
try {
|
||||
await scan();
|
||||
} catch (err) {
|
||||
set({ scanError: err instanceof Error ? err.message : String(err) });
|
||||
} catch (error) {
|
||||
set({ scanError: error instanceof Error ? error.message : String(error) });
|
||||
} finally {
|
||||
await get().refresh();
|
||||
set({ isScanning: false, scanProgress: { ...IDLE_PROGRESS } });
|
||||
endScanService();
|
||||
try {
|
||||
await get().refresh();
|
||||
} finally {
|
||||
set({ isScanning: false, scanProgress: { ...IDLE_PROGRESS } });
|
||||
endScanService();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
initialized: false,
|
||||
status: 'initializing',
|
||||
recoveryNotice: null,
|
||||
tracks: [],
|
||||
recentlyPlayedTracks: [],
|
||||
albums: [],
|
||||
artists: [],
|
||||
homeAlbums: [],
|
||||
homeArtists: [],
|
||||
folders: [],
|
||||
totalTrackCount: 0,
|
||||
viewMode: 'albums',
|
||||
@@ -156,140 +228,261 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
artistSort: 'name',
|
||||
includeCollabArtists: false,
|
||||
isScanning: false,
|
||||
isPageLoading: false,
|
||||
scanProgress: { ...IDLE_PROGRESS },
|
||||
scanError: null,
|
||||
trackNextCursor: null,
|
||||
albumNextCursor: null,
|
||||
artistNextCursor: null,
|
||||
sectionAnchors: [],
|
||||
|
||||
initialize: () => {
|
||||
if (!initPromise) {
|
||||
initPromise = (async () => {
|
||||
const db = await openLibraryDb();
|
||||
// Load the persisted grouping mode before the first refresh so the artist
|
||||
// list is built correctly; recompute it whenever the mode changes later.
|
||||
await useSettingsStore.getState().load();
|
||||
useSettingsStore.subscribe((state, prev) => {
|
||||
if (state.artistGroupingMode !== prev.artistGroupingMode) get().recomputeArtists();
|
||||
if (state.includeSingles !== prev.includeSingles) get().recomputeAlbums();
|
||||
});
|
||||
// One-time backfill when the album-identity algorithm changes (e.g. the
|
||||
// desktop three-tier grouping port): settle every track's identity key +
|
||||
// display artist before the first refresh so first paint is grouped right.
|
||||
if ((await getSetting(db, ALBUM_GROUPING_VERSION_KEY)) !== ALBUM_GROUPING_VERSION) {
|
||||
await recomputeAlbumIdentity(db);
|
||||
await setSetting(db, ALBUM_GROUPING_VERSION_KEY, ALBUM_GROUPING_VERSION);
|
||||
}
|
||||
// Restore view preferences before the first render of the library screen.
|
||||
const [
|
||||
savedViewMode,
|
||||
savedTrackSort,
|
||||
savedAlbumSort,
|
||||
savedArtistSort,
|
||||
savedIncludeCollabArtists,
|
||||
] =
|
||||
await Promise.all([
|
||||
getSetting(db, VIEW_MODE_KEY),
|
||||
getSetting(db, TRACK_SORT_KEY),
|
||||
getSetting(db, ALBUM_SORT_KEY),
|
||||
getSetting(db, ARTIST_SORT_KEY),
|
||||
getSetting(db, INCLUDE_COLLAB_ARTISTS_KEY),
|
||||
]);
|
||||
const viewMode = parseViewMode(savedViewMode);
|
||||
const trackSort = parseTrackSort(savedTrackSort);
|
||||
const albumSort = parseAlbumSort(savedAlbumSort);
|
||||
const artistSort = parseArtistSort(savedArtistSort);
|
||||
const status = await AstraLibraryData.initialize();
|
||||
set({
|
||||
...(viewMode ? { viewMode } : null),
|
||||
...(trackSort ? { trackSort } : null),
|
||||
...(albumSort ? { albumSort } : null),
|
||||
...(artistSort ? { artistSort } : null),
|
||||
includeCollabArtists: savedIncludeCollabArtists === 'true',
|
||||
status: status.status,
|
||||
recoveryNotice: status.recoveryNotice,
|
||||
totalTrackCount: status.trackCount,
|
||||
});
|
||||
if (status.status === 'fatalUserData') {
|
||||
set({ initialized: true });
|
||||
return;
|
||||
}
|
||||
await useSettingsStore.getState().load();
|
||||
const values = await AstraLibraryData.getSettings([
|
||||
VIEW_MODE_KEY,
|
||||
TRACK_SORT_KEY,
|
||||
ALBUM_SORT_KEY,
|
||||
ARTIST_SORT_KEY,
|
||||
INCLUDE_COLLAB_ARTISTS_KEY,
|
||||
]);
|
||||
const viewMode = parseViewMode(values[VIEW_MODE_KEY] ?? null);
|
||||
const trackSort = parseTrackSort(values[TRACK_SORT_KEY] ?? null);
|
||||
const albumSort = parseAlbumSort(values[ALBUM_SORT_KEY] ?? null);
|
||||
const artistSort = parseArtistSort(values[ARTIST_SORT_KEY] ?? null);
|
||||
set({
|
||||
...(viewMode ? { viewMode } : {}),
|
||||
...(trackSort ? { trackSort } : {}),
|
||||
...(albumSort ? { albumSort } : {}),
|
||||
...(artistSort ? { artistSort } : {}),
|
||||
includeCollabArtists: values[INCLUDE_COLLAB_ARTISTS_KEY] === 'true',
|
||||
});
|
||||
|
||||
if (!nativeSubscriptionsInstalled) {
|
||||
nativeSubscriptionsInstalled = true;
|
||||
AstraLibraryData.addListener('onLibraryStatus', (next) => {
|
||||
set({
|
||||
status: next.status,
|
||||
recoveryNotice: next.recoveryNotice,
|
||||
totalTrackCount: next.trackCount,
|
||||
});
|
||||
if (next.status === 'rebuilding' && !get().isScanning) {
|
||||
void get().rebuildLocalIndex();
|
||||
}
|
||||
});
|
||||
AstraLibraryData.addListener('onCatalogChanged', () => {
|
||||
void get().refresh();
|
||||
});
|
||||
useSettingsStore.subscribe((next, previous) => {
|
||||
if (next.artistGroupingMode !== previous.artistGroupingMode) {
|
||||
void resetArtists();
|
||||
void resetSectionAnchors();
|
||||
}
|
||||
if (next.includeSingles !== previous.includeSingles) {
|
||||
void resetAlbums();
|
||||
void resetSectionAnchors();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await get().refresh();
|
||||
set({ initialized: true });
|
||||
// One-time recovery: the v3 migration marks tracks stale (mtime = -1)
|
||||
// whose non-ASCII tags were truncated by the pre-fix op-sqlite binding.
|
||||
// Re-extract them now that binding is fixed. Fire-and-forget so startup
|
||||
// isn't blocked; rescan manages its own progress + refresh.
|
||||
const stale = await db.get<{ n: number }>('SELECT COUNT(*) AS n FROM tracks WHERE mtime = -1');
|
||||
if ((stale?.n ?? 0) > 0) {
|
||||
void get().rescan();
|
||||
if (status.status === 'rebuilding' && !get().isScanning) {
|
||||
void get().rebuildLocalIndex();
|
||||
}
|
||||
})().catch((err) => {
|
||||
initPromise = null; // allow retry on genuine failure
|
||||
throw err;
|
||||
})().catch((error) => {
|
||||
initPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
return initPromise;
|
||||
},
|
||||
|
||||
refresh: async () => {
|
||||
const db = await openLibraryDb();
|
||||
const [tracks, folders, totalTrackCount, recentlyPlayedTracks] = await Promise.all([
|
||||
getAllTracks(db),
|
||||
const viewMode = get().viewMode;
|
||||
const [
|
||||
trackPage,
|
||||
albumPage,
|
||||
artistPage,
|
||||
homeAlbumPage,
|
||||
homeArtistPage,
|
||||
folders,
|
||||
recentlyPlayedTracks,
|
||||
] = await Promise.all([
|
||||
viewMode === 'tracks' ? readTrackPage(null) : Promise.resolve(null),
|
||||
viewMode === 'albums' ? readAlbumPage(null) : Promise.resolve(null),
|
||||
viewMode === 'artists' ? readArtistPage(null) : Promise.resolve(null),
|
||||
AstraLibraryData.getAlbumPage<Album>(
|
||||
'recently_added',
|
||||
useSettingsStore.getState().includeSingles,
|
||||
null,
|
||||
20
|
||||
),
|
||||
AstraLibraryData.getArtistPage<Artist>(
|
||||
'name',
|
||||
useSettingsStore.getState().artistGroupingMode,
|
||||
get().includeCollabArtists,
|
||||
null,
|
||||
50
|
||||
),
|
||||
loadFolders(),
|
||||
getTrackCount(db),
|
||||
getRecentlyPlayedTracks(db),
|
||||
AstraLibraryData.getRecentlyPlayed<DbTrack>(20),
|
||||
]);
|
||||
set({
|
||||
...(trackPage ? {
|
||||
tracks: trackPage.items ?? [],
|
||||
trackNextCursor: trackPage.nextCursor ?? null,
|
||||
totalTrackCount: trackPage.totalCount ?? get().totalTrackCount,
|
||||
} : {}),
|
||||
...(albumPage ? {
|
||||
albums: albumPage.items ?? [],
|
||||
albumNextCursor: albumPage.nextCursor ?? null,
|
||||
} : {}),
|
||||
...(artistPage ? {
|
||||
artists: artistPage.items ?? [],
|
||||
artistNextCursor: artistPage.nextCursor ?? null,
|
||||
} : {}),
|
||||
homeAlbums: homeAlbumPage.items ?? [],
|
||||
homeArtists: homeArtistPage.items ?? [],
|
||||
folders,
|
||||
recentlyPlayedTracks,
|
||||
});
|
||||
await resetSectionAnchors();
|
||||
},
|
||||
|
||||
loadNextTracks: async () => {
|
||||
const cursor = get().trackNextCursor;
|
||||
if (!cursor || get().isPageLoading) return;
|
||||
set({ isPageLoading: true });
|
||||
try {
|
||||
await ensureArtworkThumbnails(tracks.map((track) => track.artwork_hash));
|
||||
} catch {
|
||||
// Missing thumbnails should not prevent the library itself from loading.
|
||||
const page = await readTrackPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetTracks();
|
||||
set((state) => ({
|
||||
tracks: appendWindow(state.tracks, page.items, (track) => track.path),
|
||||
trackNextCursor: page.nextCursor,
|
||||
}));
|
||||
} finally {
|
||||
set({ isPageLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadNextAlbums: async () => {
|
||||
const cursor = get().albumNextCursor;
|
||||
if (!cursor || get().isPageLoading) return;
|
||||
set({ isPageLoading: true });
|
||||
try {
|
||||
const page = await readAlbumPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetAlbums();
|
||||
set((state) => ({
|
||||
albums: appendWindow(state.albums, page.items, (album) => album.identity_key),
|
||||
albumNextCursor: page.nextCursor,
|
||||
}));
|
||||
} finally {
|
||||
set({ isPageLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
loadNextArtists: async () => {
|
||||
const cursor = get().artistNextCursor;
|
||||
if (!cursor || get().isPageLoading) return;
|
||||
set({ isPageLoading: true });
|
||||
try {
|
||||
const page = await readArtistPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetArtists();
|
||||
set((state) => ({
|
||||
artists: appendWindow(state.artists, page.items, (artist) => artist.artist),
|
||||
artistNextCursor: page.nextCursor,
|
||||
}));
|
||||
} finally {
|
||||
set({ isPageLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
jumpToSection: async (cursor) => {
|
||||
const state = get();
|
||||
set({ isPageLoading: true });
|
||||
try {
|
||||
if (state.viewMode === 'tracks') {
|
||||
const page = await readTrackPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetTracks();
|
||||
set({
|
||||
tracks: page.items,
|
||||
trackNextCursor: page.nextCursor,
|
||||
totalTrackCount: page.totalCount,
|
||||
});
|
||||
} else if (state.viewMode === 'albums') {
|
||||
const page = await readAlbumPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetAlbums();
|
||||
set({ albums: page.items, albumNextCursor: page.nextCursor });
|
||||
} else if (state.viewMode === 'artists') {
|
||||
const page = await readArtistPage(cursor);
|
||||
if (page.error === 'STALE_REVISION') return resetArtists();
|
||||
set({ artists: page.items, artistNextCursor: page.nextCursor });
|
||||
}
|
||||
} finally {
|
||||
set({ isPageLoading: false });
|
||||
}
|
||||
// Album + artist lists are derived in JS: albums for the desktop-parity
|
||||
// display picks + singles eligibility, artists to honor the grouping mode.
|
||||
const settings = useSettingsStore.getState();
|
||||
const albums = buildAlbumList(tracks, { includeSingles: settings.includeSingles });
|
||||
const artists = buildArtistList(tracks, settings.artistGroupingMode);
|
||||
set({ tracks, recentlyPlayedTracks, albums, artists, folders, totalTrackCount });
|
||||
// Playlist counts/missing states depend on tracks — keep them in step.
|
||||
await usePlaylistStore.getState().refresh();
|
||||
},
|
||||
|
||||
recordTrackPlayed: async (path) => {
|
||||
const db = await openLibraryDb();
|
||||
const recorded = await markTrackPlayed(db, path);
|
||||
if (!recorded) return;
|
||||
const recentlyPlayedTracks = await getRecentlyPlayedTracks(db);
|
||||
set({ recentlyPlayedTracks });
|
||||
await AstraLibraryData.recordTrackPlayed(path);
|
||||
set({ recentlyPlayedTracks: await AstraLibraryData.getRecentlyPlayed<DbTrack>(20) });
|
||||
},
|
||||
|
||||
// Rebuild the artist list from in-memory tracks (e.g. on grouping-mode change),
|
||||
// without re-querying SQLite.
|
||||
recomputeArtists: () =>
|
||||
set((state) => ({
|
||||
artists: buildArtistList(state.tracks, useSettingsStore.getState().artistGroupingMode),
|
||||
})),
|
||||
recomputeArtists: () => {
|
||||
void resetArtists();
|
||||
},
|
||||
|
||||
// Rebuild the album list from in-memory tracks (e.g. on singles-toggle change).
|
||||
recomputeAlbums: () =>
|
||||
set((state) => ({
|
||||
albums: buildAlbumList(state.tracks, {
|
||||
includeSingles: useSettingsStore.getState().includeSingles,
|
||||
}),
|
||||
})),
|
||||
recomputeAlbums: () => {
|
||||
void resetAlbums();
|
||||
},
|
||||
|
||||
setViewMode: (viewMode) => {
|
||||
set({ viewMode });
|
||||
persistSetting(VIEW_MODE_KEY, viewMode);
|
||||
if (viewMode === 'tracks' && get().tracks.length === 0) void resetTracks();
|
||||
if (viewMode === 'albums' && get().albums.length === 0) void resetAlbums();
|
||||
if (viewMode === 'artists' && get().artists.length === 0) void resetArtists();
|
||||
void resetSectionAnchors();
|
||||
},
|
||||
|
||||
setTrackSort: (trackSort) => {
|
||||
set({ trackSort });
|
||||
set({ trackSort, tracks: [], trackNextCursor: null });
|
||||
persistSetting(TRACK_SORT_KEY, trackSort);
|
||||
void resetTracks();
|
||||
void resetSectionAnchors();
|
||||
},
|
||||
|
||||
setAlbumSort: (albumSort) => {
|
||||
set({ albumSort });
|
||||
set({ albumSort, albums: [], albumNextCursor: null });
|
||||
persistSetting(ALBUM_SORT_KEY, albumSort);
|
||||
void resetAlbums();
|
||||
void resetSectionAnchors();
|
||||
},
|
||||
|
||||
setArtistSort: (artistSort) => {
|
||||
set({ artistSort });
|
||||
set({ artistSort, artists: [], artistNextCursor: null });
|
||||
persistSetting(ARTIST_SORT_KEY, artistSort);
|
||||
void resetArtists();
|
||||
void resetSectionAnchors();
|
||||
},
|
||||
|
||||
setIncludeCollabArtists: (includeCollabArtists) => {
|
||||
set({ includeCollabArtists });
|
||||
set({ includeCollabArtists, artists: [], artistNextCursor: null });
|
||||
persistSetting(INCLUDE_COLLAB_ARTISTS_KEY, includeCollabArtists ? 'true' : 'false');
|
||||
void resetArtists();
|
||||
void resetSectionAnchors();
|
||||
},
|
||||
|
||||
addFolder: () => runScan(() => addFolderViaPicker({ onProgress })),
|
||||
@@ -303,10 +496,6 @@ export const useLibraryStore = create<LibraryStore>((set, get) => {
|
||||
|
||||
rescan: () => runScan(() => rescanAll({ callbacks: { onProgress } })),
|
||||
|
||||
rebuildLocalIndex: () => runScan(async () => {
|
||||
const db = await openLibraryDb();
|
||||
await markLocalTracksStaleForRebuild(db);
|
||||
return rescanAll({ callbacks: { onProgress } });
|
||||
}),
|
||||
rebuildLocalIndex: () => runScan(() => rescanAll({ mode: 'full', callbacks: { onProgress } })),
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user