Add option to enable/disable sources in language filter

This commit is contained in:
schroda
2025-06-23 02:55:39 +02:00
parent 7b9d51142b
commit 16d66718a8
4 changed files with 195 additions and 6 deletions

View File

@@ -24,7 +24,6 @@ import IconButton from '@mui/material/IconButton'; // ms
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx';
import { LanguageSelect } from '@/modules/core/components/inputs/LanguageSelect.tsx';
import { useDebounce } from '@/modules/core/hooks/useDebounce.ts';
import { MangaCardProps } from '@/modules/manga/Manga.types.ts';
import { EmptyView } from '@/modules/core/components/feedback/EmptyView.tsx';
@@ -53,6 +52,7 @@ import { makeToast } from '@/modules/core/utils/Toast.ts';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { MetadataBrowseSettings } from '@/modules/browse/Browse.types.ts';
import { SourceLanguageSelect } from '@/modules/source/components/SourceLanguageSelect.tsx';
type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any };
type SourceToLoadingStateMap = Map<string, SourceLoadingState>;
@@ -293,13 +293,14 @@ export const SearchAll: React.FC = () => {
t(isMigrateMode ? 'migrate.search.title' : 'search.title.global_search', { title: state?.mangaTitle }),
<>
<AppbarSearch isClosable={false} />
<LanguageSelect
<SourceLanguageSelect
selectedLanguages={shownLangs}
setSelectedLanguages={setShownLangs}
languages={sourceLanguages}
sources={sources ?? []}
/>
</>,
[shownLangs, setShownLangs, sourceLanguages],
[shownLangs, setShownLangs, sourceLanguages, sources],
);
if (loading) {

View File

@@ -71,3 +71,4 @@ export type SourceNsfwInfo = Pick<SourceType, 'isNsfw'>;
export type SourceRepoInfo = { extension: Pick<ExtensionType, 'repo'> };
export type SourceMetaInfo = { meta: SourceMetaFieldsFragment[] };
export type SourceConfigurableInfo = Pick<SourceType, 'isConfigurable'>;
export type SourceIconInfo = Pick<SourceType, 'iconUrl'>;

View File

@@ -0,0 +1,186 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { Virtuoso } from 'react-virtuoso';
import Box from '@mui/material/Box';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import { CustomTooltip } from '@/modules/core/components/CustomTooltip.tsx';
import { translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
import { languageSortComparator, toUniqueLanguageCodes } from '@/modules/core/utils/Languages.ts';
import {
SourceDisplayNameInfo,
SourceIconInfo,
SourceIdInfo,
SourceLanguageInfo,
SourceMetaInfo,
SourceNameInfo,
} from '@/modules/source/Source.types.ts';
import { Sources } from '@/modules/source/services/Sources';
import { getSourceMetadata, updateSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ListCardAvatar } from '@/modules/core/components/lists/cards/ListCardAvatar.tsx';
import { makeToast } from '@/modules/core/utils/Toast.ts';
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)[];
}) => {
const { t } = useTranslation();
const [tmpSourceIdToEnabledState, setTmpSourceIdToEnabledState] = useState<Record<SourceIdInfo['id'], boolean>>({});
const [tmpSelectedLanguages, setTmpSelectedLanguages] = useState(toUniqueLanguageCodes(selectedLanguages));
const [open, setOpen] = useState<boolean>(false);
const sourcesByLanguage = useMemo(() => Sources.groupByLanguage(sources), [sources]);
const languagesSortedBySelectState = useMemo(
() =>
toUniqueLanguageCodes([
...tmpSelectedLanguages.toSorted(languageSortComparator),
...languages.toSorted(languageSortComparator),
]),
[languages, tmpSelectedLanguages],
);
const handleCancel = () => {
setOpen(false);
setTmpSourceIdToEnabledState({});
setTmpSelectedLanguages(toUniqueLanguageCodes(selectedLanguages));
};
const handleOk = () => {
setOpen(false);
Promise.all(
Object.entries(tmpSourceIdToEnabledState).map(([sourceId, enabled]) =>
updateSourceMetadata(sources.find((source) => source.id === sourceId)!, 'isEnabled', enabled),
),
).catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
setTmpSourceIdToEnabledState({});
setSelectedLanguages(toUniqueLanguageCodes(tmpSelectedLanguages));
};
const handleChange = (language: string, selected: boolean) => {
if (selected) {
setTmpSelectedLanguages([...tmpSelectedLanguages, language]);
} else {
setTmpSelectedLanguages(tmpSelectedLanguages.toSpliced(tmpSelectedLanguages.indexOf(language), 1));
}
};
return (
<>
<CustomTooltip title={t('settings.title')}>
<IconButton onClick={() => setOpen(true)} aria-label="display more actions" edge="end" color="inherit">
<FilterListIcon />
</IconButton>
</CustomTooltip>
<Dialog fullWidth maxWidth="xs" open={open} onClose={handleCancel}>
<DialogTitle>{t('global.language.title.enabled_languages')}</DialogTitle>
<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) => {
const sourcesOfLanguage = sourcesByLanguage[language];
const isEnabled = tmpSelectedLanguages.includes(language);
return (
<>
<ListItem>
<ListItemText primary={translateExtensionLanguage(language)} />
<Switch
checked={isEnabled}
onChange={(e) => handleChange(language, e.target.checked)}
/>
</ListItem>
{isEnabled && sourcesOfLanguage.length && (
<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,
},
},
}}
/>
</ListItemAvatar>
<ListItemText primary={source.name} />
<Switch
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">
{t('global.button.cancel')}
</Button>
<Button onClick={handleOk} color="primary">
{t('global.button.ok')}
</Button>
</DialogActions>
</Dialog>
</>
);
};

View File

@@ -17,7 +17,6 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { DefaultLanguage } from '@/modules/core/utils/Languages.ts';
import { LoadingPlaceholder } from '@/modules/core/components/feedback/LoadingPlaceholder.tsx';
import { SourceCard } from '@/modules/source/components/SourceCard.tsx';
import { LanguageSelect } from '@/modules/core/components/inputs/LanguageSelect.tsx';
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { isPinnedOrLastUsedSource, translateExtensionLanguage } from '@/modules/extension/Extensions.utils.ts';
@@ -30,6 +29,7 @@ import { StyledGroupedVirtuoso } from '@/modules/core/components/virtuoso/Styled
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx';
import { StyledGroupHeader } from '@/modules/core/components/virtuoso/StyledGroupHeader.tsx';
import { StyledGroupItemWrapper } from '@/modules/core/components/virtuoso/StyledGroupItemWrapper.tsx';
import { SourceLanguageSelect } from '@/modules/source/components/SourceLanguageSelect.tsx';
export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useTranslation();
@@ -102,13 +102,14 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
<TravelExploreIcon />
</IconButton>
</CustomTooltip>
<LanguageSelect
<SourceLanguageSelect
selectedLanguages={shownLangs}
setSelectedLanguages={setShownLangs}
languages={sourceLanguages}
sources={sources ?? []}
/>
</>,
[t, shownLangs, sourceLanguages],
[t, shownLangs, sourceLanguages, sources],
);
if (isLoading) return <LoadingPlaceholder />;