Files
Psychotoxical-psysonic/src/hooks/useAlbumOfflineState.ts
T
Frank Stellmacher ff99b10faa refactor(album-detail): I.7 — split AlbumDetail.tsx 511 → 369 LOC across 5 files (#679)
* refactor(album-detail): extract sanitizeFilename + useAlbumDetailData

Move sanitizeFilename into utils/albumDetailHelpers.ts. Pull the album +
related-albums fetch and the starred state seeds (isStarred,
starredSongs) into hooks/useAlbumDetailData.ts.

AlbumDetail.tsx: 511 → 483 LOC.

* refactor(album-detail): extract useAlbumOfflineState hook

Move the four primitive-selector offline status reads (cache map +
job-status filters + progress totals) into hooks/useAlbumOfflineState.ts.
Keeps the re-render minimisation comment with the code that needs it.

AlbumDetail.tsx: 483 → 461 LOC.

* refactor(album-detail): extract useAlbumDetailSort hook

Pull sortKey/Dir/clickCount state, the 3-click natural-reset cycle in
handleSort, and the displayedSongs memo (filter + sort comparator) into
hooks/useAlbumDetailSort.ts. Rating comparator keeps the same priority
chain as the row renderer.

AlbumDetail.tsx: 461 → 414 LOC.

* refactor(album-detail): extract AlbumDetailToolbar subcomponent

Pull the search input + bulk-action cluster (selection count, add-to-
playlist popover, clear-selection button) into
components/albumDetail/AlbumDetailToolbar.tsx. Parent retains showPlPicker
to coordinate the popover close with selection clears.

AlbumDetail.tsx: 414 → 369 LOC.
2026-05-14 01:19:58 +02:00

52 lines
2.3 KiB
TypeScript

import { useOfflineStore } from '../store/offlineStore';
import { useOfflineJobStore } from '../store/offlineJobStore';
interface UseAlbumOfflineStateResult {
resolvedOfflineStatus: 'none' | 'downloading' | 'cached';
offlineProgress: { done: number; total: number } | null;
}
/**
* Combined offline-cache status for an album. Splits the read across
* three primitive Zustand selectors so the page only re-renders when
* one of the resolved scalars (status / done count / total count)
* actually flips — not on every `jobs` array mutation during batch
* downloads (each track flip would otherwise trigger a full page render).
*
* Resolution rules:
* - If there's any queued / downloading job for this album, status is
* `downloading` and we expose a `{ done, total }` progress tuple.
* - Else we look at the persisted cache map: a fully-cached album is
* one where every trackId in the album-meta has a matching track entry.
* - Else `none`.
*
* `albumId` is allowed to be empty (e.g. while the page is still
* fetching) — in that case every selector short-circuits to a benign
* default.
*/
export function useAlbumOfflineState(albumId: string, serverId: string): UseAlbumOfflineStateResult {
const offlineStatus = useOfflineStore((s): 'none' | 'downloading' | 'cached' => {
if (!albumId) return 'none';
const meta = s.albums[`${serverId}:${albumId}`];
const isDownloaded = meta && meta.trackIds.length > 0 && meta.trackIds.every(tid => !!s.tracks[`${serverId}:${tid}`]);
return isDownloaded ? 'cached' : 'none';
});
const isOfflineDownloading = useOfflineJobStore(s =>
!!albumId && s.jobs.some(j => j.albumId === albumId && (j.status === 'queued' || j.status === 'downloading')),
);
const offlineProgressDone = useOfflineJobStore(s => {
if (!albumId) return 0;
return s.jobs.filter(j => j.albumId === albumId && (j.status === 'done' || j.status === 'error')).length;
});
const offlineProgressTotal = useOfflineJobStore(s => {
if (!albumId) return 0;
return s.jobs.filter(j => j.albumId === albumId).length;
});
const resolvedOfflineStatus = isOfflineDownloading ? 'downloading' : offlineStatus;
const offlineProgress = offlineProgressTotal > 0
? { done: offlineProgressDone, total: offlineProgressTotal }
: null;
return { resolvedOfflineStatus, offlineProgress };
}