/* * 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, useState } from 'react'; import Button from '@mui/material/Button'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import Dialog from '@mui/material/Dialog'; import Switch from '@mui/material/Switch'; import IconButton from '@mui/material/IconButton'; import FilterListIcon from '@mui/icons-material/FilterList'; import ListItemText from '@mui/material/ListItemText'; import ListItem from '@mui/material/ListItem'; import { GroupedVirtuoso } from 'react-virtuoso'; import ListItemAvatar from '@mui/material/ListItemAvatar'; import { useLingui } from '@lingui/react/macro'; import Checkbox from '@mui/material/Checkbox'; import Box from '@mui/material/Box'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts'; import type { SourceDisplayNameInfo, SourceIconInfo, SourceIdInfo, SourceLanguageInfo, SourceMetaInfo, SourceNameInfo, } from '@/features/source/Source.types.ts'; import { Sources } from '@/features/source/services/Sources'; import { batchUpdateSourceMetadata, getSourceMetadata } from '@/features/source/services/SourceMetadata.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx'; import { makeToast } from '@/base/utils/Toast.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx'; import { AwaitableComponent, type AwaitableComponentProps } from 'awaitable-component'; const SourceLanguageSelectDialog = ({ isVisible, onDismiss, onSubmit, onExitComplete, selectedLanguages, languages, sources, }: AwaitableComponentProps<{ selectedLanguages: string[]; sourceEnabledStateMetaUpdatePayload: Parameters[0]; }> & { selectedLanguages: string[]; languages: string[]; sources: (SourceIdInfo & SourceLanguageInfo & SourceNameInfo & SourceDisplayNameInfo & SourceIconInfo & SourceMetaInfo)[]; }) => { const { t } = useLingui(); const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState>({}); const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages)); const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]); const languagesSortedBySelectState = useMemo( () => toUniqueLanguageCodes([ ...tmpSelectedLanguages .filter((language) => languages.includes(language)) .toSorted(languageSortComparator), ...languages.toSorted(languageSortComparator), ]), [languages, tmpSelectedLanguages], ); const flattenedSourcesByLanguages = useMemo( () => languagesSortedBySelectState.flatMap((language) => sourcesByLanguage[language] ?? []), [languagesSortedBySelectState, sourcesByLanguage], ); const groupCounts = useMemo( () => languagesSortedBySelectState.map((language) => { const isEnabled = tmpSelectedLanguages .filter((selectedLanguage) => languages.includes(selectedLanguage)) .includes(language); if (!isEnabled) { return 0; } return sourcesByLanguage[language].length; }), [sourcesByLanguage, languagesSortedBySelectState, languages], ); const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey( groupCounts, useCallback((index) => languagesSortedBySelectState[index], [languagesSortedBySelectState]), useCallback((index) => flattenedSourcesByLanguages[index].id, [flattenedSourcesByLanguages]), ); const handleOk = () => { onSubmit({ selectedLanguages: toUniqueLanguageCodes(tmpSelectedLanguages), sourceEnabledStateMetaUpdatePayload: Object.entries(tmpSourceIdToEnabledState) .map(([sourceId, enabled]) => { const source = sources.find((sourceToEnable) => sourceToEnable.id === sourceId); if (!source) { return null; } return { sources: [source], entries: [{ metadataKey: 'isEnabled' as const, value: enabled }], }; }) .filter((entry) => entry !== null), }); }; const handleChange = (language: string, selected: boolean) => { if (selected) { setTmpSelectedLanguages([...tmpSelectedLanguages, language]); } else { setTmpSelectedLanguages(tmpSelectedLanguages.toSpliced(tmpSelectedLanguages.indexOf(language), 1)); } }; return ( {t`Allowed Languages`} {!languages.length && {t`No sources installed`}} { const language = languagesSortedBySelectState[index]; const isEnabled = tmpSelectedLanguages.includes(language); return ( handleChange(language, e.target.checked)} /> ); }} itemContent={(index) => { const source = flattenedSourcesByLanguages[index]; return ( setTmpSourceIdToEnabledState({ ...tmpSourceIdToEnabledState, [source.id]: e.target.checked, }) } /> ); }} /> ); }; export const SourceLanguageSelect = ({ setSelectedLanguages, ...props }: { selectedLanguages: string[]; setSelectedLanguages: (languages: string[]) => Promise; languages: string[]; sources: (SourceIdInfo & SourceLanguageInfo & SourceNameInfo & SourceDisplayNameInfo & SourceIconInfo & SourceMetaInfo)[]; }) => { const { t } = useLingui(); return ( { try { const { selectedLanguages: updatedSelectedLanguages, sourceEnabledStateMetaUpdatePayload } = await AwaitableComponent.show(SourceLanguageSelectDialog, props); await Promise.all([ setSelectedLanguages(updatedSelectedLanguages), batchUpdateSourceMetadata(sourceEnabledStateMetaUpdatePayload), ]).catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e))); } catch (e) { // Ignore } }} aria-label="display more actions" edge="end" color="inherit" > ); };