2023-10-27 23:39:05 +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 { useTranslation } from 'react-i18next';
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import ListItemButton from '@mui/material/ListItemButton';
|
|
|
|
|
import ListItemText from '@mui/material/ListItemText';
|
|
|
|
|
import Dialog from '@mui/material/Dialog';
|
|
|
|
|
import DialogContent from '@mui/material/DialogContent';
|
|
|
|
|
import DialogTitle from '@mui/material/DialogTitle';
|
|
|
|
|
import DialogContentText from '@mui/material/DialogContentText';
|
|
|
|
|
import DialogActions from '@mui/material/DialogActions';
|
|
|
|
|
import Button from '@mui/material/Button';
|
|
|
|
|
import { t as translate } from 'i18next';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput.tsx';
|
|
|
|
|
import { makeToast } from '@/components/util/Toast.tsx';
|
2024-01-21 13:31:04 -08:00
|
|
|
import { IncludeOrExclude } from '@/lib/graphql/generated/graphql.ts';
|
2023-10-27 23:39:05 +02:00
|
|
|
import { TCategory } from '@/typings.ts';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
2023-10-29 02:01:59 +02:00
|
|
|
import { CheckboxContainer } from '@/components/settings/globalUpdate/CheckboxContainer.ts';
|
2024-04-28 01:00:43 +02:00
|
|
|
import {
|
|
|
|
|
CategoryDownloadInclusionInfo,
|
|
|
|
|
CategoryIdInfo,
|
|
|
|
|
CategoryNameInfo,
|
|
|
|
|
CategoryUpdateInclusionInfo,
|
|
|
|
|
} from '@/lib/data/Categories.ts';
|
|
|
|
|
|
|
|
|
|
type CategoryType = CategoryIdInfo & CategoryNameInfo & CategoryUpdateInclusionInfo & CategoryDownloadInclusionInfo;
|
2023-10-27 23:39:05 +02:00
|
|
|
|
2024-01-21 13:31:04 -08:00
|
|
|
const booleanToIncludeOrExcludeStatus = (status: boolean | null | undefined): IncludeOrExclude => {
|
2023-10-27 23:39:05 +02:00
|
|
|
switch (status) {
|
|
|
|
|
case false:
|
2024-01-21 13:31:04 -08:00
|
|
|
return IncludeOrExclude.Exclude;
|
2023-10-27 23:39:05 +02:00
|
|
|
case true:
|
2024-01-21 13:31:04 -08:00
|
|
|
return IncludeOrExclude.Include;
|
2023-10-27 23:39:05 +02:00
|
|
|
case null:
|
|
|
|
|
case undefined:
|
2024-01-21 13:31:04 -08:00
|
|
|
return IncludeOrExclude.Unset;
|
2023-10-27 23:39:05 +02:00
|
|
|
default:
|
2024-01-21 13:31:04 -08:00
|
|
|
throw new Error(`booleanToIncludeInStatus: unexpected IncludeOrExclude status "${status}"`);
|
2023-10-27 23:39:05 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-21 13:31:04 -08:00
|
|
|
const includeInUpdateStatusToBoolean = (status: IncludeOrExclude): boolean | null => {
|
2023-10-27 23:39:05 +02:00
|
|
|
switch (status) {
|
2024-01-21 13:31:04 -08:00
|
|
|
case IncludeOrExclude.Exclude:
|
2023-10-27 23:39:05 +02:00
|
|
|
return false;
|
2024-01-21 13:31:04 -08:00
|
|
|
case IncludeOrExclude.Include:
|
2023-10-27 23:39:05 +02:00
|
|
|
return true;
|
2024-01-21 13:31:04 -08:00
|
|
|
case IncludeOrExclude.Unset:
|
2023-10-27 23:39:05 +02:00
|
|
|
return null;
|
|
|
|
|
default:
|
2024-01-21 13:31:04 -08:00
|
|
|
throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeOrExclude status "${status}"`);
|
2023-10-27 23:39:05 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getCategoryUpdateInfo = (
|
2024-04-28 01:00:43 +02:00
|
|
|
categories: CategoryType[],
|
2023-10-27 23:39:05 +02:00
|
|
|
areIncluded: boolean,
|
|
|
|
|
unsetCategories: number,
|
|
|
|
|
allCategories: number,
|
|
|
|
|
) => {
|
|
|
|
|
const noSpecificallyIncludedCategories = areIncluded && !categories.length && unsetCategories;
|
|
|
|
|
const includesAllCategories = categories.length === allCategories;
|
|
|
|
|
if (noSpecificallyIncludedCategories || includesAllCategories) {
|
|
|
|
|
return translate('extension.language.all');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!categories.length) {
|
|
|
|
|
return translate('global.label.none');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return categories.map((category) => category.name).join(', ');
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-21 13:31:04 -08:00
|
|
|
type CategoryIncludeField = keyof Pick<TCategory, 'includeInUpdate' | 'includeInDownload'>;
|
|
|
|
|
|
2024-04-28 01:00:43 +02:00
|
|
|
export type CategoriesInclusionSettingProps = {
|
|
|
|
|
categories: CategoryType[];
|
2024-01-21 13:31:04 -08:00
|
|
|
includeField: CategoryIncludeField;
|
|
|
|
|
dialogText?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-28 01:00:43 +02:00
|
|
|
export const CategoriesInclusionSetting = ({
|
|
|
|
|
categories,
|
|
|
|
|
includeField,
|
|
|
|
|
dialogText,
|
|
|
|
|
}: CategoriesInclusionSettingProps) => {
|
2023-10-27 23:39:05 +02:00
|
|
|
const { t } = useTranslation();
|
|
|
|
|
|
2024-04-28 01:00:43 +02:00
|
|
|
const [dialogCategories, setDialogCategories] = useState(categories);
|
2023-10-27 23:39:05 +02:00
|
|
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!categories) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setDialogCategories(categories);
|
|
|
|
|
}, [categories]);
|
|
|
|
|
|
2024-04-28 01:00:43 +02:00
|
|
|
const unsetCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Unset);
|
|
|
|
|
const excludedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Exclude);
|
|
|
|
|
const includedCategories = categories.filter((category) => category[includeField] === IncludeOrExclude.Include);
|
2023-10-27 23:39:05 +02:00
|
|
|
const excludedCategoriesText = getCategoryUpdateInfo(
|
|
|
|
|
excludedCategories,
|
|
|
|
|
false,
|
|
|
|
|
unsetCategories.length,
|
2024-04-28 01:00:43 +02:00
|
|
|
categories.length,
|
2023-10-27 23:39:05 +02:00
|
|
|
);
|
|
|
|
|
const includedCategoriesText = getCategoryUpdateInfo(
|
|
|
|
|
includedCategories,
|
|
|
|
|
true,
|
|
|
|
|
unsetCategories.length,
|
2024-04-28 01:00:43 +02:00
|
|
|
categories.length,
|
2023-10-27 23:39:05 +02:00
|
|
|
);
|
|
|
|
|
|
2024-04-28 01:00:43 +02:00
|
|
|
const updateCategory = (category: CategoryType) =>
|
2024-01-21 13:31:04 -08:00
|
|
|
requestManager.updateCategory(category.id, { [includeField]: category[includeField] }).response;
|
2023-10-27 23:39:05 +02:00
|
|
|
|
|
|
|
|
const updateCategories = async () => {
|
|
|
|
|
const categoriesToUpdate = dialogCategories.filter((category) => {
|
|
|
|
|
const currentCategory = categories?.find((currCategory) => currCategory.id === category.id);
|
|
|
|
|
|
|
|
|
|
if (!currentCategory) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-21 13:31:04 -08:00
|
|
|
return currentCategory[includeField] !== category[includeField];
|
2023-10-27 23:39:05 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
setIsDialogOpen(false);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await Promise.all(categoriesToUpdate.map((category) => updateCategory(category)));
|
|
|
|
|
// TODO - update cache immediately
|
|
|
|
|
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
makeToast(t('global.error.label.failed_to_save_changes'), 'error');
|
|
|
|
|
// mutate(categoriesEndpoint, [...categories]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const closeDialog = () => {
|
2024-04-28 01:00:43 +02:00
|
|
|
setDialogCategories(categories);
|
2023-10-27 23:39:05 +02:00
|
|
|
setIsDialogOpen(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<ListItemButton onClick={() => setIsDialogOpen(true)}>
|
|
|
|
|
<ListItemText
|
2024-01-26 20:50:51 +01:00
|
|
|
primary={t('category.title.category_other')}
|
2023-10-27 23:39:05 +02:00
|
|
|
secondary={
|
|
|
|
|
<>
|
|
|
|
|
<span>
|
2024-01-21 13:31:04 -08:00
|
|
|
{t('category.settings.inclusion.label.include', {
|
2023-10-27 23:39:05 +02:00
|
|
|
includedCategoriesText,
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
<span>
|
2024-01-21 13:31:04 -08:00
|
|
|
{t('category.settings.inclusion.label.exclude', {
|
2023-10-27 23:39:05 +02:00
|
|
|
excludedCategoriesText,
|
|
|
|
|
})}
|
|
|
|
|
</span>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
|
|
|
|
|
/>
|
|
|
|
|
</ListItemButton>
|
|
|
|
|
|
|
|
|
|
<Dialog open={isDialogOpen} onClose={closeDialog}>
|
|
|
|
|
<DialogContent>
|
2024-01-26 20:50:51 +01:00
|
|
|
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.category_other')}</DialogTitle>
|
2024-01-21 13:31:04 -08:00
|
|
|
{dialogText && <DialogContentText sx={{ paddingBottom: '10px' }}>{dialogText}</DialogContentText>}
|
2023-10-27 23:39:05 +02:00
|
|
|
<CheckboxContainer>
|
|
|
|
|
{dialogCategories.map((category) => (
|
|
|
|
|
<ThreeStateCheckboxInput
|
|
|
|
|
key={category.id}
|
|
|
|
|
label={category.name}
|
2024-01-21 13:31:04 -08:00
|
|
|
checked={includeInUpdateStatusToBoolean(category[includeField])}
|
2023-10-27 23:39:05 +02:00
|
|
|
onChange={(checked) => {
|
2024-01-21 13:31:04 -08:00
|
|
|
const newIncludeState = booleanToIncludeOrExcludeStatus(checked);
|
2023-10-27 23:39:05 +02:00
|
|
|
|
|
|
|
|
const categoryIndex = dialogCategories.findIndex(
|
|
|
|
|
(category_) => category_ === category,
|
|
|
|
|
);
|
2024-04-28 01:00:43 +02:00
|
|
|
const updatedDialogCategories = [
|
2023-10-27 23:39:05 +02:00
|
|
|
...dialogCategories.slice(0, categoryIndex),
|
|
|
|
|
{
|
|
|
|
|
...category,
|
2024-01-21 13:31:04 -08:00
|
|
|
[includeField]: newIncludeState,
|
2023-10-27 23:39:05 +02:00
|
|
|
},
|
|
|
|
|
...dialogCategories.slice(categoryIndex + 1, dialogCategories.length),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
setDialogCategories(updatedDialogCategories);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</CheckboxContainer>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
<Button onClick={closeDialog} color="primary">
|
|
|
|
|
{t('global.button.cancel')}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={updateCategories} color="primary">
|
|
|
|
|
{t('global.button.ok')}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|