Integrate extension api v1.6 changes

This commit is contained in:
schroda
2026-07-01 15:10:36 +02:00
parent 3812a84514
commit dd1199f3be
32 changed files with 1190 additions and 176 deletions

View File

@@ -116,14 +116,9 @@ const GroupHeader = ({
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const { t } = useLingui();
const {
data: serverSettingsData,
loading: areServerSettingsLoading,
error: serverSettingsError,
refetch: refetchServerSettings,
} = requestManager.useGetServerSettings();
const [fetchExtensions, { data, loading: areExtensionsLoading, error: extensionsError }] =
requestManager.useExtensionListFetch();
const extensionStoresRequest = requestManager.useGetExtensionStores();
const {
settings: { browseLanguages: shownLangs, showNsfw },
@@ -137,11 +132,8 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
const [updatingExtensionIds, setUpdatingExtensionIds] = useState<string[]>([]);
const [refetchExtensions, setRefetchExtensions] = useState({});
const isLoading = areServerSettingsLoading || areExtensionsLoading;
const error = serverSettingsError ?? extensionsError;
const areReposDefined = !!serverSettingsData?.settings.extensionRepos.length;
const areMultipleReposInUse = (serverSettingsData?.settings.extensionRepos.length ?? 0) > 1;
const isLoading = extensionStoresRequest.loading || areExtensionsLoading;
const error = extensionStoresRequest.error ?? extensionsError;
const allExtensions = data?.fetchExtensions?.extensions ?? STABLE_EMPTY_ARRAY;
const allLangs = useMemo(() => getLanguagesFromExtensions(allExtensions), [allExtensions]);
@@ -160,6 +152,17 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
[groupedExtensions],
);
const areReposDefined = !!extensionStoresRequest.data?.extensionStores.totalCount;
const areMultipleReposInUse = useMemo(() => {
if (!allExtensions.length) {
return false;
}
const store = allExtensions[0].extensionStore?.indexUrl;
return allExtensions.slice(1).some((extension) => extension.extensionStore?.indexUrl !== store);
}, [allExtensions]);
const computeItemKey = VirtuosoUtil.useCreateGroupedComputeItemKey(
groupCounts,
useCallback((index) => groupedExtensions[index][LANGUAGE], [groupedExtensions]),
@@ -244,8 +247,10 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
message={t`Unable to load data`}
messageExtra={getErrorMessage(error)}
retry={() => {
if (serverSettingsError) {
refetchServerSettings().catch(defaultPromiseErrorHandler('Extensions::refetchServerSettings'));
if (extensionStoresRequest.error) {
extensionStoresRequest
.refetch()
.catch(defaultPromiseErrorHandler('Extensions::refetchExtensionsStores'));
}
if (extensionsError) {
@@ -267,9 +272,13 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
paddingTop: '20px',
}}
>
<Typography>{t`You have to add a extension repository to be able to install extensions`}</Typography>
<Button component={Link} variant="contained" to={AppRoutes.settings.children.browse.path}>
{t`Settings`}
<Typography>{t`You have to add a extension store to be able to install extensions`}</Typography>
<Button
component={Link}
variant="contained"
to={AppRoutes.settings.children.browse.children.extensionStores.path}
>
{t`Add extension store`}
</Button>
</Stack>
);
@@ -306,7 +315,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
<ExtensionCard
extension={item}
handleUpdate={handleExtensionUpdate}
showSourceRepo={areMultipleReposInUse}
showSourceStore={areMultipleReposInUse}
forcedState={
updatingExtensionIds.includes(item.pkgName) ? ExtensionState.UPDATING : undefined
}

View File

@@ -23,7 +23,7 @@ import {
EXTENSION_ACTION_TO_STATE_MAP,
INSTALLED_STATE_TO_TRANSLATION_MAP,
} from '@/features/extension/Extensions.constants.ts';
import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { getInstalledState, isNsfw, updateExtension } from '@/features/extension/Extensions.utils.ts';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { ListCardAvatar } from '@/base/components/lists/cards/ListCardAvatar.tsx';
import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx';
@@ -35,7 +35,7 @@ import { languageCodeToName } from '@/base/utils/Languages.ts';
interface IProps {
extension: TExtension;
handleUpdate: () => void;
showSourceRepo: boolean;
showSourceStore: boolean;
forcedState?: ExtensionState;
}
@@ -43,9 +43,20 @@ export function ExtensionCard(props: IProps) {
const { t } = useLingui();
const {
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw, repo },
extension: {
name,
lang,
versionName,
isInstalled,
hasUpdate,
isObsolete,
pkgName,
iconUrl,
contentWarning,
extensionStore,
},
handleUpdate,
showSourceRepo,
showSourceStore,
forcedState,
} = props;
const [localInstalledState, setInstalledState] = useState<InstalledStates>(
@@ -117,11 +128,14 @@ export function ExtensionCard(props: IProps) {
<Typography variant="h6" component="h3">
{name}
</Typography>
{showSourceStore && !!extensionStore && (
<Typography variant="caption">{extensionStore.name}</Typography>
)}
<Typography variant="caption">
{isInstalled ? `${languageCodeToName(lang)}` : ''}
{isInstalled ? (
<Typography variant="caption" sx={{ px: 1 }}>
-
</Typography>
) : (
''
@@ -130,17 +144,17 @@ export function ExtensionCard(props: IProps) {
{isObsolete && (
<>
<Typography variant="caption" sx={{ px: 1 }}>
-
</Typography>
<Typography variant="caption" color="warning" sx={{ textTransform: 'uppercase' }}>
{t`Obsolete`}
</Typography>
</>
)}
{isNsfw && (
{isNsfw(contentWarning) && (
<>
<Typography variant="caption" sx={{ px: 1 }}>
-
</Typography>
<Typography variant="caption" color="error">
18+
@@ -148,7 +162,6 @@ export function ExtensionCard(props: IProps) {
</>
)}
</Typography>
{showSourceRepo && <Typography variant="caption">{repo}</Typography>}
</Stack>
{isInstalled && (
<CustomTooltip title={t`Settings`}>

View File

@@ -10,11 +10,10 @@ import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import { Trans, useLingui } from '@lingui/react/macro';
import { useLingui } from '@lingui/react/macro';
import { plural } from '@lingui/core/macro';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { MutableListSetting } from '@/base/components/settings/MutableListSetting.tsx';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import {
createUpdateMetadataServerSettings,
@@ -32,8 +31,10 @@ import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import ErrorOutlineOutlinedIcon from '@mui/icons-material/ErrorOutlineOutlined';
import ListSubheader from '@mui/material/ListSubheader';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
type ExtensionsSettings = Pick<GqlServerSettings, 'maxSourcesInParallel' | 'localSourcePath' | 'extensionRepos'>;
type ExtensionsSettings = Pick<GqlServerSettings, 'maxSourcesInParallel' | 'localSourcePath'>;
export const BrowseSettings = () => {
const { t } = useLingui();
@@ -42,6 +43,9 @@ export const BrowseSettings = () => {
const { data, loading, error, refetch } = requestManager.useGetServerSettings();
const [mutateSettings] = requestManager.useUpdateServerSettings();
const extensionStoresRequest = requestManager.useGetExtensionStores();
const extensionStoreCount = extensionStoresRequest.data?.extensionStores.totalCount;
const updateSetting = <Setting extends keyof ExtensionsSettings>(
setting: Setting,
@@ -108,34 +112,18 @@ export const BrowseSettings = () => {
stepSize={1}
handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)}
/>
<MutableListSetting
settingName={t`Extension repositories`}
description={t`Add repositories from which extensions can be installed`}
dialogDisclaimer={
<Trans>
<strong>
Suwayomi does not provide any support for 3rd party repositories or extensions!
</strong>
<br />
Use with caution as there could be malicious actors making those repositories.
<br />
You as the user need to verify the security and that you trust any repository or extension.
</Trans>
}
handleChange={(repos) => {
updateSetting('extensionRepos', repos);
requestManager.clearExtensionCache();
}}
valueInfos={serverSettings.extensionRepos.map((extensionRepo) => [extensionRepo])}
addItemButtonTitle={t`Add repository`}
placeholder="https://github.com/MY_ACCOUNT/MY_REPO/tree/repo"
validateItem={(repo) =>
!!repo.match(
/https:\/\/(www\.|raw\.)?(github|githubusercontent)\.com\/([^/]+)\/([^/]+)((\/tree|\/blob)?\/([^/\n]*))?(\/([^/\n]*\.json)?)?/g,
)
}
invalidItemError={t`Invalid repository url`}
/>
<ListItemLink to={AppRoutes.settings.children.browse.children.extensionStores.path}>
<ListItemText
primary={t`Extension stores`}
secondary={
extensionStoreCount &&
plural(extensionStoreCount, {
one: '# extension store',
other: '# extension stores',
})
}
/>
</ListItemLink>
<TextSetting
settingName={t`Local source location`}
dialogDescription={t`The path to the directory on the server where local source files are saved in`}

View File

@@ -81,7 +81,7 @@ export function Sources({ tabsMenuHeight }: { tabsMenuHeight: number }) {
[sources],
);
const areSourcesFromDifferentRepos = useMemo(
() => SourceService.areFromMultipleRepos(filteredSources),
() => SourceService.areFromMultipleStores(filteredSources),
[filteredSources],
);
const visibleSources = useMemo(

View File

@@ -30,6 +30,7 @@ import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { languageCodeToName } from '@/base/utils/Languages.ts';
import { SourceContentType } from '@/features/source/Source.types.ts';
import { isNsfw } from '@/features/extension/Extensions.utils.ts';
interface IProps {
source: GetSourcesListQuery['sources']['nodes'][number];
@@ -47,8 +48,8 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
lang,
iconUrl,
supportsLatest,
isNsfw,
extension: { repo },
contentWarning,
extension: { extensionStore },
} = source;
const { isPinned } = useGetSourceMetadata(source);
@@ -92,13 +93,15 @@ export const SourceCard: React.FC<IProps> = (props: IProps) => {
</Typography>
<Typography variant="caption">
{showLanguage && languageCodeToName(lang)}
{isNsfw && (
{isNsfw(contentWarning) && (
<Typography variant="caption" color="error">
{' 18+'}
</Typography>
)}
</Typography>
{showSourceRepo && <Typography variant="caption">{repo}</Typography>}
{showSourceRepo && extensionStore && (
<Typography variant="caption">{extensionStore.name}</Typography>
)}
</Stack>
{supportsLatest && (
<Button

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import type { ExtensionType } from '@/lib/graphql/generated/graphql-base.types.ts';
import type { ExtensionStoreType, ExtensionType } from '@/lib/graphql/generated/graphql-base.types.ts';
export enum ExtensionAction {
UPDATE = 'UPDATE',
@@ -36,15 +36,14 @@ export type TExtension = Pick<
| 'pkgName'
| 'name'
| 'lang'
| 'versionCode'
| 'versionCodeLong'
| 'versionName'
| 'iconUrl'
| 'repo'
| 'isNsfw'
| 'contentWarning'
| 'isInstalled'
| 'isObsolete'
| 'hasUpdate'
>;
> & { extensionStore?: Pick<ExtensionStoreType, 'indexUrl' | 'name'> | null };
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, TExtension[]][];

View File

@@ -25,6 +25,9 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { i18n } from '@/i18n';
import { ContentWarning } from '@/lib/graphql/generated/graphql-base.types.ts';
export const isNsfw = (contentWarning: ContentWarning): boolean => contentWarning !== ContentWarning.Safe;
export const getInstalledState = (
isInstalled: boolean,
@@ -130,7 +133,7 @@ export const filterExtensions = (
normalizedSelectedLanguages.includes(toComparableLanguage(extension.lang)) ||
extension.isInstalled,
)
.filter((extension) => showNsfw === undefined || showNsfw || !extension.isNsfw)
.filter((extension) => showNsfw === undefined || showNsfw || !isNsfw(extension.contentWarning))
.filter((extension) => query == null || enhancedCleanup(extension.name).includes(enhancedCleanup(query)));
};

View File

@@ -12,19 +12,19 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SpinnerImage } from '@/base/components/SpinnerImage.tsx';
import type { TExtension } from '@/features/extension/Extensions.types.ts';
export const Header = ({ name, pkgName, iconUrl, repo }: TExtension) => (
export const Header = ({ name, pkgName, iconUrl, extensionStore }: TExtension) => (
<Stack sx={{ alignItems: 'center' }}>
<SpinnerImage alt={name} src={requestManager.getValidImgUrlFor(iconUrl)} ignoreQueue />
<Typography variant="h5" component="h2">
{name}
</Typography>
{extensionStore && (
<Typography variant="body1" color="textSecondary">
{extensionStore.name}
</Typography>
)}
<Typography variant="body2" color="textSecondary">
{pkgName.replace('eu.kanade.tachiyomi.extension.', '')}
</Typography>
{repo && (
<Typography variant="body2" color="textSecondary">
{repo}
</Typography>
)}
</Stack>
);

View File

@@ -11,15 +11,18 @@ import { useLingui } from '@lingui/react/macro';
import type { TExtension } from '@/features/extension/Extensions.types.ts';
import { ExtensionMetadata } from '@/features/extension/info/components/ExtensionMetadata.tsx';
import { languageCodeToName } from '@/base/utils/Languages.ts';
import { isNsfw } from '@/features/extension/Extensions.utils.ts';
export const Meta = ({ versionName, lang, isNsfw }: TExtension) => {
export const Meta = ({ versionName, lang, contentWarning }: TExtension) => {
const { t } = useLingui();
return (
<Stack sx={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<ExtensionMetadata title={t`Version`} value={versionName} />
<ExtensionMetadata title={t`Language`} value={languageCodeToName(lang)} />
{isNsfw && <ExtensionMetadata title={t`Age rating`} value="18+" valueProps={{ color: 'error' }} />}
{isNsfw(contentWarning) && (
<ExtensionMetadata title={t`Age rating`} value="18+" valueProps={{ color: 'error' }} />
)}
</Stack>
);
};

View File

@@ -0,0 +1,286 @@
/*
* 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 { requestManager } from '@/lib/requests/RequestManager.ts';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { useLingui } from '@lingui/react/macro';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { STABLE_EMPTY_ARRAY } from '@/base/Base.constants.ts';
import type { ExtensionStoreFieldsFragment } from '@/lib/graphql/generated/graphql.ts';
import { VirtuosoPersisted } from '@/lib/virtuoso/Component/VirtuosoPersisted.tsx';
import Card from '@mui/material/Card';
import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import Stack from '@mui/material/Stack';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import IconButton from '@mui/material/IconButton';
import { plural } from '@lingui/core/macro';
import DeleteIcon from '@mui/icons-material/Delete';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { IconBrowser } from '@/assets/icons/IconBrowser.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { DiscordIcon } from '@/assets/icons/svg/DiscordIcon.tsx';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import Box from '@mui/material/Box';
import { ContentWarning } from '@/lib/graphql/generated/graphql-base.types.ts';
import { DEFAULT_FULL_FAB_HEIGHT, StyledFab } from '@/base/components/buttons/StyledFab.tsx';
import AddIcon from '@mui/icons-material/Add';
import { AwaitableComponent, type AwaitableComponentProps } from 'awaitable-component';
import { TextSettingDialog } from '@/base/components/settings/text/TextSettingDialog.tsx';
import { useMemo } from 'react';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
const ExtensionStoreCard = ({
indexUrl,
name,
contactDiscord,
contactWebsite,
extensions: { totalCount: extensionCount },
}: ExtensionStoreFieldsFragment) => {
const { t } = useLingui();
const [removeExtensionStore, { loading }] = requestManager.useRemoveExtensionStore();
const [fetchExtensions] = requestManager.useExtensionListFetch();
const nsfwExtensionsData = requestManager.useGetExtensionList({
variables: {
condition: { storeIndexUrl: indexUrl },
filter: { contentWarning: { greaterThanOrEqualTo: ContentWarning.Mixed } },
},
});
const installedExtensionsData = requestManager.useGetExtensionList({
variables: { condition: { storeIndexUrl: indexUrl, isInstalled: true } },
});
const isNSfw = nsfwExtensionsData.dataState === 'complete' && nsfwExtensionsData.data?.extensions.totalCount;
const installedExtensionCount = installedExtensionsData.data?.extensions.totalCount;
return (
<Box sx={{ p: 1, pb: 0 }}>
<Card>
<ListCardContent sx={{ flexWrap: 'wrap', justifyContent: 'space-between' }}>
<Stack>
<TypographyMaxLines variant="h6" component="h3">
{name}
</TypographyMaxLines>
<TypographyMaxLines variant="caption" color="textSecondary">
{isNSfw ? (
<>
<TypographyMaxLines sx={{ display: 'inline' }} variant="caption" color="error">
{' '}
+18
</TypographyMaxLines>
{extensionCount && ' - '}
</>
) : null}
{!!extensionCount &&
plural(extensionCount, {
one: '# extension',
other: '# extensions',
})}
{installedExtensionCount
? ` - ${plural(installedExtensionCount, {
one: '# installed',
other: '# installed',
})}`
: null}
</TypographyMaxLines>
</Stack>
<Stack
sx={{
flexDirection: 'row',
justifyContent: 'end',
flexGrow: 1,
}}
>
{contactWebsite && (
<CustomTooltip title={t`Open website`} disabled={!contactWebsite}>
<IconButton
disabled={!contactWebsite}
href={contactWebsite ?? undefined}
target="_blank"
rel="noreferrer"
color="inherit"
>
<IconBrowser />
</IconButton>
</CustomTooltip>
)}
{contactDiscord && (
<CustomTooltip title={t`Open discord`} disabled={!contactDiscord}>
<IconButton
href={contactDiscord ?? undefined}
target="_blank"
rel="noreferrer"
color="inherit"
>
<DiscordIcon />
</IconButton>
</CustomTooltip>
)}
<CustomTooltip title={t`Copy index url`}>
<IconButton
onClick={async () => {
await navigator.clipboard.writeText(indexUrl);
makeToast(t`Copied to clipboard`, 'info');
}}
color="inherit"
>
<ContentCopyIcon />
</IconButton>
</CustomTooltip>
<CustomTooltip disabled={loading} title={t`Delete`}>
<IconButton
disabled={loading}
onClick={async () => {
try {
await Confirmation.show({
title: t`Are you sure?`,
message: t`You are about to remove "${name}" from the extension stores`,
});
try {
await removeExtensionStore({ variables: { input: { indexUrl } } });
requestManager.clearExtensionCache();
fetchExtensions().catch(
defaultPromiseErrorHandler(
'ExtensionStoreCard::remove::fetchExtensions',
),
);
} catch (e) {
makeToast(
t`Could not remove extension store "${name}"`,
'error',
getErrorMessage(e),
);
}
} catch (e) {
// ignore
}
}}
color="inherit"
>
<DeleteIcon />
</IconButton>
</CustomTooltip>
</Stack>
</ListCardContent>
</Card>
</Box>
);
};
const AddExtensionStoreDialog = ({
onExitComplete,
onDismiss,
isVisible,
indexUrl,
processing = false,
startCreation,
}: AwaitableComponentProps<void> & {
indexUrl?: string;
processing?: boolean;
startCreation: (indexUrl: string) => void;
}) => {
const { t } = useLingui();
return (
<TextSettingDialog
settingName={t`Add extension store`}
handleChange={startCreation}
isDialogOpen={isVisible}
setIsDialogOpen={noOp}
onDismiss={onDismiss}
onExitComplete={onExitComplete}
value={indexUrl}
disabled={processing}
/>
);
};
export const ExtensionStores = () => {
const { t } = useLingui();
useAppTitle(t`Extension stores`);
const [addExtensionStore] = requestManager.useAddExtensionStore();
const { data, loading, error, refetch, dataState } = requestManager.useGetExtensionStores();
const [fetchExtensions] = requestManager.useExtensionListFetch();
const extensionStores = useMemo(() => {
if (!data?.extensionStores.nodes) {
return STABLE_EMPTY_ARRAY;
}
return data.extensionStores.nodes.toSorted((a, b) => a.name.localeCompare(b.name));
}, [data?.extensionStores.nodes]);
if (loading && dataState !== 'complete') {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t`Unable to load extension stores`}
messageExtra={getErrorMessage(error)}
retry={() => {
refetch().catch(defaultPromiseErrorHandler('ExtensionStores::refetchServerMetadataSettings'));
}}
/>
);
}
return (
<Box sx={{ pb: DEFAULT_FULL_FAB_HEIGHT }}>
<VirtuosoPersisted
persistKey="extension-store-list"
useWindowScroll
overscan={window.innerHeight * 0.5}
totalCount={extensionStores.length}
computeItemKey={(index) => extensionStores[index].indexUrl}
itemContent={(index) => <ExtensionStoreCard {...extensionStores[index]} />}
/>
<StyledFab
variant="extended"
color="primary"
sx={{ gap: 1 }}
onClick={() => {
const addStoreDialog = AwaitableComponent.showControlled(AddExtensionStoreDialog, {
startCreation: async (indexUrl) => {
const request = addExtensionStore({ variables: { input: { indexUrl } } });
addStoreDialog.update({ indexUrl, processing: true });
try {
await request;
requestManager.clearExtensionCache();
fetchExtensions().catch(
defaultPromiseErrorHandler('ExtensionStores::add:fetchExtensions'),
);
addStoreDialog.submit();
} catch (e) {
makeToast(t`Could not add extension store`, 'error', getErrorMessage(e));
addStoreDialog.update({ processing: false });
}
},
});
}}
>
<AddIcon />
{t`Add`}
</StyledFab>
</Box>
);
};

View File

@@ -70,8 +70,8 @@ export type SourceIdInfo = Pick<SourceType, 'id'>;
export type SourceLanguageInfo = Pick<SourceType, 'lang'>;
export type SourceNameInfo = Pick<SourceType, 'name'>;
export type SourceDisplayNameInfo = Pick<SourceType, 'displayName'>;
export type SourceNsfwInfo = Pick<SourceType, 'isNsfw'>;
export type SourceRepoInfo = { extension: Pick<ExtensionType, 'repo'> };
export type SourceNsfwInfo = Pick<SourceType, 'contentWarning'>;
export type SourceStoreInfo = { extension: Pick<ExtensionType, 'storeIndexUrl'> };
export type SourceMetaInfo = { meta: SourceMetaFieldsFragment[] };
export type SourceConfigurableInfo = Pick<SourceType, 'isConfigurable'>;
export type SourceIconInfo = Pick<SourceType, 'iconUrl'>;

View File

@@ -423,10 +423,10 @@ export function SourceMangas() {
<>
<AppbarSearch />
<SourceGridLayout />
<CustomTooltip title={t`Open in WebView`} disabled={!source?.baseUrl}>
<CustomTooltip title={t`Open in WebView`} disabled={!source?.homeUrl}>
<IconButton
disabled={!source?.baseUrl}
href={source?.baseUrl ? requestManager.getWebviewUrl(source?.baseUrl) : ''}
disabled={!source?.homeUrl}
href={source?.homeUrl ? requestManager.getWebviewUrl(source?.homeUrl) : ''}
rel="noreferrer"
target="_blank"
color="inherit"

View File

@@ -14,7 +14,7 @@ import type {
SourceLanguageInfo,
SourceNsfwInfo,
SourceMetaInfo,
SourceRepoInfo,
SourceStoreInfo,
} from '@/features/source/Source.types.ts';
import {
DefaultLanguage,
@@ -34,6 +34,7 @@ import type { SourceBaseFieldsFragment } from '@/lib/graphql/generated/graphql.t
import { requestManager } from '@/lib/requests/RequestManager.ts';
import type { DocumentNode, Unmasked } from '@apollo/client';
import { SOURCE_BASE_FIELDS } from '@/lib/graphql/source/SourceFragments.ts';
import { isNsfw as isNsfwFnc } from '@/features/extension/Extensions.utils.ts';
export class Sources {
static readonly LOCAL_SOURCE_ID = '0';
@@ -134,7 +135,7 @@ export class Sources {
const normalizedLanguages = toComparableLanguages(toUniqueLanguageCodes(languages ?? []));
const filters: [Condition: any, CheckKeepLocalSource: boolean, Filter: (source: Source) => boolean][] = [
[isNsfw, true, (source: Source) => source.isNsfw === isNsfw],
[isNsfw, true, (source: Source) => isNsfwFnc(source.contentWarning) === isNsfw],
[
languages,
true,
@@ -157,14 +158,14 @@ export class Sources {
}, sources);
}
static areFromMultipleRepos<Source extends SourceIdInfo & SourceRepoInfo>(sources: Source[]): boolean {
const repo = sources.find((source) => !!source.extension.repo)?.extension.repo;
static areFromMultipleStores<Source extends SourceIdInfo & SourceStoreInfo>(sources: Source[]): boolean {
const store = sources.find((source) => !!source.extension.storeIndexUrl)?.extension.storeIndexUrl;
if (!repo || !sources.length) {
if (!store || !sources.length) {
return false;
}
return sources.some((source) => source.extension.repo !== repo && !Sources.isLocalSource(source));
return sources.some((source) => source.extension.storeIndexUrl !== store && !Sources.isLocalSource(source));
}
static getLastUsedSource<Source extends SourceIdInfo & SourceMetaInfo>(