import { useEffect, useRef } from 'react' import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query' import { Link, useLocation, useParams, useSearchParams } from 'react-router-dom' import { getFeed, getSavedCounts, getSyncStatus, listCategories, listChannels } from '../api/client' import ChannelActivityList from '../components/ChannelActivityList' import Icon from '../components/Icon' import VideoCard from '../components/VideoCard' function VideoSkeleton() { return } function Feed() { const location = useLocation() const { categoryId } = useParams<{ categoryId: string }>() const [searchParams] = useSearchParams() const search = location.pathname === '/search' ? searchParams.get('q')?.trim() ?? '' : '' const categoryNumber = categoryId && /^\d+$/.test(categoryId) ? Number(categoryId) : undefined const isLocal = location.pathname === '/local' const isUncategorized = location.pathname === '/uncategorized' const supportsViewToggle = !isLocal && location.pathname !== '/search' const isChannelsView = supportsViewToggle && searchParams.get('view') === 'channels' const viewQuery = isChannelsView ? 'view=channels' : '' const withView = (path: string) => viewQuery ? `${path}?${viewQuery}` : path const newOnly = categoryNumber !== undefined && searchParams.get('new') === '1' const localCategoryRaw = isLocal ? searchParams.get('category') : null const localCategory = localCategoryRaw && /^\d+$/.test(localCategoryRaw) ? Number(localCategoryRaw) : undefined const localUncategorized = isLocal && searchParams.get('uncategorized') === 'true' const queryClient = useQueryClient() const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: listCategories }) const category = categoriesQuery.data?.find((item) => item.id === categoryNumber) const savedCountsQuery = useQuery({ queryKey: ['feed', 'saved-counts'], queryFn: getSavedCounts, enabled: isLocal }) const countQuery = useQuery({ queryKey: ['channels', isUncategorized ? 'uncategorized-count' : 'all-for-counts'], queryFn: () => listChannels({ subscribed: true, uncategorized: isUncategorized }), enabled: isUncategorized || (!categoryNumber && !isLocal && !search), }) const feedQuery = useInfiniteQuery({ queryKey: ['feed', location.pathname, search, localCategory, localUncategorized, newOnly], queryFn: ({ pageParam }) => getFeed({ categoryId: isLocal ? localCategory : categoryNumber, uncategorized: isUncategorized || localUncategorized, downloaded: isLocal, search, newOnly, cursor: pageParam, }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined, enabled: (!categoryId || categoryNumber !== undefined) && !isChannelsView, }) const syncStatusQuery = useQuery({ queryKey: ['sync-status'], queryFn: getSyncStatus, refetchInterval: (query) => query.state.data?.videos.running ? 2000 : 60_000, }) const syncRunning = Boolean(syncStatusQuery.data?.subscriptions.running || syncStatusQuery.data?.videos.running) const finishedAt = syncStatusQuery.data?.videos.finished_at const previousFinished = useRef(finishedAt) useEffect(() => { if (finishedAt && previousFinished.current !== finishedAt) { queryClient.invalidateQueries({ queryKey: ['feed'] }) queryClient.invalidateQueries({ queryKey: ['channel-activity'] }) } previousFinished.current = finishedAt }, [finishedAt, queryClient]) const subscriptionsFinished = syncStatusQuery.data?.subscriptions.finished_at const previousSubscriptionsFinished = useRef(subscriptionsFinished) useEffect(() => { if (subscriptionsFinished && previousSubscriptionsFinished.current !== subscriptionsFinished) { queryClient.invalidateQueries({ queryKey: ['channels'] }) queryClient.invalidateQueries({ queryKey: ['channel-activity'] }) } previousSubscriptionsFinished.current = subscriptionsFinished }, [subscriptionsFinished, queryClient]) const title = search ? `Поиск: ${search}` : isLocal ? 'На сервере' : isUncategorized ? 'Без категории' : categoryId ? category?.name ?? 'Категория' : 'Все видео' const subtitle = newOnly ? 'Только новые видео' : search ? 'Результаты в вашей ленте' : isLocal ? 'Видео, которые вы сохранили для просмотра' : isUncategorized ? `${countQuery.data?.length ?? '…'} каналов пока без категории` : category ? `${category.channel_count} каналов в категории` : isChannelsView ? 'Каналы, отсортированные по свежести видео' : `${countQuery.data?.length ?? '…'} подписок в вашей ленте` const items = feedQuery.data?.pages.flatMap((page) => page.items) ?? [] useEffect(() => { if (isChannelsView || feedQuery.isLoading) return const key = `feed-scroll:${location.pathname + location.search}` const saved = sessionStorage.getItem(key) if (saved) { sessionStorage.removeItem(key) requestAnimationFrame(() => window.scrollTo(0, Number(saved))) } }, [isChannelsView, feedQuery.isLoading, location.pathname, location.search]) const hasInvalidCategory = Boolean(categoryId && !categoryNumber) return

Моя лента

{title}

{subtitle}{newOnly && <> · Показать все}

{supportsViewToggle &&
Лента Каналы
}
{isLocal && } {syncStatusQuery.data?.videos.status === 'failed' &&
Последнее обновление видео завершилось ошибкой. Попробуйте позже.
} {location.pathname === '/' && (countQuery.data?.length ?? 0) > 0 && categoriesQuery.data?.length === 0 &&
Каналы уже в ленте. Разложи их по темам, чтобы быстрее находить видео.Настроить категории
} {hasInvalidCategory || (categoryId && categoriesQuery.data && !category) ?

Категория не найдена

Ко всем видео
: isChannelsView ? : <> {feedQuery.isLoading && } {feedQuery.isError &&

Не удалось загрузить видео

Проверь соединение и попробуй ещё раз.

} {!feedQuery.isLoading && !feedQuery.isError && items.length > 0 && } {!feedQuery.isLoading && !feedQuery.isError && items.length === 0 &&

{syncRunning ? 'Загружаем новые видео' : search ? 'Ничего не найдено' : isLocal ? 'Пока нет сохранённых видео' : isUncategorized ? 'Все каналы распределены' : 'Здесь пока нет видео'}

{syncRunning ? 'Подписки и лента обновляются. Видео появятся здесь автоматически.' : search ? 'Попробуй другой запрос.' : isLocal ? 'Нажми «Скачать» у понравившегося ролика, и он появится здесь.' : 'Добавь каналы в категорию или обнови ленту.'}

{!search && !isLocal && !syncRunning && Перейти к каналам}
} {feedQuery.hasNextPage && } }
} export default Feed