import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Link, NavLink, useLocation, useNavigate } from 'react-router-dom' import { getSyncStatus, listCategories, syncSubscriptions, syncVideos } from '../api/client' import { formatRelativeTime } from '../utils/format' import Icon from './Icon' function SyncIndicator() { const [open, setOpen] = useState(false) const ref = useRef(null) const queryClient = useQueryClient() const { data } = useQuery({ queryKey: ['sync-status'], queryFn: getSyncStatus, refetchInterval: (query) => query.state.data?.videos.running || query.state.data?.subscriptions.running ? 2000 : 60_000, }) const syncAllMutation = useMutation({ mutationFn: async () => { const ignoreInProgress = (error: unknown) => error instanceof Error && error.message.includes('already in progress') try { await syncSubscriptions() } catch (error) { if (!ignoreInProgress(error)) throw error } try { await syncVideos() } catch (error) { if (!ignoreInProgress(error)) throw error } }, onMutate: () => { queryClient.invalidateQueries({ queryKey: ['sync-status'] }) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['sync-status'] }) }, }) useEffect(() => { if (!open) return const close = (event: MouseEvent) => { if (!ref.current?.contains(event.target as Node)) setOpen(false) } document.addEventListener('mousedown', close) return () => document.removeEventListener('mousedown', close) }, [open]) const running = data?.videos.running || data?.subscriptions.running const busy = syncAllMutation.isPending || running const failed = data?.videos.status === 'failed' || data?.subscriptions.status === 'failed' const recent = [data?.videos.finished_at, data?.subscriptions.finished_at].filter((value): value is string => Boolean(value)).sort().at(-1) const label = running ? 'Обновление...' : failed ? 'Ошибка синхронизации' : recent ? `Обновлено ${formatRelativeTime(recent)}` : 'Синхронизация' return
{open &&
Синхронизация

Подписки: {data?.subscriptions.running ? 'обновляются' : data?.subscriptions.finished_at ? formatRelativeTime(data.subscriptions.finished_at) : 'ещё не обновлялись'}

Видео: {data?.videos.running ? 'обновляются' : data?.videos.finished_at ? formatRelativeTime(data.videos.finished_at) : 'ещё не обновлялись'}

{failed &&

Последнее обновление завершилось ошибкой. Подробности в настройках.

} {syncAllMutation.isError &&

Не удалось запустить обновление. Попробуйте позже.

}
}
} function Sidebar({ close }: { close: () => void }) { const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: listCategories }) const categories = categoriesQuery.data ?? [] const location = useLocation() // Keep the current view mode ("Лента | Каналы") when switching between // "Все видео" and category links; badges (?new=1) are feed-specific. const viewQuery = new URLSearchParams(location.search).get('view') === 'channels' ? '?view=channels' : '' const links = [ { to: `/${viewQuery}`, icon: 'home' as const, text: 'Все видео', end: true }, { to: '/local', icon: 'server' as const, text: 'На сервере' }, ] return } function SearchForm({ initialValue }: { initialValue: string }) { const [search, setSearch] = useState(initialValue) const navigate = useNavigate() return
{ event.preventDefault(); navigate(search.trim() ? `/search?q=${encodeURIComponent(search.trim())}` : '/') }}> setSearch(event.target.value)} /> {search && } } export default function AppShell({ children }: { children: React.ReactNode }) { const [drawerOpen, setDrawerOpen] = useState(false) const drawerCloseRef = useRef(null) const location = useLocation() const initialSearch = location.pathname === '/search' ? new URLSearchParams(location.search).get('q') ?? '' : '' useEffect(() => { document.body.classList.toggle('drawer-open', drawerOpen) if (drawerOpen) drawerCloseRef.current?.focus() const onEscape = (event: KeyboardEvent) => { if (event.key === 'Escape') setDrawerOpen(false) } if (drawerOpen) document.addEventListener('keydown', onEscape) else document.removeEventListener('keydown', onEscape) return () => { document.body.classList.remove('drawer-open'); document.removeEventListener('keydown', onEscape) } }, [drawerOpen]) return
MyTube
{drawerOpen &&
setDrawerOpen(false)} />
{children}
}