2021-03-26 04:17:02 +04:30
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
2021-01-26 23:32:12 +03:30
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
2023-05-18 13:17:41 +02:00
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
2021-01-26 23:32:12 +03:30
|
|
|
|
2023-11-24 03:20:16 +01:00
|
|
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
2021-09-09 05:14:17 +04:30
|
|
|
import { fromEvent } from 'file-selector';
|
2021-09-09 17:51:22 +04:30
|
|
|
import IconButton from '@mui/material/IconButton';
|
|
|
|
|
import AddIcon from '@mui/icons-material/Add';
|
2023-04-21 00:27:42 +02:00
|
|
|
import { StringParam, useQueryParam } from 'use-query-params';
|
2024-04-14 15:50:12 +02:00
|
|
|
import Button from '@mui/material/Button';
|
|
|
|
|
import Stack from '@mui/material/Stack';
|
|
|
|
|
import Tooltip from '@mui/material/Tooltip';
|
|
|
|
|
import Typography from '@mui/material/Typography';
|
2023-03-23 13:59:32 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2024-01-15 23:22:35 +01:00
|
|
|
import { Link } from 'react-router-dom';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
2023-06-05 01:42:39 +02:00
|
|
|
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language';
|
2024-04-07 15:22:03 +02:00
|
|
|
import { useLocalStorage } from '@/util/useStorage.tsx';
|
2023-04-21 00:27:42 +02:00
|
|
|
import {
|
|
|
|
|
ExtensionState,
|
|
|
|
|
GroupedExtensions,
|
|
|
|
|
GroupedExtensionsResult,
|
|
|
|
|
isExtensionStateOrLanguage,
|
2024-07-06 15:55:58 +02:00
|
|
|
TExtension,
|
2023-04-21 00:27:42 +02:00
|
|
|
translateExtensionLanguage,
|
2023-06-05 01:42:39 +02:00
|
|
|
} from '@/screens/util/Extensions';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { AppbarSearch } from '@/components/util/AppbarSearch';
|
|
|
|
|
import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder';
|
2024-09-07 01:23:48 +02:00
|
|
|
import { makeToast } from '@/components/util/Toast';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { LangSelect } from '@/components/navbar/action/LangSelect';
|
|
|
|
|
import { ExtensionCard } from '@/components/ExtensionCard';
|
|
|
|
|
import { NavBarContext } from '@/components/context/NavbarContext.tsx';
|
2024-01-02 23:32:49 +01:00
|
|
|
import { StyledGroupedVirtuoso } from '@/components/virtuoso/StyledGroupedVirtuoso.tsx';
|
|
|
|
|
import { StyledGroupHeader } from '@/components/virtuoso/StyledGroupHeader.tsx';
|
|
|
|
|
import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx';
|
2024-04-29 15:29:33 +02:00
|
|
|
import { EmptyViewAbsoluteCentered } from '@/components/util/EmptyViewAbsoluteCentered.tsx';
|
2024-04-27 22:28:55 +02:00
|
|
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
2021-03-08 21:04:42 +03:30
|
|
|
|
2021-12-19 18:06:27 +03:30
|
|
|
const LANGUAGE = 0;
|
|
|
|
|
const EXTENSIONS = 1;
|
|
|
|
|
|
2024-07-06 15:55:58 +02:00
|
|
|
function getExtensionsInfo(extensions: TExtension[]): {
|
2023-06-23 00:05:33 +02:00
|
|
|
allLangs: string[];
|
|
|
|
|
groupedExtensions: GroupedExtensionsResult;
|
|
|
|
|
} {
|
|
|
|
|
const allLangs: string[] = [];
|
2023-06-22 22:13:32 +02:00
|
|
|
const sortedExtensions: GroupedExtensions = {
|
2023-06-22 22:13:10 +02:00
|
|
|
[ExtensionState.OBSOLETE]: [],
|
2023-04-21 00:27:42 +02:00
|
|
|
[ExtensionState.INSTALLED]: [],
|
|
|
|
|
[ExtensionState.UPDATE_PENDING]: [],
|
|
|
|
|
[DefaultLanguage.ALL]: [],
|
|
|
|
|
[DefaultLanguage.OTHER]: [],
|
|
|
|
|
[DefaultLanguage.LOCAL_SOURCE]: [],
|
|
|
|
|
};
|
2021-03-08 21:04:42 +03:30
|
|
|
extensions.forEach((extension) => {
|
2023-06-22 22:13:32 +02:00
|
|
|
if (sortedExtensions[extension.lang] === undefined) {
|
|
|
|
|
sortedExtensions[extension.lang] = [];
|
2023-02-06 10:06:33 +01:00
|
|
|
if (extension.lang !== 'all') {
|
|
|
|
|
allLangs.push(extension.lang);
|
|
|
|
|
}
|
2021-03-08 21:04:42 +03:30
|
|
|
}
|
2023-09-01 20:28:23 +02:00
|
|
|
if (extension.isInstalled) {
|
2021-03-29 02:47:51 +04:30
|
|
|
if (extension.hasUpdate) {
|
2023-06-22 22:13:32 +02:00
|
|
|
sortedExtensions[ExtensionState.UPDATE_PENDING].push(extension);
|
2023-06-22 22:13:10 +02:00
|
|
|
return;
|
2021-03-29 02:47:51 +04:30
|
|
|
}
|
2023-09-01 20:28:23 +02:00
|
|
|
if (extension.isObsolete) {
|
2023-06-22 22:13:32 +02:00
|
|
|
sortedExtensions[ExtensionState.OBSOLETE].push(extension);
|
2023-06-22 22:13:10 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 22:13:32 +02:00
|
|
|
sortedExtensions[ExtensionState.INSTALLED].push(extension);
|
2021-03-08 21:04:42 +03:30
|
|
|
} else {
|
2023-06-22 22:13:32 +02:00
|
|
|
sortedExtensions[extension.lang].push(extension);
|
2021-03-08 21:04:42 +03:30
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2021-03-09 16:44:09 +03:30
|
|
|
allLangs.sort(langSortCmp);
|
2023-04-21 00:27:42 +02:00
|
|
|
const result: GroupedExtensionsResult<ExtensionState | DefaultLanguage | string> = [
|
2023-06-22 22:13:32 +02:00
|
|
|
[ExtensionState.OBSOLETE, sortedExtensions[ExtensionState.OBSOLETE]],
|
|
|
|
|
[ExtensionState.UPDATE_PENDING, sortedExtensions[ExtensionState.UPDATE_PENDING]],
|
|
|
|
|
[ExtensionState.INSTALLED, sortedExtensions[ExtensionState.INSTALLED]],
|
|
|
|
|
[DefaultLanguage.ALL, sortedExtensions[DefaultLanguage.ALL]],
|
|
|
|
|
[DefaultLanguage.OTHER, sortedExtensions[DefaultLanguage.OTHER]],
|
|
|
|
|
[DefaultLanguage.LOCAL_SOURCE, sortedExtensions[DefaultLanguage.LOCAL_SOURCE]],
|
2021-12-19 13:25:35 +05:30
|
|
|
];
|
|
|
|
|
|
2023-06-22 22:13:32 +02:00
|
|
|
const langExt: GroupedExtensionsResult = allLangs.map((lang) => [lang, sortedExtensions[lang]]);
|
2021-03-08 21:04:42 +03:30
|
|
|
|
2023-06-23 00:05:33 +02:00
|
|
|
return {
|
|
|
|
|
allLangs,
|
2024-07-11 17:55:00 +02:00
|
|
|
groupedExtensions: result.concat(langExt),
|
2023-06-23 00:05:33 +02:00
|
|
|
};
|
2021-03-08 21:04:42 +03:30
|
|
|
}
|
2021-01-19 14:47:07 +03:30
|
|
|
|
2024-07-11 17:22:52 +02:00
|
|
|
export function Extensions({ tabsMenuHeight }: { tabsMenuHeight: number }) {
|
2023-03-23 13:59:32 +01:00
|
|
|
const { t } = useTranslation();
|
2024-01-26 20:50:51 +01:00
|
|
|
const { setAction } = useContext(NavBarContext);
|
2023-03-23 13:59:32 +01:00
|
|
|
|
2024-05-05 00:45:50 +02:00
|
|
|
const {
|
|
|
|
|
data: serverSettingsData,
|
|
|
|
|
loading: areServerSettingsLoading,
|
|
|
|
|
error: serverSettingsError,
|
|
|
|
|
refetch: refetchServerSettings,
|
|
|
|
|
} = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true });
|
2024-01-15 23:22:35 +01:00
|
|
|
const areReposDefined = !!serverSettingsData?.settings.extensionRepos.length;
|
|
|
|
|
const areMultipleReposInUse = (serverSettingsData?.settings.extensionRepos.length ?? 0) > 1;
|
2024-01-06 18:38:16 +01:00
|
|
|
|
2022-11-02 21:48:22 +01:00
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
2023-02-08 18:19:26 +01:00
|
|
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
2021-09-05 01:25:37 +04:30
|
|
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
2021-12-19 13:25:35 +05:30
|
|
|
const [query] = useQueryParam('query', StringParam);
|
2021-03-08 21:04:42 +03:30
|
|
|
|
2023-11-24 03:20:16 +01:00
|
|
|
const [refetchExtensions, setRefetchExtensions] = useState({});
|
2024-05-05 00:45:50 +02:00
|
|
|
const [fetchExtensions, { data, loading: areExtensionsLoading, error: extensionsError }] =
|
|
|
|
|
requestManager.useExtensionListFetch();
|
2024-06-02 15:24:29 +02:00
|
|
|
const allExtensions = data?.fetchExtensions?.extensions;
|
2023-09-01 20:28:23 +02:00
|
|
|
|
2023-11-24 03:20:16 +01:00
|
|
|
const handleExtensionUpdate = useCallback(() => setRefetchExtensions({}), []);
|
|
|
|
|
|
2023-09-01 20:28:23 +02:00
|
|
|
useEffect(() => {
|
2023-11-04 22:40:38 +01:00
|
|
|
fetchExtensions();
|
2023-11-24 03:20:16 +01:00
|
|
|
}, [refetchExtensions]);
|
2023-02-06 10:06:33 +01:00
|
|
|
|
|
|
|
|
const filteredExtensions = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
(allExtensions ?? []).filter((ext) => {
|
|
|
|
|
const nsfwFilter = showNsfw || !ext.isNsfw;
|
|
|
|
|
if (!query) return nsfwFilter;
|
|
|
|
|
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
|
|
|
|
}),
|
|
|
|
|
[allExtensions, showNsfw, query],
|
|
|
|
|
);
|
|
|
|
|
|
2023-06-23 00:05:33 +02:00
|
|
|
const { allLangs, groupedExtensions } = useMemo(() => getExtensionsInfo(filteredExtensions), [filteredExtensions]);
|
|
|
|
|
|
|
|
|
|
const filteredGroupedExtensions = useMemo(
|
2023-02-06 10:06:33 +01:00
|
|
|
() =>
|
2023-06-23 00:05:33 +02:00
|
|
|
groupedExtensions
|
2023-02-06 10:06:33 +01:00
|
|
|
.filter((group) => group[EXTENSIONS].length > 0)
|
2023-04-21 00:27:42 +02:00
|
|
|
.filter((group) => isExtensionStateOrLanguage(group[LANGUAGE]) || shownLangs.includes(group[LANGUAGE])),
|
2023-06-23 00:05:33 +02:00
|
|
|
[shownLangs, groupedExtensions],
|
2023-02-06 10:06:33 +01:00
|
|
|
);
|
2021-01-19 14:47:07 +03:30
|
|
|
|
2024-01-02 23:32:49 +01:00
|
|
|
const groupCounts = useMemo(
|
2024-01-03 18:57:12 +01:00
|
|
|
() => filteredGroupedExtensions.map((extensionGroup) => extensionGroup[EXTENSIONS].length),
|
2024-01-02 23:32:49 +01:00
|
|
|
[filteredGroupedExtensions],
|
|
|
|
|
);
|
|
|
|
|
const visibleExtensions = useMemo(
|
|
|
|
|
() => filteredGroupedExtensions.map(([, extensions]) => extensions).flat(1),
|
|
|
|
|
[filteredGroupedExtensions],
|
|
|
|
|
);
|
2021-03-08 21:04:42 +03:30
|
|
|
|
2021-09-09 06:03:59 +04:30
|
|
|
const submitExternalExtension = (file: File) => {
|
2021-09-09 05:14:17 +04:30
|
|
|
if (file.name.toLowerCase().endsWith('apk')) {
|
2022-11-02 21:48:22 +01:00
|
|
|
if (inputRef.current) {
|
|
|
|
|
inputRef.current.value = '';
|
|
|
|
|
}
|
2021-09-09 06:03:59 +04:30
|
|
|
|
2023-03-23 13:59:32 +01:00
|
|
|
makeToast(t('extension.label.installing_file'), 'info');
|
2023-05-18 13:25:19 +02:00
|
|
|
requestManager
|
2023-09-01 20:28:23 +02:00
|
|
|
.installExternalExtension(file)
|
2023-05-23 13:27:38 +02:00
|
|
|
.response.then(() => {
|
2024-01-29 20:59:27 +01:00
|
|
|
handleExtensionUpdate();
|
2023-03-23 13:59:32 +01:00
|
|
|
makeToast(t('extension.label.installed_successfully'), 'success');
|
2021-09-09 05:14:17 +04:30
|
|
|
})
|
2023-03-23 13:59:32 +01:00
|
|
|
.catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
|
2021-09-09 05:14:17 +04:30
|
|
|
} else {
|
2023-03-23 13:59:32 +01:00
|
|
|
makeToast(t('global.error.label.invalid_file_type'), 'error');
|
2021-09-09 05:14:17 +04:30
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-06-23 00:05:33 +02:00
|
|
|
useEffect(() => {
|
|
|
|
|
setAction(
|
|
|
|
|
<>
|
|
|
|
|
<AppbarSearch />
|
2023-11-05 17:22:29 +01:00
|
|
|
<Tooltip title={t('extension.action.label.install_external')}>
|
2024-09-06 16:59:27 +02:00
|
|
|
<IconButton onClick={() => inputRef.current?.click()} size="large" color="inherit">
|
2023-11-05 17:22:29 +01:00
|
|
|
<AddIcon />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
|
2023-06-23 00:05:33 +02:00
|
|
|
<LangSelect shownLangs={shownLangs} setShownLangs={setShownLangs} allLangs={allLangs} />
|
|
|
|
|
</>,
|
|
|
|
|
);
|
2024-01-26 20:50:51 +01:00
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
setAction(null);
|
|
|
|
|
};
|
2023-10-15 13:04:31 +02:00
|
|
|
}, [t, shownLangs, allLangs]);
|
2023-06-23 00:05:33 +02:00
|
|
|
|
2022-11-02 21:48:22 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const dropHandler = async (e: Event) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const files = await fromEvent(e);
|
|
|
|
|
submitExternalExtension(files[0] as File);
|
|
|
|
|
};
|
2021-09-09 05:14:17 +04:30
|
|
|
|
2022-11-02 21:48:22 +01:00
|
|
|
const dragOverHandler = (e: Event) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
};
|
2021-09-09 05:14:17 +04:30
|
|
|
|
|
|
|
|
document.addEventListener('drop', dropHandler);
|
|
|
|
|
document.addEventListener('dragover', dragOverHandler);
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener('drop', dropHandler);
|
|
|
|
|
document.removeEventListener('dragover', dragOverHandler);
|
|
|
|
|
};
|
2022-11-02 21:48:22 +01:00
|
|
|
}, []);
|
2021-09-09 05:14:17 +04:30
|
|
|
|
2024-01-29 20:59:27 +01:00
|
|
|
const FileInputComponent = useMemo(
|
|
|
|
|
() => (
|
|
|
|
|
<input
|
|
|
|
|
type="file"
|
|
|
|
|
style={{ display: 'none' }}
|
|
|
|
|
ref={inputRef}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const file = e.target.files?.[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
submitExternalExtension(file);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2024-05-05 00:45:50 +02:00
|
|
|
const isLoading = areServerSettingsLoading || areExtensionsLoading;
|
|
|
|
|
const error = serverSettingsError ?? extensionsError;
|
|
|
|
|
|
2024-04-27 22:28:55 +02:00
|
|
|
if (isLoading) {
|
2021-10-24 22:56:10 +03:30
|
|
|
return <LoadingPlaceholder />;
|
2021-01-19 14:47:07 +03:30
|
|
|
}
|
2021-12-19 13:25:35 +05:30
|
|
|
|
2024-04-27 22:28:55 +02:00
|
|
|
if (error) {
|
|
|
|
|
return (
|
2024-04-29 15:29:33 +02:00
|
|
|
<EmptyViewAbsoluteCentered
|
2024-04-27 22:28:55 +02:00
|
|
|
message={t('global.error.label.failed_to_load_data')}
|
|
|
|
|
messageExtra={error.message}
|
2024-05-05 00:45:50 +02:00
|
|
|
retry={() => {
|
|
|
|
|
if (serverSettingsError) {
|
|
|
|
|
refetchServerSettings().catch(defaultPromiseErrorHandler('Extensions::refetchServerSettings'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (extensionsError) {
|
|
|
|
|
fetchExtensions().catch(defaultPromiseErrorHandler('Extensions::refetchExtensions'));
|
|
|
|
|
}
|
|
|
|
|
}}
|
2024-04-27 22:28:55 +02:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-15 23:22:35 +01:00
|
|
|
const showAddRepoInfo = !allExtensions?.length && !areReposDefined;
|
|
|
|
|
if (showAddRepoInfo) {
|
|
|
|
|
return (
|
2024-01-29 20:59:27 +01:00
|
|
|
<>
|
|
|
|
|
{FileInputComponent}
|
2024-09-03 17:15:47 +02:00
|
|
|
<Stack
|
|
|
|
|
sx={{
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
rowGap: '10px',
|
|
|
|
|
paddingTop: '20px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2024-01-29 20:59:27 +01:00
|
|
|
<Typography>{t('extension.label.add_repository_info')}</Typography>
|
2024-02-17 16:53:53 +01:00
|
|
|
<Button component={Link} variant="contained" to="/settings/browseSettings">
|
2024-01-29 20:59:27 +01:00
|
|
|
{t('settings.title')}
|
|
|
|
|
</Button>
|
|
|
|
|
</Stack>
|
|
|
|
|
</>
|
2024-01-15 23:22:35 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 21:04:42 +03:30
|
|
|
return (
|
|
|
|
|
<>
|
2024-01-29 20:59:27 +01:00
|
|
|
{FileInputComponent}
|
2024-01-02 23:32:49 +01:00
|
|
|
<StyledGroupedVirtuoso
|
2024-07-11 17:22:52 +02:00
|
|
|
heightToSubtract={tabsMenuHeight}
|
2024-01-02 23:32:49 +01:00
|
|
|
overscan={window.innerHeight * 0.5}
|
|
|
|
|
groupCounts={groupCounts}
|
|
|
|
|
groupContent={(index) => {
|
|
|
|
|
const [groupName] = filteredGroupedExtensions[index];
|
|
|
|
|
|
|
|
|
|
return (
|
2024-08-30 21:54:11 +02:00
|
|
|
<StyledGroupHeader key={groupName} variant="h5" component="h2" isFirstItem={index === 0}>
|
2024-01-02 23:32:49 +01:00
|
|
|
{translateExtensionLanguage(groupName)}
|
|
|
|
|
</StyledGroupHeader>
|
|
|
|
|
);
|
2021-12-19 18:06:27 +03:30
|
|
|
}}
|
|
|
|
|
itemContent={(index) => {
|
2024-01-02 23:32:49 +01:00
|
|
|
const item = visibleExtensions[index];
|
2023-09-01 20:28:23 +02:00
|
|
|
|
2024-01-02 23:32:49 +01:00
|
|
|
return (
|
2024-01-29 20:59:27 +01:00
|
|
|
<StyledGroupItemWrapper
|
|
|
|
|
key={`${item.pkgName}_${item.isInstalled}_${item.isObsolete}_${item.hasUpdate}`}
|
|
|
|
|
>
|
2024-01-06 18:38:16 +01:00
|
|
|
<ExtensionCard
|
|
|
|
|
extension={item}
|
|
|
|
|
handleUpdate={handleExtensionUpdate}
|
2024-01-09 22:09:31 +01:00
|
|
|
showSourceRepo={areMultipleReposInUse}
|
2024-01-06 18:38:16 +01:00
|
|
|
/>
|
2024-01-02 23:32:49 +01:00
|
|
|
</StyledGroupItemWrapper>
|
|
|
|
|
);
|
2021-12-19 18:06:27 +03:30
|
|
|
}}
|
2021-12-19 13:25:35 +05:30
|
|
|
/>
|
2021-03-08 21:04:42 +03:30
|
|
|
</>
|
|
|
|
|
);
|
2021-01-19 14:47:07 +03:30
|
|
|
}
|