diff --git a/src/modules/source/Source.types.ts b/src/modules/source/Source.types.ts index eedc82bf..befd2a45 100644 --- a/src/modules/source/Source.types.ts +++ b/src/modules/source/Source.types.ts @@ -7,6 +7,7 @@ */ import { + ExtensionType, GetSourceBrowseQuery, GetSourceSettingsQuery, SourcePreferenceChangeInput, @@ -60,3 +61,7 @@ export type EditTextPreferenceProps = PreferenceProps & ExtractByKeyValue; export type SourceIdInfo = Pick; +export type SourceLanguageInfo = Pick; +export type SourceDisplayNameInfo = Pick; +export type SourceNsfwInfo = Pick; +export type SourceRepoInfo = { extension: Pick }; diff --git a/src/modules/source/screens/Sources.tsx b/src/modules/source/screens/Sources.tsx index 7f0e1538..03a4b499 100644 --- a/src/modules/source/screens/Sources.tsx +++ b/src/modules/source/screens/Sources.tsx @@ -15,48 +15,18 @@ import { useTranslation } from 'react-i18next'; import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; -import { sourceDefualtLangs, langSortCmp, DefaultLanguage } from '@/modules/core/utils/Languages.ts'; +import { sourceDefualtLangs } from '@/modules/core/utils/Languages.ts'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { SourceCard } from '@/modules/source/components/SourceCard.tsx'; import { LangSelect } from '@/modules/core/components/inputs/LangSelect.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import { SourceType } from '@/lib/graphql/generated/graphql.ts'; import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts'; import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx'; import { Sources as SourceService } from '@/modules/source/services/Sources.ts'; -function sourceToLangList(sources: Pick[]) { - const result = new Set(); - - sources.forEach((source) => { - const lang = SourceService.isLocalSource(source) ? DefaultLanguage.OTHER : source.lang; - - result.add(lang); - }); - - return [...result].sort(langSortCmp); -} - -function groupByLang>( - sources: Source[], -): Record { - const result: Record = {}; - - sources.forEach((source) => { - const lang = SourceService.isLocalSource(source) ? DefaultLanguage.OTHER : source.lang; - - result[lang] ??= []; - result[lang].push(source); - }); - - Object.values(result).forEach((langSources) => langSources.sort((a, b) => a.name.localeCompare(b.name))); - - return result; -} - export function Sources() { const { t } = useTranslation(); const { setAction } = useNavBarContext(); @@ -71,15 +41,20 @@ export function Sources() { refetch, } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true }); const sources = data?.sources.nodes; + const filteredSources = useMemo( + () => SourceService.filter(sources ?? [], { showNsfw, languages: shownLangs, keepLocalSource: true }), + [sources, shownLangs], + ); + const sourcesByLanguageTuple = useMemo( + () => Object.entries(SourceService.groupByLanguage(filteredSources)), + [filteredSources], + ); - const areSourcesFromDifferentRepos = useMemo(() => { - if (!sources || !!sources?.length) { - return false; - } - - const { repo } = sources[0].extension; - return sources.some((source) => source.extension.repo !== repo); - }, [sources]); + const sourceLanguages = useMemo(() => SourceService.getLanguages(sources ?? []), [sources]); + const areSourcesFromDifferentRepos = useMemo( + () => SourceService.areFromMultipleRepos(filteredSources), + [filteredSources], + ); const navigate = useNavigate(); @@ -91,18 +66,14 @@ export function Sources() { - + , ); return () => { setAction(null); }; - }, [t, shownLangs, sources]); + }, [t, shownLangs, sourceLanguages]); if (isLoading) return ; @@ -122,50 +93,26 @@ export function Sources() { return ( <> - {Object.entries(groupByLang(sources ?? [])) - .sort((a, b) => langSortCmp(a[0], b[0])) - .map( - ([lang, list]) => - (lang === DefaultLanguage.OTHER || shownLangs.includes(lang)) && ( - - - {translateExtensionLanguage(lang)} - - {list - .filter((source) => { - const isLangOther = lang === DefaultLanguage.OTHER; - if (isLangOther) { - const isLocalSource = SourceService.isLocalSource(source); - const isLangShown = shownLangs.includes(lang); - - const isLangOtherSourceShown = isLangShown || isLocalSource; - if (!isLangOtherSourceShown) { - return false; - } - } - - return showNsfw || !source.isNsfw; - }) - .map((source) => ( - - ))} - - ), - )} + {sourcesByLanguageTuple.map(([languages, sourcesOfLanguage]) => ( + + + {translateExtensionLanguage(languages)} + + {sourcesOfLanguage.map((source) => ( + + ))} + + ))} ); } diff --git a/src/modules/source/services/Sources.ts b/src/modules/source/services/Sources.ts index e118e0b5..baa4dd0d 100644 --- a/src/modules/source/services/Sources.ts +++ b/src/modules/source/services/Sources.ts @@ -6,7 +6,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { SourceIdInfo } from '@/modules/source/Source.types.ts'; +import { + SourceDisplayNameInfo, + SourceIdInfo, + SourceLanguageInfo, + SourceNsfwInfo, + SourceRepoInfo, +} from '@/modules/source/Source.types.ts'; +import { langSortCmp } from '@/modules/core/utils/Languages.ts'; export class Sources { static readonly LOCAL_SOURCE_ID = '0'; @@ -14,4 +21,47 @@ export class Sources { static isLocalSource(source: SourceIdInfo): boolean { return source.id === Sources.LOCAL_SOURCE_ID; } + + static getLanguages(sources: SourceLanguageInfo[]): string[] { + return [...new Set(sources.map((source) => source.lang))].toSorted(langSortCmp); + } + + static groupByLanguage( + sources: Source[], + ): Record { + const sourcesByLanguage = Object.groupBy(sources, (source) => source.lang); + const sourcesBySortedLanguage = Object.entries(sourcesByLanguage).toSorted(([a], [b]) => langSortCmp(a, b)); + const sortedSourcesBySortedLanguage = sourcesBySortedLanguage.map(([language, sourcesOfLanguage]) => [ + language, + (sourcesOfLanguage ?? []).toSorted((a, b) => a.displayName.localeCompare(b.displayName)), + ]); + + return Object.fromEntries(sortedSourcesBySortedLanguage); + } + + static filter( + sources: Source[], + { + showNsfw, + languages, + keepLocalSource, + }: { showNsfw?: boolean; languages?: string[]; keepLocalSource?: boolean } = {}, + ): Source[] { + return sources + .filter((source) => showNsfw || !source.isNsfw || (keepLocalSource && Sources.isLocalSource(source))) + .filter( + (source) => + !languages || languages.includes(source.lang) || (keepLocalSource && Sources.isLocalSource(source)), + ); + } + + static areFromMultipleRepos(sources: Source[]): boolean { + const repo = sources.find((source) => !!source.extension.repo)?.extension.repo; + + if (!repo || !sources.length) { + return false; + } + + return sources.some((source) => source.extension.repo !== repo && !Sources.isLocalSource(source)); + } }