Feature/library global update exclude manga with state (#281)

* Extract global update settings

* Add option to exclude mangas with specific state from global update
This commit is contained in:
schroda
2023-10-27 23:39:05 +02:00
committed by GitHub
parent c8f02b3b8c
commit 68e7b4b16d
9 changed files with 484 additions and 240 deletions

View File

@@ -0,0 +1,17 @@
/*
* 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 { styled } from '@mui/material';
// eslint-disable-next-line import/prefer-default-export
export const CheckboxContainer = styled('div')({
display: 'flex',
flexDirection: 'column',
maxHeight: '170px',
overflow: 'auto',
});

View File

@@ -0,0 +1,31 @@
/*
* 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 List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import { useTranslation } from 'react-i18next';
import { GlobalUpdateSettingsCategories } from '@/components/globalUpdate/GlobalUpdateSettingsCategories.tsx';
import { GlobalUpdateSettingsEntries } from '@/components/globalUpdate/GlobalUpdateSettingsEntries.tsx';
// eslint-disable-next-line import/prefer-default-export
export const GlobalUpdateSettings = () => {
const { t } = useTranslation();
return (
<List
subheader={
<ListSubheader component="div" id="global-update-settings">
{t('library.settings.global_update.title')}
</ListSubheader>
}
>
<GlobalUpdateSettingsEntries />
<GlobalUpdateSettingsCategories />
</List>
);
};

View File

@@ -0,0 +1,217 @@
/*
* 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';
import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput.tsx';
import makeToast from '@/components/util/Toast.tsx';
import { IncludeInUpdate } from '@/lib/graphql/generated/graphql.ts';
import { TCategory } from '@/typings.ts';
import requestManager from '@/lib/requests/RequestManager.ts';
import { CheckboxContainer } from '@/components/globalUpdate/CheckboxContainer.ts';
const booleanToIncludeInStatus = (status: boolean | null | undefined): IncludeInUpdate => {
switch (status) {
case false:
return IncludeInUpdate.Exclude;
case true:
return IncludeInUpdate.Include;
case null:
case undefined:
return IncludeInUpdate.Unset;
default:
throw new Error(`booleanToIncludeInStatus: unexpected IncludeInUpdate status "${status}"`);
}
};
const includeInUpdateStatusToBoolean = (status: IncludeInUpdate): boolean | null => {
switch (status) {
case IncludeInUpdate.Exclude:
return false;
case IncludeInUpdate.Include:
return true;
case IncludeInUpdate.Unset:
return null;
default:
throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeInUpdate status "${status}"`);
}
};
const getCategoryUpdateInfo = (
categories: TCategory[],
areIncluded: boolean,
unsetCategories: number,
allCategories: number,
error: any,
) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (allCategories === -1) {
return translate('global.label.loading');
}
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(', ');
};
// eslint-disable-next-line import/prefer-default-export
export const GlobalUpdateSettingsCategories = () => {
const { t } = useTranslation();
const { data, error: requestError } = requestManager.useGetCategories();
const categories = data?.categories.nodes;
const [dialogCategories, setDialogCategories] = useState<TCategory[]>(categories ?? []);
const [isDialogOpen, setIsDialogOpen] = useState(false);
useEffect(() => {
if (!categories) {
return;
}
setDialogCategories(categories);
}, [categories]);
const unsetCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Unset) ?? [];
const excludedCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Exclude) ?? [];
const includedCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Include) ?? [];
const excludedCategoriesText = getCategoryUpdateInfo(
excludedCategories,
false,
unsetCategories.length,
categories?.length ?? -1,
requestError,
);
const includedCategoriesText = getCategoryUpdateInfo(
includedCategories,
true,
unsetCategories.length,
categories?.length ?? -1,
requestError,
);
const updateCategory = (category: TCategory) =>
requestManager.updateCategory(category.id, { includeInUpdate: category.includeInUpdate }).response;
const updateCategories = async () => {
const categoriesToUpdate = dialogCategories.filter((category) => {
const currentCategory = categories?.find((currCategory) => currCategory.id === category.id);
if (!currentCategory) {
return false;
}
return currentCategory.includeInUpdate !== category.includeInUpdate;
});
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 = () => {
setDialogCategories(categories ?? []);
setIsDialogOpen(false);
};
return (
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('category.title.categories')}
secondary={
<>
<span>
{t('library.settings.global_update.categories.label.include', {
includedCategoriesText,
})}
</span>
<span>
{t('library.settings.global_update.categories.label.exclude', {
excludedCategoriesText,
})}
</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.categories')}</DialogTitle>
<DialogContentText sx={{ paddingBottom: '10px' }}>
{t('library.settings.global_update.categories.label.info')}
</DialogContentText>
<CheckboxContainer>
{dialogCategories.map((category) => (
<ThreeStateCheckboxInput
key={category.id}
label={category.name}
checked={includeInUpdateStatusToBoolean(category.includeInUpdate)}
onChange={(checked) => {
const newIncludeState = booleanToIncludeInStatus(checked);
const categoryIndex = dialogCategories.findIndex(
(category_) => category_ === category,
);
const updatedDialogCategories: TCategory[] = [
...dialogCategories.slice(0, categoryIndex),
{
...category,
includeInUpdate: newIncludeState,
},
...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>
</>
);
};

View File

@@ -0,0 +1,164 @@
/*
* 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 { t as translate } from 'i18next';
import { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
import { GetServerSettingsQuery } from '@/lib/graphql/generated/graphql.ts';
import { TranslationKey } from '@/typings.ts';
import requestManager from '@/lib/requests/RequestManager.ts';
import makeToast from '@/components/util/Toast.tsx';
import { CheckboxContainer } from '@/components/globalUpdate/CheckboxContainer';
import CheckboxInput from '@/components/atoms/CheckboxInput';
type GlobalUpdateSkipEntriesSettings = Pick<
GetServerSettingsQuery['settings'],
'excludeUnreadChapters' | 'excludeNotStarted' | 'excludeCompleted'
>;
const settingToTextMap: { [setting in keyof GlobalUpdateSkipEntriesSettings]: TranslationKey } = {
excludeUnreadChapters: 'library.settings.global_update.entries.label.unread_chapters',
excludeNotStarted: 'library.settings.global_update.entries.label.not_started',
excludeCompleted: 'library.settings.global_update.entries.label.completed',
};
const getSkipMangasText = (settings: GlobalUpdateSkipEntriesSettings | undefined, isLoading: boolean, error: any) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (!settings || isLoading) {
return translate('global.label.loading');
}
const skipSettings: string[] = [];
if (settings.excludeUnreadChapters) {
skipSettings.push(translate(settingToTextMap.excludeUnreadChapters) as string);
}
if (settings.excludeNotStarted) {
skipSettings.push(translate(settingToTextMap.excludeNotStarted) as string);
}
if (settings.excludeCompleted) {
skipSettings.push(translate(settingToTextMap.excludeCompleted) as string);
}
const isNothingExcluded = !skipSettings.length;
if (isNothingExcluded) {
skipSettings.push(translate('global.label.none'));
}
return skipSettings.join(', ');
};
const extractSkipEntriesSettings = (
serverSettings: GetServerSettingsQuery['settings'],
): GlobalUpdateSkipEntriesSettings => ({
excludeCompleted: serverSettings.excludeCompleted,
excludeNotStarted: serverSettings.excludeNotStarted,
excludeUnreadChapters: serverSettings.excludeUnreadChapters,
});
// eslint-disable-next-line import/prefer-default-export
export const GlobalUpdateSettingsEntries = () => {
const { t } = useTranslation();
const { data, loading, error: requestError } = requestManager.useGetServerSettings();
const globalUpdateSettings = data ? extractSkipEntriesSettings(data.settings) : undefined;
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [dialogSettings, setDialogSettings] = useState<GlobalUpdateSkipEntriesSettings>(
globalUpdateSettings ?? ({} as GlobalUpdateSkipEntriesSettings),
);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const skipEntriesText = getSkipMangasText(globalUpdateSettings, loading, requestError);
const updateSettings = async () => {
const didSettingsChange =
globalUpdateSettings?.excludeCompleted !== dialogSettings.excludeCompleted ||
globalUpdateSettings.excludeNotStarted !== dialogSettings.excludeNotStarted ||
globalUpdateSettings.excludeUnreadChapters !== dialogSettings.excludeUnreadChapters;
setIsDialogOpen(false);
if (!didSettingsChange) {
return;
}
try {
await mutateSettings({ variables: { input: { settings: dialogSettings } } });
} catch (error) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error');
}
};
const closeDialog = () => {
setDialogSettings(globalUpdateSettings ?? ({} as GlobalUpdateSkipEntriesSettings));
setIsDialogOpen(false);
};
useEffect(() => {
if (!globalUpdateSettings) {
return;
}
setDialogSettings(globalUpdateSettings);
}, [
globalUpdateSettings?.excludeCompleted,
globalUpdateSettings?.excludeNotStarted,
globalUpdateSettings?.excludeUnreadChapters,
]);
return (
<>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('library.settings.global_update.entries.title')}
secondary={skipEntriesText}
onClick={() => setIsDialogOpen(true)}
/>
</ListItemButton>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>
{t('library.settings.global_update.entries.title')}
</DialogTitle>
<CheckboxContainer>
{Object.entries(dialogSettings).map(([setting, value]) => (
<CheckboxInput
key={setting}
label={t(settingToTextMap[setting as keyof GlobalUpdateSkipEntriesSettings])}
checked={value}
onChange={(_, checked) => {
setDialogSettings({
...dialogSettings,
[setting]: checked,
});
}}
/>
))}
</CheckboxContainer>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
{t('global.button.cancel')}
</Button>
<Button onClick={updateSettings} color="primary">
{t('global.button.ok')}
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -313,6 +313,14 @@
"info": "Entries in excluded categories will not be updated even if they are also in included categories"
}
},
"entries": {
"title": "Skip updating entries",
"label": {
"completed": "With \"Completed\" status",
"not_started": "That haven't been started",
"unread_chapters": "With unread chapter(s)"
}
},
"title": "Global update"
},
"title": "Library Settings"

View File

@@ -83,6 +83,7 @@ import {
GetMangaQueryVariables,
GetMangasQuery,
GetMangasQueryVariables,
GetServerSettingsQuery,
GetSourceMangasFetchMutation,
GetSourceMangasFetchMutationVariables,
GetSourceQuery,
@@ -137,6 +138,8 @@ import {
UpdateMangaPatchInput,
UpdaterSubscription,
UpdaterSubscriptionVariables,
UpdateServerSettingsMutation,
UpdateServerSettingsMutationVariables,
UpdateSourcePreferencesMutation,
UpdateSourcePreferencesMutationVariables,
ValidateBackupQuery,
@@ -199,6 +202,8 @@ import { RESTORE_BACKUP } from '@/lib/graphql/mutations/BackupMutation.ts';
import { VALIDATE_BACKUP } from '@/lib/graphql/queries/BackupQuery.ts';
import { DOWNLOAD_STATUS_SUBSCRIPTION } from '@/lib/graphql/subscriptions/DownloaderSubscription.ts';
import { UPDATER_SUBSCRIPTION } from '@/lib/graphql/subscriptions/UpdaterSubscription.ts';
import { GET_SERVER_SETTINGS } from '@/lib/graphql/queries/SettingsQuery.ts';
import { UPDATE_SERVER_SETTINGS } from '@/lib/graphql/mutations/SettingsMutation.ts';
enum GQLMethod {
QUERY = 'QUERY',
@@ -1776,6 +1781,18 @@ export class RequestManager {
): SubscriptionResult<UpdaterSubscription, UpdaterSubscriptionVariables> {
return this.doRequest(GQLMethod.USE_SUBSCRIPTION, UPDATER_SUBSCRIPTION, {}, options);
}
public useGetServerSettings(
options?: QueryHookOptions<GetServerSettingsQuery, GetSourcesQueryVariables>,
): AbortableApolloUseQueryResponse<GetServerSettingsQuery, GetSourcesQueryVariables> {
return this.doRequest(GQLMethod.USE_QUERY, GET_SERVER_SETTINGS, undefined, options);
}
public useUpdateServerSettings(
options?: MutationHookOptions<UpdateServerSettingsMutation, UpdateServerSettingsMutationVariables>,
): AbortableApolloUseMutationResponse<UpdateServerSettingsMutation, UpdateServerSettingsMutationVariables> {
return this.doRequest(GQLMethod.USE_MUTATION, UPDATE_SERVER_SETTINGS, undefined, options);
}
}
const requestManager = new RequestManager();

View File

@@ -26,6 +26,7 @@ const typePolicies: StrictTypedTypePolicies = {
GlobalMetaType: { keyFields: ['key'] },
ExtensionType: { keyFields: ['apkName'] },
AboutPayload: { keyFields: [] },
SettingsType: { keyFields: [] },
Query: {
fields: {
chapters: {

View File

@@ -6,88 +6,11 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useContext, useEffect, useState } from 'react';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import ListItemButton from '@mui/material/ListItemButton';
import DialogTitle from '@mui/material/DialogTitle';
import { styled } from '@mui/material';
import { t as translate } from 'i18next';
import { useTranslation } from 'react-i18next';
import requestManager from '@/lib/requests/RequestManager.ts';
import makeToast from '@/components/util/Toast';
import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput';
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
import SearchSettings from '@/screens/settings/SearchSettings';
import { IncludeInUpdate } from '@/lib/graphql/generated/graphql.ts';
import { TCategory } from '@/typings.ts';
const CategoriesDiv = styled('div')({
display: 'flex',
flexDirection: 'column',
maxHeight: '170px',
overflow: 'auto',
});
const booleanToIncludeInStatus = (status: boolean | null | undefined): IncludeInUpdate => {
switch (status) {
case false:
return IncludeInUpdate.Exclude;
case true:
return IncludeInUpdate.Include;
case null:
case undefined:
return IncludeInUpdate.Unset;
default:
throw new Error(`booleanToIncludeInStatus: unexpected IncludeInUpdate status "${status}"`);
}
};
const includeInUpdateStatusToBoolean = (status: IncludeInUpdate): boolean | null => {
switch (status) {
case IncludeInUpdate.Exclude:
return false;
case IncludeInUpdate.Include:
return true;
case IncludeInUpdate.Unset:
return null;
default:
throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeInUpdate status "${status}"`);
}
};
const getCategoryUpdateInfo = (
categories: TCategory[],
areIncluded: boolean,
unsetCategories: number,
allCategories: number,
error: any,
) => {
if (error) {
return translate('global.error.label.failed_to_load_data');
}
if (allCategories === -1) {
return translate('global.label.loading');
}
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(', ');
};
import { useContext, useEffect } from 'react';
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext.tsx';
import { GlobalUpdateSettings } from '@/components/globalUpdate/GlobalUpdateSettings.tsx';
import SearchSettings from '@/screens/settings/SearchSettings.tsx';
export default function LibrarySettings() {
const { t } = useTranslation();
@@ -100,153 +23,10 @@ export default function LibrarySettings() {
useSetDefaultBackTo('settings');
const { data, error: requestError } = requestManager.useGetCategories();
const categories = data?.categories.nodes;
const [dialogCategories, setDialogCategories] = useState<TCategory[]>(categories ?? []);
const [isDialogOpen, setIsDialogOpen] = useState(false);
useEffect(() => {
if (!categories) {
return;
}
setDialogCategories(categories);
}, [categories]);
const unsetCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Unset) ?? [];
const excludedCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Exclude) ?? [];
const includedCategories: TCategory[] =
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Include) ?? [];
const excludedCategoriesText = getCategoryUpdateInfo(
excludedCategories,
false,
unsetCategories.length,
categories?.length ?? -1,
requestError,
);
const includedCategoriesText = getCategoryUpdateInfo(
includedCategories,
true,
unsetCategories.length,
categories?.length ?? -1,
requestError,
);
const updateCategory = (category: TCategory) =>
requestManager.updateCategory(category.id, { includeInUpdate: category.includeInUpdate }).response;
const updateCategories = async () => {
const categoriesToUpdate = dialogCategories.filter((category) => {
const currentCategory = categories?.find((currCategory) => currCategory.id === category.id);
if (!currentCategory) {
return false;
}
return currentCategory.includeInUpdate !== category.includeInUpdate;
});
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 = () => {
setDialogCategories(categories ?? []);
setIsDialogOpen(false);
};
return (
<>
<List
subheader={
<ListSubheader component="div" id="nested-list-subheader">
{t('search.title.search')}
</ListSubheader>
}
>
<SearchSettings />
</List>
<List
subheader={
<ListSubheader component="div" id="nested-list-subheader">
{t('library.settings.global_update.title')}
</ListSubheader>
}
>
<ListItemButton onClick={() => setIsDialogOpen(true)}>
<ListItemText
primary={t('category.title.categories')}
secondary={
<>
<span>
{t('library.settings.global_update.categories.label.include', {
includedCategoriesText,
})}
</span>
<span>
{t('library.settings.global_update.categories.label.exclude', {
excludedCategoriesText,
})}
</span>
</>
}
secondaryTypographyProps={{ style: { display: 'flex', flexDirection: 'column' } }}
/>
</ListItemButton>
</List>
<Dialog open={isDialogOpen} onClose={closeDialog}>
<DialogContent>
<DialogTitle sx={{ paddingLeft: 0 }}>{t('category.title.categories')}</DialogTitle>
<DialogContentText sx={{ paddingBottom: '10px' }}>
{t('library.settings.global_update.categories.label.info')}
</DialogContentText>
<CategoriesDiv>
{dialogCategories.map((category) => (
<ThreeStateCheckboxInput
key={category.id}
label={category.name}
checked={includeInUpdateStatusToBoolean(category.includeInUpdate)}
onChange={(checked) => {
const newIncludeState = booleanToIncludeInStatus(checked);
const categoryIndex = dialogCategories.findIndex(
(category_) => category_ === category,
);
const updatedDialogCategories: TCategory[] = [
...dialogCategories.slice(0, categoryIndex),
{
...category,
includeInUpdate: newIncludeState,
},
...dialogCategories.slice(categoryIndex + 1, dialogCategories.length),
];
setDialogCategories(updatedDialogCategories);
}}
/>
))}
</CategoriesDiv>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
{t('global.button.cancel')}
</Button>
<Button onClick={updateCategories} color="primary">
{t('global.button.ok')}
</Button>
</DialogActions>
</Dialog>
<GlobalUpdateSettings />
</>
);
}

View File

@@ -6,11 +6,12 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { ListItem, ListItemText, Switch } from '@mui/material';
import { List, ListItem, ListItemText, Switch } from '@mui/material';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import ListItemIcon from '@mui/material/ListItemIcon';
import SearchIcon from '@mui/icons-material/Search';
import { useTranslation } from 'react-i18next';
import ListSubheader from '@mui/material/ListSubheader';
import { SearchMetadataKeys } from '@/typings';
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
import { useSearchSettings } from '@/util/searchSettings';
@@ -29,6 +30,13 @@ export default function SearchSettings() {
);
};
return (
<List
subheader={
<ListSubheader component="div" id="library-search-filter">
{t('search.title.search')}
</ListSubheader>
}
>
<ListItem>
<ListItemIcon>
<SearchIcon />
@@ -42,5 +50,6 @@ export default function SearchSettings() {
/>
</ListItemSecondaryAction>
</ListItem>
</List>
);
}