import type { components } from '@pipu/api-client'; import { unwrap } from '@pipu/core'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession, useUser } from '../app/session'; import { useErrorText } from '../shared/lib/errors'; import { Button, Dialog, Field, FormError, Icon } from '../shared/ui/ui'; import { playlistAddIcon } from '../shared/ui/icons'; import s from './catalog.module.css'; export type Playlist = components['schemas']['Playlist']; export type PlaylistInput = components['schemas']['PlaylistInput']; export function usePlaylists(enabled = true) { const session = useSession(); return useQuery({ queryKey: ['playlists'], enabled, queryFn: async () => unwrap(await session.client.GET('/api/v1/me/playlists')) }); } export function usePlaylist(id: string, shared = false) { const session = useSession(); const user = useUser(); return useQuery({ queryKey: shared ? ['shared-playlists', id, user.id] : ['playlists', id], staleTime: 0, queryFn: async () => shared ? unwrap(await session.client.GET('/api/v1/shared/playlists/{shareId}', { params: { path: { shareId: id } } })) : unwrap(await session.client.GET('/api/v1/me/playlists/{id}', { params: { path: { id } } })) }); } export function playlistInput(p: Playlist): PlaylistInput & { revision: number } { return { name: p.name, description: p.description, track_ids: p.tracks.map(t => t.id), revision: p.revision }; } export function PlaylistEditor({ playlist, trackIds = [], onClose, onSaved }: { playlist?: Playlist; trackIds?: string[]; onClose: () => void; onSaved: (p: Playlist) => void; }) { const { t } = useTranslation(); const session = useSession(); const cache = useQueryClient(); const errorText = useErrorText(); const [original] = useState(playlist); const [name, setName] = useState(playlist?.name ?? ''); const [description, setDescription] = useState(playlist?.description ?? ''); const save = useMutation({ mutationFn: async () => { const body = { name: name.trim(), description, track_ids: original?.tracks.map(t => t.id) ?? trackIds }; return original ? unwrap(await session.client.PUT('/api/v1/me/playlists/{id}', { params: { path: { id: original.id } }, body: { ...body, revision: original.revision } })) : unwrap(await session.client.POST('/api/v1/me/playlists', { body })); }, onSuccess: p => { cache.setQueryData(['playlists', p.id], p); void cache.invalidateQueries({ queryKey: ['playlists'] }); onSaved(p); }, onError: () => { void cache.invalidateQueries({ queryKey: ['playlists'] }); } }); const close = useCallback(() => { if (!save.isPending) onClose(); }, [save.isPending, onClose]); return { e.preventDefault(); if (!save.isPending && name.trim()) save.mutate(); }}> {save.error ? errorText(save.error) : null} {t('playlists.save')} {t('form.cancel')} ; } /** Mounted only while open, so catalog rows do not each subscribe to the playlist list. */ const PlaylistPicker = memo(function PlaylistPicker({ trackIds, onClose }: { trackIds: string[]; onClose: () => void }) { const { t } = useTranslation(); const session = useSession(); const cache = useQueryClient(); const errorText = useErrorText(); const list = usePlaylists(); const [create, setCreate] = useState(false); const add = useMutation({ mutationFn: async (id: string) => { const p = unwrap(await session.client.GET('/api/v1/me/playlists/{id}', { params: { path: { id } } })); const body = playlistInput(p); body.track_ids = [...new Set([...body.track_ids, ...trackIds])]; return unwrap(await session.client.PUT('/api/v1/me/playlists/{id}', { params: { path: { id } }, body })); }, onSuccess: () => { void cache.invalidateQueries({ queryKey: ['playlists'] }); onClose(); } }); if (create) return setCreate(false)} onSaved={onClose} />; return { if (!add.isPending) onClose(); }}> {list.error || add.error ? errorText(list.error ?? add.error) : null} {list.isPending && {t('playlists.loading')}} {list.isSuccess && list.data.items.length === 0 && {t('playlists.empty')}} {list.data?.items.map(p => add.mutate(p.id)}>{p.name} ยท {p.track_count})} setCreate(true)}>{t('playlists.create')} ; }); export function AddToPlaylist({ trackIds, compact = false, className }: { trackIds: string[]; compact?: boolean; className?: string }) { const { t } = useTranslation(); const [selectedIds, setSelectedIds] = useState(null); const close = useCallback(() => setSelectedIds(null), []); return <> {compact ? setSelectedIds([...trackIds])}> : setSelectedIds([...trackIds])}>{t('playlists.add')}} {selectedIds && } >; }
{t('playlists.loading')}
{t('playlists.empty')}