veza/apps/web/src/features/library/components/library-manager/useLibraryManager.ts
senke f1457e845b feat: frontend pages and feature modules polish
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>
2026-03-23 15:46:21 +01:00

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,
};
}