From dd1199f3be04bbc7d86748a1ddd37340931ac75e Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:10:36 +0200 Subject: [PATCH] Integrate extension api v1.6 changes --- CHANGELOG.md | 1 + src/App.tsx | 12 +- src/assets/icons/svg/DiscordIcon.tsx | 22 ++ src/base/AppRoute.constants.ts | 6 + .../settings/text/TextSettingDialog.tsx | 21 +- src/features/browse/extensions/Extensions.tsx | 43 +-- .../extensions/components/ExtensionCard.tsx | 31 +- .../browse/screens/BrowseSettings.tsx | 50 ++- src/features/browse/sources/Sources.tsx | 2 +- .../browse/sources/components/SourceCard.tsx | 11 +- src/features/extension/Extensions.types.ts | 9 +- src/features/extension/Extensions.utils.ts | 5 +- .../extension/info/components/Header.tsx | 12 +- .../extension/info/components/Meta.tsx | 7 +- .../store/screens/ExtensionStores.tsx | 286 ++++++++++++++++++ src/features/source/Source.types.ts | 4 +- .../source/browse/screens/SourceMangas.tsx | 6 +- src/features/source/services/Sources.ts | 13 +- src/i18n/locales/en.po | 73 +++-- .../graphql/extension/ExtensionFragments.ts | 11 +- .../graphql/extension/ExtensionMutation.ts | 5 + .../store/ExtensionStoreFragments.ts | 25 ++ .../extension/store/ExtensionStoreMutation.ts | 34 +++ .../extension/store/ExtensionStoreQuery.ts | 38 +++ src/lib/graphql/generated/apollo-helpers.ts | 111 +++++++ .../graphql/generated/graphql-base.types.ts | 166 +++++++++- src/lib/graphql/generated/graphql.ts | 202 +++++++++++-- src/lib/graphql/settings/SettingsFragments.ts | 5 +- src/lib/graphql/source/SourceFragments.ts | 10 +- src/lib/requests/RequestManager.ts | 136 ++++++++- src/lib/requests/client/GraphQLClient.ts | 7 + versionToServerVersionMapping.json | 2 +- 32 files changed, 1190 insertions(+), 176 deletions(-) create mode 100644 src/assets/icons/svg/DiscordIcon.tsx create mode 100644 src/features/extension/store/screens/ExtensionStores.tsx create mode 100644 src/lib/graphql/extension/store/ExtensionStoreFragments.ts create mode 100644 src/lib/graphql/extension/store/ExtensionStoreMutation.ts create mode 100644 src/lib/graphql/extension/store/ExtensionStoreQuery.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b01bbd..d910119e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - (**Migration**) Show dialog on manual search selection to optionally open the search result entry instead of selecting it - (**Migration**) Show "manual search matches" under "other matches" - (**Source/Extension**) Rename language "All" to "Multi" +- (**Extension/Settings**) Renamed "extension repo" to "extension stores" - (**Reader**) Simplify changing settings in desktop sidebar - (**Reader**) Ignore tap zone clicks while window does not have focus - (**Reader**) Improve preloading pages from the previous/next chapter diff --git a/src/App.tsx b/src/App.tsx index 192f9727..bf873aca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -100,6 +100,10 @@ const { GlobalReaderSettings } = loadable( const { More } = loadable(() => import('@/features/settings/screens/More.tsx'), lazyLoadFallback); const { Reader } = loadable(() => import('@/features/reader/screens/Reader.tsx'), lazyLoadFallback); const { HistorySettings } = loadable(() => import('@/features/history/screens/HistorySettings.tsx'), lazyLoadFallback); +const { ExtensionStores } = loadable( + () => import('@/features/extension/store/screens/ExtensionStores.tsx'), + lazyLoadFallback, +); if (import.meta.env.DEV) { // Adds messages only in a dev environment @@ -323,7 +327,13 @@ const MainApp = () => { } /> } /> } /> - } /> + + } /> + } + /> + } /> } /> } /> diff --git a/src/assets/icons/svg/DiscordIcon.tsx b/src/assets/icons/svg/DiscordIcon.tsx new file mode 100644 index 00000000..c6de6675 --- /dev/null +++ b/src/assets/icons/svg/DiscordIcon.tsx @@ -0,0 +1,22 @@ +/* + * 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 type { SvgIconProps } from '@mui/material/SvgIcon'; +import SvgIcon from '@mui/material/SvgIcon'; + +export const DiscordIcon = (props: SvgIconProps) => ( + + + + + +); diff --git a/src/base/AppRoute.constants.ts b/src/base/AppRoute.constants.ts index e410bedd..327cd6de 100644 --- a/src/base/AppRoute.constants.ts +++ b/src/base/AppRoute.constants.ts @@ -109,6 +109,12 @@ export const AppRoutes = { browse: { match: 'browse', path: '/settings/browse', + children: { + extensionStores: { + match: 'extension-stores', + path: '/settings/browse/extension-stores', + }, + }, }, device: { match: 'device', diff --git a/src/base/components/settings/text/TextSettingDialog.tsx b/src/base/components/settings/text/TextSettingDialog.tsx index 67417592..efcc02c0 100644 --- a/src/base/components/settings/text/TextSettingDialog.tsx +++ b/src/base/components/settings/text/TextSettingDialog.tsx @@ -28,6 +28,9 @@ export type TextSettingDialogProps = { isDialogOpen: boolean; setIsDialogOpen: (open: boolean) => void; validate?: (value: string) => boolean; + onExitComplete?: () => void; + onDismiss?: () => void; + disabled?: boolean; }; export const TextSettingDialog = ({ @@ -41,6 +44,9 @@ export const TextSettingDialog = ({ isDialogOpen, setIsDialogOpen, validate = () => true, + onExitComplete, + onDismiss, + disabled, }: TextSettingDialogProps) => { const { t } = useLingui(); @@ -60,12 +66,21 @@ export const TextSettingDialog = ({ }, [value]); const closeDialog = (resetValue: boolean = true) => { + if (disabled) { + return; + } + if (resetValue) { setDialogValue(value ?? ''); setIsValidValue(true); } setIsDialogOpen(false); + + const rejected = resetValue; + if (rejected) { + onDismiss?.(); + } }; const updateSetting = () => { @@ -74,7 +89,7 @@ export const TextSettingDialog = ({ }; return ( - closeDialog()} fullWidth> + closeDialog()} fullWidth onTransitionExited={onExitComplete}> {dialogTitle} {!!dialogDescription && ( @@ -99,10 +114,10 @@ export const TextSettingDialog = ({ /> - - diff --git a/src/features/browse/extensions/Extensions.tsx b/src/features/browse/extensions/Extensions.tsx index 4d825f0d..4668e23a 100644 --- a/src/features/browse/extensions/Extensions.tsx +++ b/src/features/browse/extensions/Extensions.tsx @@ -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([]); 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', }} > - {t`You have to add a extension repository to be able to install extensions`} - ); @@ -306,7 +315,7 @@ export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) { 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( @@ -117,11 +128,14 @@ export function ExtensionCard(props: IProps) { {name} + {showSourceStore && !!extensionStore && ( + {extensionStore.name} + )} {isInstalled ? `${languageCodeToName(lang)}` : ''} {isInstalled ? ( - • + - ) : ( '' @@ -130,17 +144,17 @@ export function ExtensionCard(props: IProps) { {isObsolete && ( <> - • + - {t`Obsolete`} )} - {isNsfw && ( + {isNsfw(contentWarning) && ( <> - • + - 18+ @@ -148,7 +162,6 @@ export function ExtensionCard(props: IProps) { )} - {showSourceRepo && {repo}} {isInstalled && ( diff --git a/src/features/browse/screens/BrowseSettings.tsx b/src/features/browse/screens/BrowseSettings.tsx index 09f140bc..a33f3d46 100644 --- a/src/features/browse/screens/BrowseSettings.tsx +++ b/src/features/browse/screens/BrowseSettings.tsx @@ -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; +type ExtensionsSettings = Pick; 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: Setting, @@ -108,34 +112,18 @@ export const BrowseSettings = () => { stepSize={1} handleUpdate={(parallelSources) => updateSetting('maxSourcesInParallel', parallelSources)} /> - - - Suwayomi does not provide any support for 3rd party repositories or extensions! - -
- Use with caution as there could be malicious actors making those repositories. -
- You as the user need to verify the security and that you trust any repository or extension. - - } - 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`} - /> + + + SourceService.areFromMultipleRepos(filteredSources), + () => SourceService.areFromMultipleStores(filteredSources), [filteredSources], ); const visibleSources = useMemo( diff --git a/src/features/browse/sources/components/SourceCard.tsx b/src/features/browse/sources/components/SourceCard.tsx index 4675f110..771eb111 100644 --- a/src/features/browse/sources/components/SourceCard.tsx +++ b/src/features/browse/sources/components/SourceCard.tsx @@ -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 = (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 = (props: IProps) => {
{showLanguage && languageCodeToName(lang)} - {isNsfw && ( + {isNsfw(contentWarning) && ( {' 18+'} )} - {showSourceRepo && {repo}} + {showSourceRepo && extensionStore && ( + {extensionStore.name} + )} {supportsLatest && (