Project started 🥂

This commit is contained in:
2026-06-02 01:13:22 +03:00
commit 612d0f0125
146 changed files with 15242 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { Tabs, TabsList, TabsContent, SearchField, ScrollArea, Card } from 'modern-sk';
import { useGetTracksQuery, useGetAlbumsQuery, useGetArtistsQuery } from '../../api/endpoints/library';
import { TrackRow } from '../../components/track/TrackRow';
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
import { EmptyState } from '../../components/common/EmptyState';
import { ErrorState } from '../../components/common/ErrorState';
import { useAppDispatch } from '../../hooks/useAppDispatch';
import { setQueue } from '../../store/slices/queue';
import type { Track, Album, Artist } from '../../api/types';
import { getCoverUrl } from '../../api/endpoints/streaming';
import { formatDuration } from '../../lib/format';
export function LibraryPage() {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const [tab, setTab] = useState('tracks');
const [search, setSearch] = useState('');
const tracksQuery = useGetTracksQuery(search ? { search } : undefined);
const albumsQuery = useGetAlbumsQuery(search ? { search } : undefined);
const artistsQuery = useGetArtistsQuery(search ? { search } : undefined);
const handlePlayAll = (tracks: Track[]) => {
dispatch(setQueue({
entries: tracks.map((t) => ({
trackId: t.id,
title: t.title,
artistName: t.artistName,
albumTitle: t.albumTitle,
durationMs: t.durationMs,
albumArtUrl: t.albumArtUrl,
})),
source: 'manual',
sourceName: 'Library',
}));
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div style={{ padding: '1.25rem 1.5rem', borderBottom: '1px solid var(--color-border)', display: 'flex', alignItems: 'center', gap: '1rem', flexShrink: 0 }}>
<h2 style={{ margin: 0, fontSize: '1.125rem', fontWeight: 700 }}>Library</h2>
<div style={{ flex: 1, maxWidth: '20rem' }}>
<SearchField
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search library…"
icon="⌕"
/>
</div>
</div>
<Tabs value={tab} onValueChange={setTab} style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ padding: '0 1.5rem', borderBottom: '1px solid var(--color-border)', flexShrink: 0 }}>
<TabsList items={[{ value: 'tracks', label: 'Tracks' }, { value: 'albums', label: 'Albums' }, { value: 'artists', label: 'Artists' }]} />
</div>
<TabsContent value="tracks" style={{ flex: 1, overflow: 'hidden' }}>
<ScrollArea style={{ height: '100%' }}>
{tracksQuery.isLoading && <LoadingSkeleton rows={12} />}
{tracksQuery.isError && <ErrorState onRetry={() => tracksQuery.refetch()} />}
{tracksQuery.data && tracksQuery.data.items.length === 0 && (
<EmptyState icon="♫" title="No tracks" description="Your library is empty. Start by downloading some music." />
)}
{tracksQuery.data && tracksQuery.data.items.length > 0 && (() => {
const data = tracksQuery.data!;
return (
<div>
<div style={{ padding: '0.5rem 0.75rem', display: 'flex', gap: '0.5rem', alignItems: 'center', borderBottom: '1px solid var(--color-border)' }}>
<button
onClick={() => handlePlayAll(data.items)}
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--color-accent)', fontSize: '0.875rem', fontWeight: 500 }}
>
Play all ({data.total})
</button>
</div>
{data.items.map((track, i) => (
<TrackRow key={track.id} track={track} index={i} showAlbum />
))}
</div>
);
})()}
</ScrollArea>
</TabsContent>
<TabsContent value="albums" style={{ flex: 1, overflow: 'hidden' }}>
<ScrollArea style={{ height: '100%' }}>
{albumsQuery.isLoading && <LoadingSkeleton rows={8} height={72} />}
{albumsQuery.isError && <ErrorState onRetry={() => albumsQuery.refetch()} />}
{albumsQuery.data && albumsQuery.data.items.length === 0 && (
<EmptyState icon="💿" title="No albums" description="No albums in library." />
)}
{albumsQuery.data && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(10rem, 1fr))', gap: '1rem', padding: '1.25rem 1.5rem' }}>
{albumsQuery.data.items.map((album) => (
<AlbumCard key={album.id} album={album} onClick={() => void navigate(`/library/albums/${album.id}`)} />
))}
</div>
)}
</ScrollArea>
</TabsContent>
<TabsContent value="artists" style={{ flex: 1, overflow: 'hidden' }}>
<ScrollArea style={{ height: '100%' }}>
{artistsQuery.isLoading && <LoadingSkeleton rows={8} />}
{artistsQuery.isError && <ErrorState onRetry={() => artistsQuery.refetch()} />}
{artistsQuery.data && artistsQuery.data.items.length === 0 && (
<EmptyState icon="🎤" title="No artists" description="No artists in library." />
)}
{artistsQuery.data && (
<div style={{ padding: '0.5rem 0' }}>
{artistsQuery.data.items.map((artist) => (
<ArtistRow key={artist.id} artist={artist} />
))}
</div>
)}
</ScrollArea>
</TabsContent>
</Tabs>
</div>
);
}
function AlbumCard({ album, onClick }: { album: Album; onClick: () => void }) {
const artUrl = getCoverUrl(album.artUrl);
return (
<Card
onClick={onClick}
style={{ cursor: 'pointer', padding: '0.75rem', display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
>
{artUrl ? (
<img src={artUrl} alt={album.title} style={{ width: '100%', aspectRatio: '1', objectFit: 'cover', borderRadius: 6 }} />
) : (
<div style={{ width: '100%', aspectRatio: '1', background: 'var(--color-surface-3)', borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '2rem' }}>💿</div>
)}
<div>
<div style={{ fontWeight: 600, fontSize: '0.8125rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{album.title}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--color-text-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{album.artistName}</div>
<div style={{ fontSize: '0.6875rem', color: 'var(--color-text-3)' }}>{album.trackCount} tracks · {formatDuration(album.totalDurationMs)}</div>
</div>
</Card>
);
}
function ArtistRow({ artist }: { artist: Artist }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', padding: '0.5rem 1.5rem' }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', background: 'var(--color-surface-3)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, fontSize: '1.25rem' }}>🎤</div>
<div>
<div style={{ fontWeight: 500, fontSize: '0.875rem' }}>{artist.name}</div>
<div style={{ fontSize: '0.75rem', color: 'var(--color-text-3)' }}>{artist.albumCount} albums · {artist.trackCount} tracks</div>
</div>
</div>
);
}