2025-06-23 02:55:39 +02:00
|
|
|
/*
|
|
|
|
|
* 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 { 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 { Virtuoso } from 'react-virtuoso';
|
|
|
|
|
import Box from '@mui/material/Box';
|
|
|
|
|
import ListItemAvatar from '@mui/material/ListItemAvatar';
|
2026-01-10 19:45:02 +01:00
|
|
|
import { useLingui } from '@lingui/react/macro';
|
2026-01-14 23:41:58 +01:00
|
|
|
import Checkbox from '@mui/material/Checkbox';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
|
2025-08-15 22:02:58 +02:00
|
|
|
import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts';
|
2026-01-14 16:33:37 +01:00
|
|
|
import { languageSortComparator, toUniqueLanguageCodes } from '@/base/utils/Languages.ts';
|
2025-06-23 02:55:39 +02:00
|
|
|
import {
|
|
|
|
|
SourceDisplayNameInfo,
|
|
|
|
|
SourceIconInfo,
|
|
|
|
|
SourceIdInfo,
|
|
|
|
|
SourceLanguageInfo,
|
|
|
|
|
SourceMetaInfo,
|
|
|
|
|
SourceNameInfo,
|
2025-08-15 22:02:58 +02:00
|
|
|
} from '@/features/source/Source.types.ts';
|
|
|
|
|
import { Sources } from '@/features/source/services/Sources';
|
|
|
|
|
import { getSourceMetadata, updateSourceMetadata } from '@/features/source/services/SourceMetadata.ts';
|
2025-06-23 02:55:39 +02:00
|
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
2025-08-16 02:48:46 +02:00
|
|
|
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
|
|
|
|
|
import { makeToast } from '@/base/utils/Toast.ts';
|
2025-06-23 02:55:39 +02:00
|
|
|
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
|
|
|
|
|
|
|
|
|
|
export const SourceLanguageSelect = ({
|
|
|
|
|
selectedLanguages,
|
|
|
|
|
setSelectedLanguages,
|
|
|
|
|
languages,
|
|
|
|
|
sources,
|
|
|
|
|
}: {
|
|
|
|
|
selectedLanguages: string[];
|
|
|
|
|
setSelectedLanguages: (languages: string[]) => void;
|
|
|
|
|
languages: string[];
|
|
|
|
|
sources: (SourceIdInfo &
|
|
|
|
|
SourceLanguageInfo &
|
|
|
|
|
SourceNameInfo &
|
|
|
|
|
SourceDisplayNameInfo &
|
|
|
|
|
SourceIconInfo &
|
|
|
|
|
SourceMetaInfo)[];
|
|
|
|
|
}) => {
|
2026-01-10 19:45:02 +01:00
|
|
|
const { t } = useLingui();
|
2025-06-23 02:55:39 +02:00
|
|
|
|
|
|
|
|
const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState<Record<SourceIdInfo['id'], boolean>>({});
|
|
|
|
|
|
2026-01-14 16:33:37 +01:00
|
|
|
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
|
2025-06-23 02:55:39 +02:00
|
|
|
const [open, setOpen] = useState<boolean>(false);
|
|
|
|
|
|
|
|
|
|
const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]);
|
|
|
|
|
|
|
|
|
|
const languagesSortedBySelectState = useMemo(
|
|
|
|
|
() =>
|
2026-01-14 16:33:37 +01:00
|
|
|
toUniqueLanguageCodes([
|
2025-06-23 02:55:39 +02:00
|
|
|
...tmpSelectedLanguages.toSorted(languageSortComparator),
|
|
|
|
|
...languages.toSorted(languageSortComparator),
|
|
|
|
|
]),
|
|
|
|
|
[languages, tmpSelectedLanguages],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
|
|
|
|
|
setTmpSourceIdToEnabledState({});
|
2026-01-14 16:33:37 +01:00
|
|
|
setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages));
|
2025-06-23 02:55:39 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOk = () => {
|
|
|
|
|
setOpen(false);
|
|
|
|
|
|
|
|
|
|
Promise.all(
|
|
|
|
|
Object.entries(tmpSourceIdToEnabledState).map(([sourceId, enabled]) =>
|
|
|
|
|
updateSourceMetadata(sources.find((source) => source.id === sourceId)!, 'isEnabled', enabled),
|
|
|
|
|
),
|
2026-01-10 19:45:02 +01:00
|
|
|
).catch((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)));
|
2025-06-23 02:55:39 +02:00
|
|
|
setTmpSourceIdToEnabledState({});
|
2026-01-14 16:33:37 +01:00
|
|
|
setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages));
|
2025-06-23 02:55:39 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleChange = (language: string, selected: boolean) => {
|
|
|
|
|
if (selected) {
|
|
|
|
|
setTmpSelectedLanguages([...tmpSelectedLanguages, language]);
|
|
|
|
|
} else {
|
|
|
|
|
setTmpSelectedLanguages(tmpSelectedLanguages.toSpliced(tmpSelectedLanguages.indexOf(language), 1));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-01-10 19:45:02 +01:00
|
|
|
<CustomTooltip title={t`Settings`}>
|
2025-06-23 02:55:39 +02:00
|
|
|
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
|
|
|
|
|
<FilterListIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</CustomTooltip>
|
|
|
|
|
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
|
2026-01-10 19:45:02 +01:00
|
|
|
<DialogTitle>{t`Allowed Languages`}</DialogTitle>
|
2025-06-23 02:55:39 +02:00
|
|
|
<DialogContent dividers sx={{ padding: 0 }}>
|
|
|
|
|
<Virtuoso
|
|
|
|
|
style={{
|
|
|
|
|
height: languagesSortedBySelectState.length * 54,
|
|
|
|
|
minHeight: '25vh',
|
|
|
|
|
maxHeight: '50vh',
|
|
|
|
|
}}
|
|
|
|
|
data={languagesSortedBySelectState}
|
|
|
|
|
increaseViewportBy={400}
|
|
|
|
|
computeItemKey={(index) => languagesSortedBySelectState[index]}
|
|
|
|
|
itemContent={(_index, language) => {
|
2025-06-23 23:51:07 +02:00
|
|
|
const sourcesOfLanguage = sourcesByLanguage[language] ?? [];
|
2025-06-23 02:55:39 +02:00
|
|
|
const isEnabled = tmpSelectedLanguages.includes(language);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<ListItem>
|
|
|
|
|
<ListItemText primary={translateExtensionLanguage(language)} />
|
|
|
|
|
<Switch
|
|
|
|
|
checked={isEnabled}
|
|
|
|
|
onChange={(e) => handleChange(language, e.target.checked)}
|
|
|
|
|
/>
|
|
|
|
|
</ListItem>
|
2025-09-22 23:58:49 +02:00
|
|
|
{isEnabled && !!sourcesOfLanguage.length && (
|
2025-06-23 02:55:39 +02:00
|
|
|
<Box sx={{ ml: 1 }}>
|
|
|
|
|
{sourcesOfLanguage.map((source) => (
|
|
|
|
|
<ListItem key={source.id}>
|
|
|
|
|
<ListItemAvatar sx={{ minWidth: 32, mr: 1 }}>
|
|
|
|
|
<ListCardAvatar
|
|
|
|
|
iconUrl={requestManager.getValidImgUrlFor(source.iconUrl)}
|
|
|
|
|
alt={source.name}
|
|
|
|
|
slots={{
|
|
|
|
|
avatarProps: {
|
|
|
|
|
sx: {
|
|
|
|
|
width: 32,
|
|
|
|
|
height: 32,
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-11-22 17:28:56 +01:00
|
|
|
spinnerImageProps: {
|
|
|
|
|
ignoreQueue: true,
|
|
|
|
|
},
|
2025-06-23 02:55:39 +02:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</ListItemAvatar>
|
|
|
|
|
<ListItemText primary={source.name} />
|
2026-01-14 23:41:58 +01:00
|
|
|
<Checkbox
|
2025-06-23 02:55:39 +02:00
|
|
|
checked={
|
|
|
|
|
tmpSourceIdToEnabledState[source.id] ??
|
|
|
|
|
getSourceMetadata(source).isEnabled
|
|
|
|
|
}
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
setTmpSourceIdToEnabledState({
|
|
|
|
|
...tmpSourceIdToEnabledState,
|
|
|
|
|
[source.id]: e.target.checked,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</ListItem>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
<Button autoFocus onClick={handleCancel} color="primary">
|
2026-01-10 19:45:02 +01:00
|
|
|
{t`Cancel`}
|
2025-06-23 02:55:39 +02:00
|
|
|
</Button>
|
|
|
|
|
<Button onClick={handleOk} color="primary">
|
2026-01-10 19:45:02 +01:00
|
|
|
{t`Ok`}
|
2025-06-23 02:55:39 +02:00
|
|
|
</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|