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-01-15 23:22:35 +01:00
|
|
|
import { Button, Stack, Tooltip, Typography, useMediaQuery } from '@mui/material';
|
2023-03-23 13:59:32 +01:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2024-01-02 23:32:49 +01:00
|
|
|
import { useTheme } from '@mui/material/styles';
|
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';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { useLocalStorage } from '@/util/useLocalStorage';
|
2023-04-21 00:27:42 +02:00
|
|
|
import {
|
|
|
|
|
ExtensionState,
|
|
|
|
|
GroupedExtensions,
|
|
|
|
|
GroupedExtensionsResult,
|
|
|
|
|
isExtensionStateOrLanguage,
|
|
|
|
|
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';
|
2023-06-05 01:42:39 +02:00
|
|
|
import { makeToaster } from '@/components/util/Toast';
|
2023-10-28 00:32:02 +02:00
|
|
|
import { LangSelect } from '@/components/navbar/action/LangSelect';
|
|
|
|
|
import { ExtensionCard } from '@/components/ExtensionCard';
|
2023-10-15 16:03:08 +02:00
|
|
|
import { PartialExtension } from '@/typings.ts';
|
2023-10-28 00:32:02 +02:00
|
|
|
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';
|
2021-03-08 21:04:42 +03:30
|
|
|
|
2021-12-19 18:06:27 +03:30
|
|
|
const LANGUAGE = 0;
|
|
|
|
|
const EXTENSIONS = 1;
|
|
|
|
|
|
2023-10-15 16:03:08 +02:00
|
|
|
function getExtensionsInfo(extensions: PartialExtension[]): {
|
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,
|
|
|
|
|
groupedExtensions: (result as GroupedExtensionsResult).concat(langExt),
|
|
|
|
|
};
|
2021-03-08 21:04:42 +03:30
|
|
|
}
|
2021-01-19 14:47:07 +03:30
|
|
|
|
2023-10-28 00:32:02 +02:00
|
|
|
export function Extensions() {
|
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-01-02 23:32:49 +01:00
|
|
|
const theme = useTheme();
|
|
|
|
|
const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));
|
|
|
|
|
|
2024-01-06 18:38:16 +01:00
|
|
|
const { data: serverSettingsData } = requestManager.useGetServerSettings();
|
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({});
|
2023-11-04 22:40:38 +01:00
|
|
|
const [fetchExtensions, { data, loading: isLoading, called }] = requestManager.useExtensionListFetch();
|
|
|
|
|
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 05:14:17 +04:30
|
|
|
const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([]));
|
|
|
|
|
|
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')}>
|
|
|
|
|
<IconButton onClick={() => inputRef.current?.click()} size="large">
|
|
|
|
|
<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);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
);
|
|
|
|
|
|
2023-11-04 22:40:38 +01:00
|
|
|
if (!allExtensions && (isLoading || !called)) {
|
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-01-15 23:22:35 +01:00
|
|
|
const showAddRepoInfo = !allExtensions?.length && !areReposDefined;
|
|
|
|
|
if (showAddRepoInfo) {
|
|
|
|
|
return (
|
2024-01-29 20:59:27 +01:00
|
|
|
<>
|
|
|
|
|
{toasts}
|
|
|
|
|
{FileInputComponent}
|
|
|
|
|
<Stack sx={{ paddingTop: '20px' }} alignItems="center" justifyContent="center" rowGap="10px">
|
|
|
|
|
<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 (
|
|
|
|
|
<>
|
2021-09-09 05:14:17 +04:30
|
|
|
{toasts}
|
2024-01-29 20:59:27 +01:00
|
|
|
{FileInputComponent}
|
2024-01-02 23:32:49 +01:00
|
|
|
<StyledGroupedVirtuoso
|
2021-12-19 18:06:27 +03:30
|
|
|
style={{
|
2024-01-02 23:32:49 +01:00
|
|
|
// override Virtuoso default values and set them with class
|
|
|
|
|
height: 'undefined',
|
|
|
|
|
}}
|
|
|
|
|
heightToSubtract={
|
|
|
|
|
isMobileWidth
|
|
|
|
|
? // desktop: TabsMenu height
|
|
|
|
|
48
|
|
|
|
|
: // desktop: TabsMenu height - TabsMenu bottom padding + grid item top padding
|
|
|
|
|
48 + 13 - 8
|
|
|
|
|
}
|
|
|
|
|
overscan={window.innerHeight * 0.5}
|
|
|
|
|
groupCounts={groupCounts}
|
|
|
|
|
groupContent={(index) => {
|
|
|
|
|
const [groupName] = filteredGroupedExtensions[index];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StyledGroupHeader
|
|
|
|
|
key={groupName}
|
|
|
|
|
variant="h4"
|
|
|
|
|
style={{
|
|
|
|
|
paddingLeft: '24px',
|
|
|
|
|
paddingTop: '6px',
|
|
|
|
|
paddingBottom: '16px',
|
|
|
|
|
fontWeight: 'bold',
|
|
|
|
|
}}
|
|
|
|
|
isFirstItem={index === 0}
|
|
|
|
|
>
|
|
|
|
|
{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}`}
|
|
|
|
|
isLastItem={index === visibleExtensions.length - 1}
|
|
|
|
|
>
|
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
|
|
|
}
|