Add option to disable sources of an extension

This commit is contained in:
schroda
2025-06-23 01:51:14 +02:00
parent ecfde354b8
commit f3c479fe32
7 changed files with 72 additions and 35 deletions

View File

@@ -17,6 +17,8 @@ import Card from '@mui/material/Card';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import SettingsIcon from '@mui/icons-material/Settings'; import SettingsIcon from '@mui/icons-material/Settings';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import Switch from '@mui/material/Switch';
import CardActionArea from '@mui/material/CardActionArea';
import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx'; import { SpinnerImage } from '@/modules/core/components/SpinnerImage.tsx';
import { Metadata } from '@/modules/core/components/texts/Metadata.tsx'; import { Metadata } from '@/modules/core/components/texts/Metadata.tsx';
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts'; import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts';
@@ -41,7 +43,8 @@ import { AppRoutes } from '@/modules/core/AppRoute.constants.ts';
import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/modules/source/Source.types.ts'; import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/modules/source/Source.types.ts';
import { MUIUtil } from '@/lib/mui/MUI.util.ts'; import { MUIUtil } from '@/lib/mui/MUI.util.ts';
import { useBackButton } from '@/modules/core/hooks/useBackButton.ts'; import { useBackButton } from '@/modules/core/hooks/useBackButton.ts';
import { OptionalCardActionAreaLink } from '@/modules/core/components/lists/cards/OptionalCardActionAreaLink.tsx'; import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts';
import { makeToast } from '@/modules/core/utils/Toast.ts';
const Header = ({ name, pkgName, iconUrl, repo }: TExtension) => ( const Header = ({ name, pkgName, iconUrl, repo }: TExtension) => (
<Stack sx={{ alignItems: 'center' }}> <Stack sx={{ alignItems: 'center' }}>
@@ -140,43 +143,53 @@ const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtensio
); );
}; };
const SourceList = ({ sources }: { sources: (SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo)[] }) => { const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => {
const { id, isConfigurable } = source;
const { t } = useTranslation(); const { t } = useTranslation();
const { isEnabled } = useGetSourceMetadata(source);
const updateSetting = createUpdateSourceMetadata(source, (e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
return ( return (
<Box sx={{ px: 1 }}> <StyledGroupItemWrapper key={id} sx={{ px: 0 }}>
{sources?.map((source) => ( <Card>
<StyledGroupItemWrapper key={source.id} sx={{ px: 0 }}> <CardActionArea onClick={() => updateSetting('isEnabled', !isEnabled)}>
<Card> <ListCardContent>
<OptionalCardActionAreaLink <Typography variant="h6" component="h3" sx={{ flexGrow: 1 }}>
disabled={!source.isConfigurable} {translateExtensionLanguage(Sources.getLanguage(source))}
to={AppRoutes.sources.childRoutes.configure.path(source.id)} </Typography>
> {isConfigurable && (
<ListCardContent> <CustomTooltip title={t('settings.title')}>
<Typography variant="h6" component="h3" sx={{ flexGrow: 1 }}> <IconButton
{translateExtensionLanguage(Sources.getLanguage(source))} component={Link}
</Typography> to={AppRoutes.sources.childRoutes.configure.path(id)}
{source.isConfigurable && ( color="inherit"
<CustomTooltip title={t('settings.title')}> onClick={(e) => e.stopPropagation()}
<IconButton {...MUIUtil.preventRippleProp()}
component={Link} >
to={AppRoutes.sources.childRoutes.configure.path(source.id)} <SettingsIcon />
color="inherit" </IconButton>
{...MUIUtil.preventRippleProp()} </CustomTooltip>
> )}
<SettingsIcon /> <Switch checked={isEnabled} onChange={(_, checked) => updateSetting('isEnabled', checked)} />
</IconButton> </ListCardContent>
</CustomTooltip> </CardActionArea>
)} </Card>
</ListCardContent> </StyledGroupItemWrapper>
</OptionalCardActionAreaLink>
</Card>
</StyledGroupItemWrapper>
))}
</Box>
); );
}; };
const SourceList = ({ sources }: { sources: (SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo)[] }) => (
<Box sx={{ px: 1 }}>
{sources.map((source) => (
<SourceCard key={source.id} {...source} />
))}
</Box>
);
export const ExtensionInfo = () => { export const ExtensionInfo = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pkgName } = useParams<{ pkgName: string }>(); const { pkgName } = useParams<{ pkgName: string }>();
@@ -195,7 +208,7 @@ export const ExtensionInfo = () => {
return sourcesResponse.data.sources.nodes return sourcesResponse.data.sources.nodes
.filter((source) => source.extension.pkgName === extension?.pkgName) .filter((source) => source.extension.pkgName === extension?.pkgName)
.sort((a, b) => languageSortComparator(Sources.getLanguage(a), Sources.getLanguage(b))); .sort((a, b) => languageSortComparator(Sources.getLanguage(a), Sources.getLanguage(b)));
}, [extension?.pkgName]); }, [extension?.pkgName, sourcesResponse.data]);
const isLoading = extensionResponse.loading || sourcesResponse.loading; const isLoading = extensionResponse.loading || sourcesResponse.loading;
const error = extensionResponse.error || sourcesResponse.error; const error = extensionResponse.error || sourcesResponse.error;

View File

@@ -263,6 +263,7 @@ export const SearchAll: React.FC = () => {
languages: shownLangs, languages: shownLangs,
keepLocalSource: true, keepLocalSource: true,
pinned: shouldShowOnlyPinnedSources, pinned: shouldShowOnlyPinnedSources,
enabled: true,
}), }),
[sources, shownLangs, shouldShowOnlyPinnedSources], [sources, shownLangs, shouldShowOnlyPinnedSources],
); );

View File

@@ -96,6 +96,7 @@ export const APP_METADATA_KEY_TO_TYPE = {
shouldUsePureBlackMode: 'boolean', shouldUsePureBlackMode: 'boolean',
mangaGridItemWidth: 'number', mangaGridItemWidth: 'number',
isPinned: 'boolean', isPinned: 'boolean',
isEnabled: 'boolean',
lastUsedSourceId: 'string', lastUsedSourceId: 'string',
shouldShowOnlySourcesWithResults: 'boolean', shouldShowOnlySourcesWithResults: 'boolean',
excludedScanlators: 'string', // string[] excludedScanlators: 'string', // string[]

View File

@@ -27,6 +27,7 @@ export type SavedSourceSearch = { query?: string; filters?: IPos[] };
export interface ISourceMetadata { export interface ISourceMetadata {
savedSearches?: Record<string, SavedSourceSearch>; savedSearches?: Record<string, SavedSourceSearch>;
isPinned: boolean; isPinned: boolean;
isEnabled: boolean;
} }
export type SourceFilters = GetSourceBrowseQuery['source']['filters'][number]; export type SourceFilters = GetSourceBrowseQuery['source']['filters'][number];

View File

@@ -48,7 +48,13 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
} = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true }); } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true });
const sources = data?.sources.nodes; const sources = data?.sources.nodes;
const filteredSources = useMemo( const filteredSources = useMemo(
() => SourceService.filter(sources ?? [], { showNsfw, languages: shownLangs, keepLocalSource: true }), () =>
SourceService.filter(sources ?? [], {
showNsfw,
languages: shownLangs,
keepLocalSource: true,
enabled: true,
}),
[sources, shownLangs], [sources, shownLangs],
); );
const sourcesByLanguage = useMemo(() => { const sourcesByLanguage = useMemo(() => {

View File

@@ -23,6 +23,7 @@ import {
const DEFAULT_SOURCE_METADATA: ISourceMetadata = { const DEFAULT_SOURCE_METADATA: ISourceMetadata = {
savedSearches: undefined, savedSearches: undefined,
isPinned: false, isPinned: false,
isEnabled: true,
}; };
const convertAppMetadataToGqlMetadata = ( const convertAppMetadataToGqlMetadata = (

View File

@@ -81,7 +81,14 @@ export class Sources {
languages, languages,
keepLocalSource, keepLocalSource,
pinned, pinned,
}: { showNsfw?: boolean; languages?: string[]; keepLocalSource?: boolean; pinned?: boolean } = {}, enabled,
}: {
showNsfw?: boolean;
languages?: string[];
keepLocalSource?: boolean;
pinned?: boolean;
enabled?: boolean;
} = {},
): Source[] { ): Source[] {
const normalizedLanguages = toComparableLanguages(toUniqueLanguageCodes(languages ?? [])); const normalizedLanguages = toComparableLanguages(toUniqueLanguageCodes(languages ?? []));
@@ -105,6 +112,13 @@ export class Sources {
!pinned || !pinned ||
getSourceMetadata(source).isPinned || getSourceMetadata(source).isPinned ||
(keepLocalSource && Sources.isLocalSource(source)), (keepLocalSource && Sources.isLocalSource(source)),
)
.filter(
(source) =>
enabled === undefined ||
!enabled ||
getSourceMetadata(source).isEnabled ||
(keepLocalSource && Sources.isLocalSource(source)),
); );
} }