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 (
-
-
-
-
-
-
- );
-};