/* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { useCallback, useMemo } from 'react'; import IconButton from '@mui/material/IconButton'; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import { useNavigate } from 'react-router-dom'; import Typography from '@mui/material/Typography'; import { useLingui } from '@lingui/react/macro'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { DefaultLanguage } from '@/base/utils/Languages.ts'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { SourceCard } from '@/features/browse/sources/components/SourceCard.tsx'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { isPinnedOrLastUsedSource, translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; import { AppRoutes } from '@/base/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { Sources as SourceService } from '@/features/source/services/Sources.ts'; import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts'; import { StyledGroupedVirtuoso } from '@/base/components/virtuoso/StyledGroupedVirtuoso.tsx'; import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { StyledGroupHeader } from '@/base/components/virtuoso/StyledGroupHeader.tsx'; import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx'; export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) { const { t } = useLingui(); const { languages: shownLangs, setLanguages: setShownLangs } = SourceService.useLanguages(); const { settings: { showNsfw, lastUsedSourceId }, } = useMetadataServerSettings(); const { data, loading: isLoading, error, refetch, } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true }); const sources = data?.sources.nodes; const filteredSources = useMemo( () => SourceService.filter(sources ?? [], { showNsfw, languages: shownLangs, keepLocalSource: true, enabled: true, }), [sources, shownLangs], ); const sourcesForLanguageFilter = useMemo( () => SourceService.filter(sources ?? [], { showNsfw, keepLocalSource: false, }), [sources], ); const sourcesByLanguage = useMemo(() => { const lastUsedSource = SourceService.getLastUsedSource(lastUsedSourceId, filteredSources); const groupedByLanguageTuple = Object.entries( SourceService.groupByLanguage(filteredSources, { withPinnedGroup: true }), ); if (lastUsedSource) { return [ [DefaultLanguage.LAST_USED_SOURCE, [lastUsedSource]], ...groupedByLanguageTuple, ] satisfies typeof groupedByLanguageTuple; } return groupedByLanguageTuple; }, [filteredSources]); const sourceLanguages = useMemo( () => SourceService.getLanguages(sourcesForLanguageFilter, { excludeLocalSource: true }), [sources], ); const areSourcesFromDifferentRepos = useMemo( () => SourceService.areFromMultipleRepos(filteredSources), [filteredSources], ); const visibleSources = useMemo( () => sourcesByLanguage.map(([, sourcesOfLanguage]) => sourcesOfLanguage).flat(1), [sourcesByLanguage], ); const groupCounts = useMemo( () => sourcesByLanguage.map((sourceGroup) => sourceGroup[1].length), [sourcesByLanguage], ); const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( groupCounts, useCallback((index) => sourcesByLanguage[index][0], [sourcesByLanguage]), useCallback( (index, groupIndex) => `${sourcesByLanguage[groupIndex][0]}_${visibleSources[index].id}`, [visibleSources], ), ); const navigate = useNavigate(); useAppAction( <> navigate(AppRoutes.sources.childRoutes.searchAll.path())} color="inherit"> , [t, shownLangs, sourceLanguages, sourcesForLanguageFilter], ); if (isLoading) return ; if (error) { return ( refetch().catch(defaultPromiseErrorHandler('Sources::refetch'))} /> ); } if (sources?.length === 0) { return ; } return ( { const [language] = sourcesByLanguage[index]; return ( {translateExtensionLanguage(language)} ); }} itemContent={(index, groupIndex) => { const language = sourcesByLanguage[groupIndex][0]; const source = visibleSources[index]; return ( ); }} /> ); }