Files
suwayomi-material-you-webui/src/features/browse/sources/Sources.tsx

179 lines
6.9 KiB
TypeScript
Raw Normal View History

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
2021-01-26 23:32:12 +03:30
* 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/.
*/
2021-01-26 23:32:12 +03:30
2025-06-23 00:36:32 +02:00
import { useCallback, useMemo } from 'react';
import IconButton from '@mui/material/IconButton';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import { useNavigate } from 'react-router-dom';
2025-06-23 00:36:32 +02:00
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';
2025-08-16 15:58:27 +02:00
import { SourceCard } from '@/features/browse/sources/components/SourceCard.tsx';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
2024-10-05 16:09:34 +02:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
2025-08-15 22:02:58 +02:00
import { isPinnedOrLastUsedSource, translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
2024-12-21 23:27:03 +01:00
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
2025-08-15 22:02:58 +02:00
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';
2025-06-23 00:36:32 +02:00
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
import { StyledGroupHeader } from '@/base/components/virtuoso/StyledGroupHeader.tsx';
import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx';
2025-08-15 22:02:58 +02:00
import { SourceLanguageSelect } from '@/features/source/components/SourceLanguageSelect.tsx';
2021-03-09 16:44:09 +03:30
2025-06-23 00:36:32 +02:00
export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useLingui();
2021-03-09 16:44:09 +03:30
const { languages: shownLangs, setLanguages: setShownLangs } = SourceService.useLanguages();
2025-04-21 19:18:42 +02:00
const {
2025-05-17 23:37:00 +02:00
settings: { showNsfw, lastUsedSourceId },
2025-04-21 19:18:42 +02:00
} = useMetadataServerSettings();
2021-03-09 16:44:09 +03:30
2024-04-27 22:28:55 +02:00
const {
data,
loading: isLoading,
error,
refetch,
} = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true });
2023-09-01 20:29:19 +02:00
const sources = data?.sources.nodes;
2025-04-21 17:46:19 +02:00
const filteredSources = useMemo(
() =>
SourceService.filter(sources ?? [], {
showNsfw,
languages: shownLangs,
keepLocalSource: true,
enabled: true,
}),
2025-04-21 17:46:19 +02:00
[sources, shownLangs],
);
const sourcesForLanguageFilter = useMemo(
() =>
SourceService.filter(sources ?? [], {
showNsfw,
keepLocalSource: false,
}),
[sources],
);
2025-05-17 23:37:00 +02:00
const sourcesByLanguage = useMemo(() => {
const lastUsedSource = SourceService.getLastUsedSource(lastUsedSourceId, filteredSources);
const groupedByLanguageTuple = Object.entries(
SourceService.groupByLanguage(filteredSources, { withPinnedGroup: true }),
);
2025-05-17 23:37:00 +02:00
if (lastUsedSource) {
return [
[DefaultLanguage.LAST_USED_SOURCE, [lastUsedSource]],
...groupedByLanguageTuple,
] satisfies typeof groupedByLanguageTuple;
}
return groupedByLanguageTuple;
}, [filteredSources]);
2021-01-19 14:47:07 +03:30
const sourceLanguages = useMemo(
() => SourceService.getLanguages(sourcesForLanguageFilter, { excludeLocalSource: true }),
[sources],
);
2025-04-21 17:46:19 +02:00
const areSourcesFromDifferentRepos = useMemo(
() => SourceService.areFromMultipleRepos(filteredSources),
[filteredSources],
);
2025-06-23 00:36:32 +02:00
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();
2025-05-03 13:24:55 +02:00
useAppAction(
<>
<CustomTooltip title={t`Global Search`}>
<IconButton onClick={() => navigate(AppRoutes.sources.childRoutes.searchAll.path())} color="inherit">
2025-05-03 13:24:55 +02:00
<TravelExploreIcon />
</IconButton>
</CustomTooltip>
<SourceLanguageSelect
2025-05-03 13:24:55 +02:00
selectedLanguages={shownLangs}
setSelectedLanguages={setShownLangs}
languages={sourceLanguages}
sources={sourcesForLanguageFilter}
2025-05-03 13:24:55 +02:00
/>
</>,
[t, shownLangs, sourceLanguages, sourcesForLanguageFilter],
2025-05-03 13:24:55 +02:00
);
2021-03-09 16:44:09 +03:30
if (isLoading) return <LoadingPlaceholder />;
2021-01-19 14:47:07 +03:30
2024-04-27 22:28:55 +02:00
if (error) {
return (
2024-04-29 15:29:33 +02:00
<EmptyViewAbsoluteCentered
message={t`Unable to load data`}
2024-12-21 23:27:03 +01:00
messageExtra={getErrorMessage(error)}
2024-04-27 22:28:55 +02:00
retry={() => refetch().catch(defaultPromiseErrorHandler('Sources::refetch'))}
/>
);
}
if (sources?.length === 0) {
return <EmptyViewAbsoluteCentered message={t`No sources found. Install Some extensions first.`} />;
2021-01-19 14:47:07 +03:30
}
2021-03-09 16:44:09 +03:30
return (
2025-06-23 00:36:32 +02:00
<StyledGroupedVirtuoso
persistKey="sources"
heightToSubtract={tabsMenuHeight}
overscan={window.innerHeight * 0.5}
groupCounts={groupCounts}
computeItemKey={computeItemKey}
groupContent={(index) => {
const [language] = sourcesByLanguage[index];
return (
<StyledGroupHeader isFirstItem={!index}>
<Typography variant="h5" component="h2">
{translateExtensionLanguage(language)}
</Typography>
</StyledGroupHeader>
);
}}
itemContent={(index, groupIndex) => {
const language = sourcesByLanguage[groupIndex][0];
2025-06-23 00:36:32 +02:00
const source = visibleSources[index];
return (
<StyledGroupItemWrapper>
<SourceCard
source={source}
showSourceRepo={areSourcesFromDifferentRepos}
showLanguage={isPinnedOrLastUsedSource(language)}
/>
2025-06-23 00:36:32 +02:00
</StyledGroupItemWrapper>
);
}}
/>
2021-03-09 16:44:09 +03:30
);
2021-01-19 14:47:07 +03:30
}