Update dashboard (stats, recent tracks/activity), discover, distribution, education, feed, subscription, support, search, settings, live, cloud, analytics, auth, chat, social, tracks, playlists, presence, upload, and library manager. Consistent UI patterns and error handling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
149 lines
4.3 KiB
TypeScript
149 lines
4.3 KiB
TypeScript
/**
|
|
* LibraryManager — fetch, state, handlers.
|
|
*/
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { apiClient } from '@/services/api/client';
|
|
import type { Track as ApiTrack } from '@/features/tracks/types/track';
|
|
import type { Track as PlayerTrack } from '@/features/player/types';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import { logger } from '@/utils/logger';
|
|
import { parseApiError } from '@/utils/apiErrorHandler';
|
|
|
|
export interface UseLibraryManagerOverrides {
|
|
tracksOverride?: ApiTrack[] | null;
|
|
isLoadingOverride?: boolean;
|
|
errorOverride?: string | null;
|
|
}
|
|
|
|
export function useLibraryManager(
|
|
onTrackSelect?: (track: ApiTrack) => void,
|
|
overrides?: UseLibraryManagerOverrides,
|
|
) {
|
|
const [tracks, setTracks] = useState<ApiTrack[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
|
const [filterType, setFilterType] = useState<string>('all');
|
|
const [pagination, setPagination] = useState({
|
|
page: 1,
|
|
limit: 20,
|
|
total: 0,
|
|
});
|
|
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
|
const [playingTrackId, setPlayingTrackId] = useState<string | null>(null);
|
|
const { info: showInfo } = useToast();
|
|
|
|
const fetchTracks = useCallback(async () => {
|
|
try {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
const response = await apiClient.get<{
|
|
data: ApiTrack[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
}>('/tracks', {
|
|
params: {
|
|
page: pagination.page,
|
|
limit: pagination.limit,
|
|
search: searchQuery || undefined,
|
|
artist: filterType !== 'all' ? filterType : undefined,
|
|
},
|
|
});
|
|
const data = response.data;
|
|
setTracks(data.data ?? []);
|
|
setPagination((prev) => ({
|
|
...prev,
|
|
total: data.total ?? 0,
|
|
}));
|
|
} catch (err: unknown) {
|
|
const apiError = parseApiError(err);
|
|
setError(apiError.message);
|
|
logger.error('Error fetching tracks:', { message: apiError.message });
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [pagination.page, pagination.limit, searchQuery, filterType]);
|
|
|
|
useEffect(() => {
|
|
if (overrides?.tracksOverride !== undefined) {
|
|
setTracks(overrides.tracksOverride ?? []);
|
|
setPagination((p) => ({ ...p, total: (overrides.tracksOverride ?? []).length }));
|
|
setIsLoading(false);
|
|
setError(overrides.errorOverride ?? null);
|
|
return;
|
|
}
|
|
if (overrides?.errorOverride != null) {
|
|
setError(overrides.errorOverride);
|
|
setTracks([]);
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
fetchTracks();
|
|
}, [fetchTracks, overrides?.tracksOverride, overrides?.errorOverride]);
|
|
|
|
const handleUploadComplete = useCallback(() => {
|
|
fetchTracks();
|
|
}, [fetchTracks]);
|
|
|
|
const handleEditTrack = useCallback(
|
|
(_track: PlayerTrack) => {
|
|
showInfo('Edit functionality coming soon — available in the next release');
|
|
},
|
|
[showInfo],
|
|
);
|
|
|
|
const handlePlayTrack = useCallback(
|
|
(track: PlayerTrack) => {
|
|
setPlayingTrackId(track.id);
|
|
const originalTrack = tracks.find((t) => t.id === track.id);
|
|
if (originalTrack) {
|
|
onTrackSelect?.(originalTrack);
|
|
showInfo(`Now playing: ${track.title} by ${track.artist}`);
|
|
}
|
|
},
|
|
[tracks, onTrackSelect, showInfo],
|
|
);
|
|
|
|
const mapToPlayerTrack = useCallback((track: ApiTrack): PlayerTrack => {
|
|
const t = track as ApiTrack & {
|
|
album?: string;
|
|
cover_art_path?: string;
|
|
stream_manifest_url?: string;
|
|
genre?: string;
|
|
};
|
|
return {
|
|
id: track.id,
|
|
title: track.title,
|
|
artist: track.artist,
|
|
album: t.album,
|
|
duration: track.duration,
|
|
url: t.stream_manifest_url || `/api/v1/tracks/${track.id}/download`,
|
|
cover: t.cover_art_path,
|
|
genre: t.genre,
|
|
};
|
|
}, []);
|
|
|
|
const playerTracks = tracks.map(mapToPlayerTrack);
|
|
|
|
return {
|
|
tracks,
|
|
isLoading,
|
|
error,
|
|
searchQuery,
|
|
setSearchQuery,
|
|
viewMode,
|
|
setViewMode,
|
|
filterType,
|
|
setFilterType,
|
|
pagination,
|
|
isUploadModalOpen,
|
|
setIsUploadModalOpen,
|
|
playingTrackId,
|
|
playerTracks,
|
|
handleUploadComplete,
|
|
handleEditTrack,
|
|
handlePlayTrack,
|
|
};
|
|
}
|