From a403e6cb5fa5bbef59d9c79357566e34ef0ab10b Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Sat, 16 Aug 2025 16:03:32 +0200 Subject: [PATCH] Structure "extension" in sub-features --- src/App.tsx | 5 +- .../info/components/ActionButton.tsx | 50 ++++ .../info/components/ExtensionMetadata.tsx | 39 +++ .../extension/info/components/Header.tsx | 30 +++ .../extension/info/components/Meta.tsx | 31 +++ .../extension/info/components/SourceCard.tsx | 66 +++++ .../extension/info/screens/ExtensionInfo.tsx | 89 +++++++ .../extension/screens/ExtensionInfo.tsx | 251 ------------------ 8 files changed, 309 insertions(+), 252 deletions(-) create mode 100644 src/features/extension/info/components/ActionButton.tsx create mode 100644 src/features/extension/info/components/ExtensionMetadata.tsx create mode 100644 src/features/extension/info/components/Header.tsx create mode 100644 src/features/extension/info/components/Meta.tsx create mode 100644 src/features/extension/info/components/SourceCard.tsx create mode 100644 src/features/extension/info/screens/ExtensionInfo.tsx delete mode 100644 src/features/extension/screens/ExtensionInfo.tsx diff --git a/src/App.tsx b/src/App.tsx index c93ba60f..855c7fde 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,7 +45,10 @@ const { SourceConfigure } = loadable( lazyLoadFallback, ); const { SourceMangas } = loadable(() => import('@/features/source/browse/screens/SourceMangas.tsx'), lazyLoadFallback); -const { ExtensionInfo } = loadable(() => import('@/features/extension/screens/ExtensionInfo.tsx'), lazyLoadFallback); +const { ExtensionInfo } = loadable( + () => import('@/features/extension/info/screens/ExtensionInfo.tsx'), + lazyLoadFallback, +); const { Updates } = loadable(() => import('@/features/updates/screens/Updates.tsx'), lazyLoadFallback); const { History } = loadable(() => import('@/features/history/screens/History.tsx'), lazyLoadFallback); const { LibrarySettings } = loadable(() => import('@/features/library/screens/LibrarySettings.tsx'), lazyLoadFallback); diff --git a/src/features/extension/info/components/ActionButton.tsx b/src/features/extension/info/components/ActionButton.tsx new file mode 100644 index 00000000..c0de5c9a --- /dev/null +++ b/src/features/extension/info/components/ActionButton.tsx @@ -0,0 +1,50 @@ +/* + * 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 Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { INSTALLED_STATE_TO_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { getInstalledState, updateExtension } from '@/features/extension/Extensions.utils.ts'; +import { useBackButton } from '@/base/hooks/useBackButton.ts'; +import { ExtensionAction, InstalledState, TExtension } from '@/features/extension/Extensions.types.ts'; + +export const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtension) => { + const handleBack = useBackButton(); + const { t } = useTranslation(); + + const installedState = getInstalledState(isInstalled, isObsolete, hasUpdate); + + return ( + + + + ); +}; diff --git a/src/features/extension/info/components/ExtensionMetadata.tsx b/src/features/extension/info/components/ExtensionMetadata.tsx new file mode 100644 index 00000000..173d44c4 --- /dev/null +++ b/src/features/extension/info/components/ExtensionMetadata.tsx @@ -0,0 +1,39 @@ +/* + * 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 { ComponentProps } from 'react'; +import Divider from '@mui/material/Divider'; +import { Metadata } from '@/base/components/texts/Metadata.tsx'; + +export const ExtensionMetadata = ({ + addDivider = true, + ...props +}: ComponentProps & { addDivider?: boolean }) => ( + <> + + {addDivider && } + +); diff --git a/src/features/extension/info/components/Header.tsx b/src/features/extension/info/components/Header.tsx new file mode 100644 index 00000000..66f6bf0f --- /dev/null +++ b/src/features/extension/info/components/Header.tsx @@ -0,0 +1,30 @@ +/* + * 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 Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { SpinnerImage } from '@/base/components/SpinnerImage.tsx'; +import { TExtension } from '@/features/extension/Extensions.types.ts'; + +export const Header = ({ name, pkgName, iconUrl, repo }: TExtension) => ( + + + + {name} + + + {pkgName.replace('eu.kanade.tachiyomi.extension.', '')} + + {repo && ( + + {repo} + + )} + +); diff --git a/src/features/extension/info/components/Meta.tsx b/src/features/extension/info/components/Meta.tsx new file mode 100644 index 00000000..cc1fae29 --- /dev/null +++ b/src/features/extension/info/components/Meta.tsx @@ -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 Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { TExtension } from '@/features/extension/Extensions.types.ts'; +import { ExtensionMetadata } from '@/features/extension/info/components/ExtensionMetadata.tsx'; +import { languageCodeToName } from '@/base/utils/Languages.ts'; + +export const Meta = ({ versionName, lang, isNsfw }: TExtension) => { + const { t } = useTranslation(); + + return ( + + + + {isNsfw && ( + + )} + + ); +}; diff --git a/src/features/extension/info/components/SourceCard.tsx b/src/features/extension/info/components/SourceCard.tsx new file mode 100644 index 00000000..e37a633d --- /dev/null +++ b/src/features/extension/info/components/SourceCard.tsx @@ -0,0 +1,66 @@ +/* + * 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 Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import { Link } from 'react-router-dom'; +import Card from '@mui/material/Card'; +import IconButton from '@mui/material/IconButton'; +import SettingsIcon from '@mui/icons-material/Settings'; +import Switch from '@mui/material/Switch'; +import CardActionArea from '@mui/material/CardActionArea'; +import { MUIUtil } from '@/lib/mui/MUI.util.ts'; +import { AppRoutes } from '@/base/AppRoute.constants.ts'; +import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; +import { Sources } from '@/features/source/services/Sources.ts'; +import { translateExtensionLanguage } from '@/features/extension/Extensions.utils.ts'; +import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx'; +import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { makeToast } from '@/base/utils/Toast.ts'; +import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/features/source/services/SourceMetadata.ts'; +import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/features/source/Source.types.ts'; + +export const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => { + const { id, isConfigurable } = source; + + 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 ( + + + updateSetting('isEnabled', !isEnabled)}> + + + {translateExtensionLanguage(Sources.getLanguage(source))} + + {isConfigurable && ( + + e.stopPropagation()} + {...MUIUtil.preventRippleProp()} + > + + + + )} + + + + + + ); +}; diff --git a/src/features/extension/info/screens/ExtensionInfo.tsx b/src/features/extension/info/screens/ExtensionInfo.tsx new file mode 100644 index 00000000..392dc092 --- /dev/null +++ b/src/features/extension/info/screens/ExtensionInfo.tsx @@ -0,0 +1,89 @@ +/* + * 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 Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { useMemo } from 'react'; +import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; +import { requestManager } from '@/lib/requests/RequestManager.ts'; +import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; +import { getErrorMessage } from '@/lib/HelperFunctions.ts'; +import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; +import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; +import { languageSortComparator } from '@/base/utils/Languages.ts'; +import { assertIsDefined } from '@/base/Asserts.ts'; +import { Sources } from '@/features/source/services/Sources.ts'; +import { Header } from '@/features/extension/info/components/Header.tsx'; +import { Meta } from '@/features/extension/info/components/Meta.tsx'; +import { ActionButton } from '@/features/extension/info/components/ActionButton.tsx'; +import { SourceCard } from '@/features/extension/info/components/SourceCard.tsx'; + +export const ExtensionInfo = () => { + const { t } = useTranslation(); + const { pkgName } = useParams<{ pkgName: string }>(); + + useAppTitle(t('source.extension_info.title')); + + const extensionResponse = requestManager.useGetExtension(pkgName); + const sourcesResponse = requestManager.useGetSourceList(); + + const { extension } = extensionResponse.data ?? {}; + const sources = useMemo(() => { + if (!sourcesResponse.data?.sources) { + return []; + } + + return sourcesResponse.data.sources.nodes + .filter((source) => source.extension.pkgName === extension?.pkgName) + .sort((a, b) => languageSortComparator(Sources.getLanguage(a), Sources.getLanguage(b))); + }, [extension?.pkgName, sourcesResponse.data]); + + const isLoading = extensionResponse.loading || sourcesResponse.loading; + const error = extensionResponse.error || sourcesResponse.error; + + if (isLoading) { + return ; + } + + if (error) { + return ( + { + if (extensionResponse.error) { + extensionResponse + .refetch() + .catch(defaultPromiseErrorHandler('ExtensionInfo::extension::refetch')); + } + + if (sourcesResponse.error) { + sourcesResponse.refetch().catch(defaultPromiseErrorHandler('ExtensionInfo::sources::refetch')); + } + }} + /> + ); + } + + assertIsDefined(extension); + + return ( + +
+ + + + {sources.map((source) => ( + + ))} + + + ); +}; diff --git a/src/features/extension/screens/ExtensionInfo.tsx b/src/features/extension/screens/ExtensionInfo.tsx deleted file mode 100644 index 4b8de16f..00000000 --- a/src/features/extension/screens/ExtensionInfo.tsx +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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 Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import Button from '@mui/material/Button'; -import { useTranslation } from 'react-i18next'; -import { useParams, Link } from 'react-router-dom'; -import { ComponentProps, useMemo } from 'react'; -import Card from '@mui/material/Card'; -import IconButton from '@mui/material/IconButton'; -import SettingsIcon from '@mui/icons-material/Settings'; -import Divider from '@mui/material/Divider'; -import Switch from '@mui/material/Switch'; -import CardActionArea from '@mui/material/CardActionArea'; -import { SpinnerImage } from '@/base/components/SpinnerImage.tsx'; -import { Metadata } from '@/base/components/texts/Metadata.tsx'; -import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; -import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; -import { getErrorMessage } from '@/lib/HelperFunctions.ts'; -import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; -import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; -import { ExtensionAction, InstalledState, TExtension } from '@/features/extension/Extensions.types.ts'; -import { languageCodeToName, languageSortComparator } from '@/base/utils/Languages.ts'; -import { assertIsDefined } from '@/base/Asserts.ts'; -import { StyledGroupItemWrapper } from '@/base/components/virtuoso/StyledGroupItemWrapper.tsx'; -import { ListCardContent } from '@/base/components/lists/cards/ListCardContent.tsx'; -import { - getInstalledState, - translateExtensionLanguage, - updateExtension, -} from '@/features/extension/Extensions.utils.ts'; -import { Sources } from '@/features/source/services/Sources.ts'; -import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; -import { AppRoutes } from '@/base/AppRoute.constants.ts'; -import { SourceConfigurableInfo, SourceIdInfo, SourceLanguageInfo } from '@/features/source/Source.types.ts'; -import { MUIUtil } from '@/lib/mui/MUI.util.ts'; -import { useBackButton } from '@/base/hooks/useBackButton.ts'; -import { createUpdateSourceMetadata, useGetSourceMetadata } from '@/features/source/services/SourceMetadata.ts'; -import { makeToast } from '@/base/utils/Toast.ts'; -import { INSTALLED_STATE_TO_TRANSLATION_KEY_MAP } from '@/features/extension/Extensions.constants.ts'; - -const Header = ({ name, pkgName, iconUrl, repo }: TExtension) => ( - - - - {name} - - - {pkgName.replace('eu.kanade.tachiyomi.extension.', '')} - - {repo && ( - - {repo} - - )} - -); - -const ExtensionMetadata = ({ - addDivider = true, - ...props -}: ComponentProps & { addDivider?: boolean }) => ( - <> - - {addDivider && } - -); - -const Meta = ({ versionName, lang, isNsfw }: TExtension) => { - const { t } = useTranslation(); - - return ( - - - - {isNsfw && ( - - )} - - ); -}; - -const ActionButton = ({ pkgName, isInstalled, isObsolete, hasUpdate }: TExtension) => { - const handleBack = useBackButton(); - const { t } = useTranslation(); - - const installedState = getInstalledState(isInstalled, isObsolete, hasUpdate); - - return ( - - - - ); -}; - -const SourceCard = (source: SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo) => { - const { id, isConfigurable } = source; - - 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 ( - - - updateSetting('isEnabled', !isEnabled)}> - - - {translateExtensionLanguage(Sources.getLanguage(source))} - - {isConfigurable && ( - - e.stopPropagation()} - {...MUIUtil.preventRippleProp()} - > - - - - )} - - - - - - ); -}; - -const SourceList = ({ sources }: { sources: (SourceIdInfo & SourceLanguageInfo & SourceConfigurableInfo)[] }) => ( - - {sources.map((source) => ( - - ))} - -); - -export const ExtensionInfo = () => { - const { t } = useTranslation(); - const { pkgName } = useParams<{ pkgName: string }>(); - - useAppTitle(t('source.extension_info.title')); - - const extensionResponse = requestManager.useGetExtension(pkgName); - const sourcesResponse = requestManager.useGetSourceList(); - - const { extension } = extensionResponse.data ?? {}; - const sources = useMemo(() => { - if (!sourcesResponse.data?.sources) { - return []; - } - - return sourcesResponse.data.sources.nodes - .filter((source) => source.extension.pkgName === extension?.pkgName) - .sort((a, b) => languageSortComparator(Sources.getLanguage(a), Sources.getLanguage(b))); - }, [extension?.pkgName, sourcesResponse.data]); - - const isLoading = extensionResponse.loading || sourcesResponse.loading; - const error = extensionResponse.error || sourcesResponse.error; - - if (isLoading) { - return ; - } - - if (error) { - return ( - { - if (extensionResponse.error) { - extensionResponse - .refetch() - .catch(defaultPromiseErrorHandler('ExtensionInfo::extension::refetch')); - } - - if (sourcesResponse.error) { - sourcesResponse.refetch().catch(defaultPromiseErrorHandler('ExtensionInfo::sources::refetch')); - } - }} - /> - ); - } - - assertIsDefined(extension); - - return ( - -
- - - - - ); -};