import { unwrap } from '@pipu/core'; import { useQuery } from '@tanstack/react-query'; import { Link, Outlet, useNavigate } from '@tanstack/react-router'; import { useTranslation } from 'react-i18next'; import { useAuthState, useSession, useUser } from '../app/session'; import { useCollectionCounts } from '../entities/collection'; import { auditFillIcon, auditIcon, groupFillIcon, groupIcon, homeFillIcon, homeIcon, libraryFillIcon, libraryIcon, logoutIcon, historyFillIcon, historyIcon, likeFillIcon, likeIcon, dislikeFillIcon, dislikeIcon, settingsIcon, storageFillIcon, storageIcon, inboxFillIcon, inboxIcon, myUploadsFillIcon, myUploadsIcon, uploadFillIcon, uploadIcon, tuneFillIcon, tuneIcon} from '../shared/ui/icons'; import { Avatar, cx, Icon, Logo, Menu } from '../shared/ui/ui'; import { LanguageSwitch } from './LanguageSwitch'; import { PlayerProvider, usePlayerState } from '../player/context'; import { Ambient } from './Ambient'; import { PlayerBar } from './PlayerBar'; import { SearchBox } from './SearchBox'; import s from './AppShell.module.css'; type NavPath = | '/' | '/admin/users' | '/admin/audit' | '/admin/sources' | '/admin/library' | '/admin/uploads' | '/uploads/new' | '/uploads/mine' | '/admin/settings' | '/collection/history' | '/collection/likes' | '/collection/dislikes'; function NavLink({ to, label, icon, activeIcon, count, badge, }: { to: NavPath; label: string; icon: string; activeIcon: string; count?: number; // счётчик справа (лайки, дизлайки) badge?: number; // яркий бейдж (ждут решения) }) { return ( {({ isActive }) => ( <> {label} {badge ? {badge > 99 ? '99+' : badge} : null} {!badge && count ? {count} : null} )} ); } /** Между выходом и редиректом на /login каркас не рисуется: пользователя уже нет. */ export function AppShell() { return useAuthState().status === 'signed_in' ? ( ) : null; } function Shell() { const { t } = useTranslation(); const user = useUser(); const session = useSession(); const navigate = useNavigate(); const name = user.display_name ?? user.username; const staff = user.role !== 'user'; const island = user.ui_prefs.web_player === 'island'; const counts = useCollectionCounts(); const pending = usePendingUploads(user.role === 'admin'); return (
{user.ui_prefs.ambient !== false && }
} header={
{name}
@{user.username} · {t(`roles.${user.role}`)}
} items={[ { label: t('profile.settings'), icon: settingsIcon, onSelect: () => void navigate({ to: '/settings' }) }, { label: t('profile.logout'), icon: logoutIcon, danger: true, onSelect: () => void session.logout() }, ]} />
); } /** Третье пятно фона — в цвете обложки текущего трека. */ function ShellAmbient() { const track = usePlayerState().track; return ; } /** Файлов на рассмотрении — бейдж «Очереди» у админа (docs/uploads.md#10). */ function usePendingUploads(enabled: boolean) { const session = useSession(); const q = useQuery({ queryKey: ['admin-uploads-pending'], enabled, queryFn: async () => unwrap( await session.client.GET('/api/v1/admin/uploads', { params: { query: { status: ['pending_review'], limit: 100 } } }), ).items.length, refetchInterval: 60_000, }); return q.data; }