Regression a7dcdd5651
"usePrevious" uses "useEffect" to update the ref while the fix of the commit updated the state during the render function call.
This causes the component to not actually render and immediately call the render function again. Which then leads to the "usePrevious" hooks "useEffect" to never getting called.
264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
/*
|
|
* 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<typeof batchUpdateSourceMetadata>[0];
|
|
}> & {
|
|
selectedLanguages: string[];
|
|
languages: string[];
|
|
sources: (SourceIdInfo &
|
|
SourceLanguageInfo &
|
|
SourceNameInfo &
|
|
SourceDisplayNameInfo &
|
|
SourceIconInfo &
|
|
SourceMetaInfo)[];
|
|
}) => {
|
|
const { t } = useLingui();
|
|
|
|
const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState<Record<SourceIdInfo['id'], boolean>>({});
|
|
|
|
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 (
|
|
<Dialog fullWidth maxWidth="xs" open={isVisible} onClose={onDismiss} onTransitionExited={onExitComplete}>
|
|
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
|
<DialogContent dividers sx={{ padding: 0 }}>
|
|
{!languages.length && <Box sx={{ p: 1 }}>{t`No sources installed`}</Box>}
|
|
<GroupedVirtuoso
|
|
style={{
|
|
height: languagesSortedBySelectState.length * 54,
|
|
minHeight: '25vh',
|
|
maxHeight: '50vh',
|
|
}}
|
|
groupCounts={groupCounts}
|
|
increaseViewportBy={400}
|
|
computeItemKey={computeItemKey}
|
|
groupContent={(index) => {
|
|
const language = languagesSortedBySelectState[index];
|
|
const isEnabled = tmpSelectedLanguages.includes(language);
|
|
|
|
return (
|
|
<ListItem
|
|
sx={{
|
|
backgroundColor: 'background.paper',
|
|
backgroundImage: 'var(--Paper-overlay)',
|
|
}}
|
|
>
|
|
<ListItemText primary={translateExtensionLanguage(language)} />
|
|
<Switch
|
|
checked={isEnabled}
|
|
onChange={(e) => handleChange(language, e.target.checked)}
|
|
/>
|
|
</ListItem>
|
|
);
|
|
}}
|
|
itemContent={(index) => {
|
|
const source = flattenedSourcesByLanguages[index];
|
|
|
|
return (
|
|
<ListItem sx={{ pl: 3 }}>
|
|
<ListItemAvatar sx={{ minWidth: 32, mr: 1 }}>
|
|
<ListCardAvatar
|
|
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
|
alt={source.name}
|
|
slots={{
|
|
avatarProps: {
|
|
sx: {
|
|
width: 32,
|
|
height: 32,
|
|
},
|
|
},
|
|
spinnerImageProps: {
|
|
ignoreQueue: true,
|
|
},
|
|
}}
|
|
/>
|
|
</ListItemAvatar>
|
|
<ListItemText primary={source.name} />
|
|
<Checkbox
|
|
checked={
|
|
tmpSourceIdToEnabledState[source.id] ?? getSourceMetadata(source).isEnabled
|
|
}
|
|
onChange={(e) =>
|
|
setTmpSourceIdToEnabledState({
|
|
...tmpSourceIdToEnabledState,
|
|
[source.id]: e.target.checked,
|
|
})
|
|
}
|
|
/>
|
|
</ListItem>
|
|
);
|
|
}}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button autoFocus onClick={onDismiss} color="primary">
|
|
{t`Cancel`}
|
|
</Button>
|
|
<Button onClick={handleOk} color="primary">
|
|
{t`Ok`}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export const SourceLanguageSelect = ({
|
|
setSelectedLanguages,
|
|
...props
|
|
}: {
|
|
selectedLanguages: string[];
|
|
setSelectedLanguages: (languages: string[]) => Promise<void>;
|
|
languages: string[];
|
|
sources: (SourceIdInfo &
|
|
SourceLanguageInfo &
|
|
SourceNameInfo &
|
|
SourceDisplayNameInfo &
|
|
SourceIconInfo &
|
|
SourceMetaInfo)[];
|
|
}) => {
|
|
const { t } = useLingui();
|
|
|
|
return (
|
|
<CustomTooltip title={t`Settings`}>
|
|
<IconButton
|
|
onClick={async () => {
|
|
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"
|
|
>
|
|
<FilterListIcon />
|
|
</IconButton>
|
|
</CustomTooltip>
|
|
);
|
|
};
|