Move app-update-checker files into new folder
This commit is contained in:
12
src/modules/app-updates/AppUpdateChecker.types.ts
Normal file
12
src/modules/app-updates/AppUpdateChecker.types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
export type MetadataUpdateSettings = {
|
||||
webUIInformAvailableUpdate: boolean;
|
||||
serverInformAvailableUpdate: boolean;
|
||||
};
|
||||
146
src/modules/app-updates/components/ServerUpdateChecker.tsx
Normal file
146
src/modules/app-updates/components/ServerUpdateChecker.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 { useTranslation } from 'react-i18next';
|
||||
import { useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { useUpdateChecker } from '@/modules/app-updates/hooks/useUpdateChecker.tsx';
|
||||
import { VersionUpdateInfoDialog } from '@/modules/app-updates/components/VersionUpdateInfoDialog.tsx';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { getVersion } from '@/modules/app-updates/services/AppUpdateChecker.tsx';
|
||||
|
||||
const disabledUpdateCheck = () => Promise.resolve();
|
||||
|
||||
export const ServerUpdateChecker = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [serverVersion, setServerVersion] = useLocalStorage<string>('serverVersion');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {
|
||||
settings: { serverInformAvailableUpdate },
|
||||
} = useMetadataServerSettings();
|
||||
|
||||
const {
|
||||
data: serverUpdateCheckData,
|
||||
loading: isCheckingForServerUpdate,
|
||||
error: serverUpdateCheckError,
|
||||
refetch: checkForUpdate,
|
||||
} = requestManager.useCheckForServerUpdate({
|
||||
notifyOnNetworkStatusChange: true,
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { data } = requestManager.useGetAbout();
|
||||
const { aboutServer } = data ?? {};
|
||||
|
||||
const selectedServerChannelInfo = serverUpdateCheckData?.checkForServerUpdates?.find(
|
||||
(channel) => channel.channel === aboutServer?.buildType,
|
||||
);
|
||||
const version = aboutServer ? getVersion(aboutServer) : undefined;
|
||||
const isServerUpdateAvailable = !!selectedServerChannelInfo?.tag && selectedServerChannelInfo.tag !== version;
|
||||
|
||||
const updateChecker = useUpdateChecker(
|
||||
'server',
|
||||
serverInformAvailableUpdate ? checkForUpdate : disabledUpdateCheck,
|
||||
selectedServerChannelInfo?.tag,
|
||||
);
|
||||
|
||||
const changelogUrl =
|
||||
aboutServer?.buildType.toLowerCase() === 'stable'
|
||||
? `https://github.com/Suwayomi/Suwayomi-Server/releases/tag/${aboutServer.version}`
|
||||
: undefined;
|
||||
|
||||
const isSameAsCurrent = !version || !serverVersion || serverVersion === version;
|
||||
|
||||
const saveInitialVersion = !serverVersion && !!version;
|
||||
if (saveInitialVersion) {
|
||||
setServerVersion(version);
|
||||
}
|
||||
|
||||
if (!isSameAsCurrent && !open) {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
if (isCheckingForServerUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (serverUpdateCheckError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isServerUpdateAvailable) {
|
||||
if (!serverInformAvailableUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAboutPage = window.location.pathname === '/settings/about';
|
||||
if (isAboutPage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!updateChecker.handleUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VersionUpdateInfoDialog
|
||||
info={t('global.update.label.info', {
|
||||
channel: selectedServerChannelInfo.channel,
|
||||
version: selectedServerChannelInfo.tag,
|
||||
})}
|
||||
actionTitle={t('chapter.action.download.add.label.action')}
|
||||
actionUrl={selectedServerChannelInfo.url}
|
||||
updateCheckerProps={['server', checkForUpdate, selectedServerChannelInfo?.tag]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('global.update.label.update_success', {
|
||||
name: t('settings.server.title.server'),
|
||||
version,
|
||||
channel: aboutServer?.buildType,
|
||||
})}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{changelogUrl && (
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setServerVersion(version);
|
||||
setOpen(false);
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('global.button.ok')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
166
src/modules/app-updates/components/VersionInfo.tsx
Normal file
166
src/modules/app-updates/components/VersionInfo.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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 Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import { t as translate } from 'i18next';
|
||||
import DownloadingIcon from '@mui/icons-material/Downloading';
|
||||
import { UpdateState } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export type BaseVersionInfoProps = {
|
||||
version: string;
|
||||
isCheckingForUpdate: boolean;
|
||||
isUpdateAvailable: boolean;
|
||||
updateCheckError: any;
|
||||
checkForUpdate: () => void;
|
||||
};
|
||||
export type LinkVersionInfoProps = {
|
||||
downloadAsLink: true;
|
||||
url: string;
|
||||
};
|
||||
export type TriggerVersionInfoProps = {
|
||||
triggerUpdate: () => void;
|
||||
updateState: UpdateState;
|
||||
progress: number;
|
||||
};
|
||||
export type VersionInfoProps =
|
||||
| (BaseVersionInfoProps & PropertiesNever<TriggerVersionInfoProps> & LinkVersionInfoProps)
|
||||
| (BaseVersionInfoProps & TriggerVersionInfoProps & PropertiesNever<LinkVersionInfoProps>);
|
||||
|
||||
const getUpdateCheckButtonIcon = (
|
||||
isLoading: boolean,
|
||||
isUpdateAvailable: boolean,
|
||||
updateState?: UpdateState,
|
||||
asLink: boolean = false,
|
||||
) => {
|
||||
const isUpdateInProgress = updateState === UpdateState.Downloading;
|
||||
if (isUpdateInProgress) {
|
||||
return <DownloadingIcon />;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <CircularProgress size={15} />;
|
||||
}
|
||||
|
||||
const isRefreshRequired = !isUpdateAvailable || updateState === UpdateState.Error;
|
||||
if (isRefreshRequired) {
|
||||
return <RefreshIcon />;
|
||||
}
|
||||
|
||||
return asLink ? <OpenInNewIcon /> : <DownloadIcon />;
|
||||
};
|
||||
|
||||
const getUpdateCheckButtonText = (
|
||||
isLoading: boolean,
|
||||
isUpdateAvailable: boolean,
|
||||
error: any,
|
||||
updateState?: UpdateState,
|
||||
progress: number = 0,
|
||||
) => {
|
||||
const isUpdating = updateState === UpdateState.Downloading;
|
||||
if (isUpdating) {
|
||||
return translate('global.update.label.updating', { progress });
|
||||
}
|
||||
|
||||
const didUpdateFail = updateState === UpdateState.Error;
|
||||
if (didUpdateFail) {
|
||||
return translate('global.update.label.update_failure');
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return translate('global.update.label.checking');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return translate('global.update.label.check_failure');
|
||||
}
|
||||
|
||||
if (isUpdateAvailable) {
|
||||
return translate('global.update.label.available');
|
||||
}
|
||||
|
||||
return translate('global.update.label.up_to_date');
|
||||
};
|
||||
|
||||
export const VersionInfo = ({
|
||||
version,
|
||||
isCheckingForUpdate,
|
||||
isUpdateAvailable,
|
||||
updateCheckError,
|
||||
checkForUpdate,
|
||||
triggerUpdate,
|
||||
updateState,
|
||||
progress,
|
||||
downloadAsLink,
|
||||
url,
|
||||
}: VersionInfoProps) => {
|
||||
const isUpdateInProgress = updateState === UpdateState.Downloading;
|
||||
|
||||
const onClick = () => {
|
||||
if (isUpdateInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldCheckForUpdate = !isUpdateAvailable || updateCheckError || updateState === UpdateState.Error;
|
||||
if (shouldCheckForUpdate) {
|
||||
checkForUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUpdateAvailable) {
|
||||
triggerUpdate?.();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
sx={{
|
||||
alignItems: 'start',
|
||||
}}
|
||||
>
|
||||
<Typography component="span" variant="body2">
|
||||
{version}
|
||||
</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
marginTop: '5px',
|
||||
backgroundColor: 'transparent',
|
||||
pointerEvents: isUpdateInProgress ? 'none' : 'unset',
|
||||
}}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={getUpdateCheckButtonIcon(
|
||||
isCheckingForUpdate,
|
||||
isUpdateAvailable,
|
||||
updateState,
|
||||
downloadAsLink,
|
||||
)}
|
||||
onClick={onClick}
|
||||
{...(!!url && isUpdateAvailable
|
||||
? {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}
|
||||
: undefined)}
|
||||
>
|
||||
{getUpdateCheckButtonText(
|
||||
isCheckingForUpdate,
|
||||
isUpdateAvailable,
|
||||
updateCheckError,
|
||||
updateState,
|
||||
progress,
|
||||
)}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
126
src/modules/app-updates/components/VersionUpdateInfoDialog.tsx
Normal file
126
src/modules/app-updates/components/VersionUpdateInfoDialog.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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 Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { useUpdateChecker } from '@/modules/app-updates/hooks/useUpdateChecker.tsx';
|
||||
|
||||
interface BaseProps {
|
||||
info: string;
|
||||
actionTitle: string;
|
||||
updateCheckerProps: Parameters<typeof useUpdateChecker>;
|
||||
changelogUrl?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface UrlActionProps extends BaseProps {
|
||||
actionUrl: string;
|
||||
}
|
||||
|
||||
interface ActionProps extends BaseProps {
|
||||
onAction: () => void;
|
||||
}
|
||||
|
||||
type VersionUpdateInfoDialogProps =
|
||||
| (UrlActionProps & PropertiesNever<Pick<ActionProps, 'onAction'>>)
|
||||
| (PropertiesNever<Pick<UrlActionProps, 'actionUrl'>> & ActionProps);
|
||||
|
||||
export const VersionUpdateInfoDialog = ({
|
||||
info,
|
||||
actionUrl,
|
||||
onAction,
|
||||
actionTitle,
|
||||
updateCheckerProps,
|
||||
changelogUrl,
|
||||
disabled,
|
||||
}: VersionUpdateInfoDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateChecker = useUpdateChecker(...updateCheckerProps);
|
||||
|
||||
if (!updateChecker.handleUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open>
|
||||
<DialogTitle>{t('global.update.label.available')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{info}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
justifyContent: changelogUrl ? 'space-between' : 'end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{changelogUrl && (
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row">
|
||||
<PopupState variant="popover" popupId="update-checker-close-menu">
|
||||
{(popupState) => (
|
||||
<>
|
||||
<Button disabled={disabled} {...bindTrigger(popupState)}>
|
||||
{t('global.label.close')}
|
||||
</Button>
|
||||
<Menu {...bindMenu(popupState)}>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
updateChecker.remindLater();
|
||||
popupState.close();
|
||||
}}
|
||||
>
|
||||
{t('global.button.remind_later')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
updateChecker.ignoreUpdate();
|
||||
popupState.close();
|
||||
}}
|
||||
>
|
||||
{t('global.button.ignore')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
)}
|
||||
</PopupState>
|
||||
{actionUrl ? (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant="contained"
|
||||
href={actionUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{actionTitle}
|
||||
</Button>
|
||||
) : (
|
||||
<Button disabled={disabled} onClick={onAction} variant="contained">
|
||||
{actionTitle}
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
188
src/modules/app-updates/components/WebUIUpdateChecker.tsx
Normal file
188
src/modules/app-updates/components/WebUIUpdateChecker.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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 { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogContentText from '@mui/material/DialogContentText';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import { ABOUT_WEBUI, WEBUI_UPDATE_CHECK } from '@/lib/graphql/fragments/InfoFragments.ts';
|
||||
import { VersionUpdateInfoDialog } from '@/modules/app-updates/components/VersionUpdateInfoDialog.tsx';
|
||||
import { useUpdateChecker } from '@/modules/app-updates/hooks/useUpdateChecker.tsx';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
|
||||
const disabledUpdateCheck = () => Promise.resolve();
|
||||
|
||||
export const WebUIUpdateChecker = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {
|
||||
settings: { webUIInformAvailableUpdate },
|
||||
} = useMetadataServerSettings();
|
||||
const serverSettings = requestManager.useGetServerSettings();
|
||||
const isAutoUpdateEnabled = !!serverSettings.data?.settings.webUIUpdateCheckInterval;
|
||||
|
||||
const shouldCheckForUpdate = !isAutoUpdateEnabled && webUIInformAvailableUpdate;
|
||||
|
||||
const { data: aboutData } = requestManager.useGetAbout();
|
||||
const { aboutWebUI } = aboutData ?? {};
|
||||
|
||||
const { data: webUIUpdateData, refetch: checkForUpdate } = requestManager.useCheckForWebUIUpdate({
|
||||
notifyOnNetworkStatusChange: true,
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { data: webUIUpdateStatusData } = requestManager.useGetWebUIUpdateStatus();
|
||||
const { state: webUIUpdateState, ...updateStatus } = (webUIUpdateStatusData?.getWebUIUpdateStatus ?? {
|
||||
state: UpdateState.Idle,
|
||||
progress: 0,
|
||||
info: undefined,
|
||||
}) satisfies OptionalProperty<WebUiUpdateStatus, 'info'>;
|
||||
|
||||
const updateChecker = useUpdateChecker(
|
||||
'webUI',
|
||||
shouldCheckForUpdate ? checkForUpdate : disabledUpdateCheck,
|
||||
webUIUpdateData?.checkForWebUIUpdate.tag,
|
||||
);
|
||||
|
||||
const changelogUrl =
|
||||
updateStatus.info?.channel === WebUiChannel.Stable
|
||||
? `https://github.com/Suwayomi/Suwayomi-WebUI/releases/tag/${updateStatus.info?.tag}`
|
||||
: `https://github.com/Suwayomi/Suwayomi-WebUI/issues/749`;
|
||||
|
||||
const newVersion = aboutWebUI?.tag;
|
||||
const isSameAsCurrent = !newVersion || !webUIVersion || webUIVersion === newVersion;
|
||||
|
||||
const saveInitialVersion = !webUIVersion && !!newVersion;
|
||||
if (saveInitialVersion) {
|
||||
setWebUIVersion(newVersion);
|
||||
}
|
||||
|
||||
if (!isSameAsCurrent && !open) {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const isError = webUIUpdateState === UpdateState.Error;
|
||||
if (isError) {
|
||||
makeToast(t('settings.about.webui.label.update_failure'), 'error');
|
||||
}
|
||||
|
||||
const updateFinished = webUIUpdateState === UpdateState.Finished;
|
||||
|
||||
const resetUpdateStatus = isError || updateFinished;
|
||||
if (resetUpdateStatus) {
|
||||
requestManager
|
||||
.resetWebUIUpdateStatus()
|
||||
.response.catch(defaultPromiseErrorHandler('WebUIUpdateChecker::resetWebUIUpdateStatus'));
|
||||
}
|
||||
|
||||
if (!updateFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!updateStatus.info) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestManager.graphQLClient.client.cache.writeFragment({
|
||||
fragment: ABOUT_WEBUI,
|
||||
data: {
|
||||
__typename: 'AboutWebUI',
|
||||
channel: webUIUpdateStatusData!.getWebUIUpdateStatus.info.channel,
|
||||
tag: webUIUpdateStatusData!.getWebUIUpdateStatus.info.tag,
|
||||
},
|
||||
});
|
||||
requestManager.graphQLClient.client.cache.writeFragment({
|
||||
fragment: WEBUI_UPDATE_CHECK,
|
||||
data: {
|
||||
__typename: 'WebUIUpdateCheck',
|
||||
channel: webUIUpdateStatusData!.getWebUIUpdateStatus.info.channel,
|
||||
tag: webUIUpdateStatusData!.getWebUIUpdateStatus.info.tag,
|
||||
updateAvailable: false,
|
||||
},
|
||||
});
|
||||
}, [webUIUpdateState]);
|
||||
|
||||
const isUpdateAvailable =
|
||||
shouldCheckForUpdate && updateChecker.handleUpdate && webUIUpdateData?.checkForWebUIUpdate.updateAvailable;
|
||||
if (isUpdateAvailable) {
|
||||
const isUpdateInProgress = webUIUpdateState === UpdateState.Downloading;
|
||||
|
||||
return (
|
||||
<VersionUpdateInfoDialog
|
||||
info={t('settings.about.webui.label.info', {
|
||||
version: webUIUpdateData?.checkForWebUIUpdate.tag,
|
||||
channel: webUIUpdateData?.checkForWebUIUpdate.channel,
|
||||
})}
|
||||
changelogUrl={changelogUrl}
|
||||
disabled={isUpdateInProgress}
|
||||
onAction={() =>
|
||||
requestManager
|
||||
.updateWebUI()
|
||||
.response.catch(() => makeToast(t('settings.about.webui.label.update_failure'), 'error'))
|
||||
}
|
||||
actionTitle={
|
||||
isUpdateInProgress
|
||||
? t('global.update.label.updating', { progress: updateStatus.progress })
|
||||
: t('extension.action.label.update')
|
||||
}
|
||||
updateCheckerProps={[
|
||||
'webUI',
|
||||
isAutoUpdateEnabled ? disabledUpdateCheck : checkForUpdate,
|
||||
webUIUpdateData?.checkForWebUIUpdate.tag,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!open) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open}>
|
||||
<DialogTitle>{t('settings.about.webui.label.updated')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
{t('global.update.label.update_success', {
|
||||
name: t('settings.webui.title.webui'),
|
||||
version: newVersion,
|
||||
channel: aboutWebUI?.channel,
|
||||
})}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button href={changelogUrl} target="_blank" rel="noreferrer">
|
||||
{t('global.button.changelog')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setWebUIVersion(newVersion);
|
||||
setOpen(false);
|
||||
window.location.reload();
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
{t('global.button.refresh')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
61
src/modules/app-updates/hooks/useUpdateChecker.tsx
Normal file
61
src/modules/app-updates/hooks/useUpdateChecker.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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 { useCallback, useEffect, useMemo } from 'react';
|
||||
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
|
||||
const UPDATE_CHECK_INTERVAL = 1000 * 60 * 60; // 1 hour
|
||||
const UPDATE_REMINDER_THRESHOLD = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
export const useUpdateChecker = (
|
||||
storageKey: string,
|
||||
checkForUpdate: () => Promise<unknown>,
|
||||
version?: string,
|
||||
interval: number = UPDATE_CHECK_INTERVAL,
|
||||
): { handleUpdate: boolean; ignoreUpdate: () => void; remindLater: () => void } => {
|
||||
const [lastUpdateCheck, setLastUpdateCheck] = useLocalStorage(`UpdateChecker::${storageKey}::lastUpdateCheck`, 0);
|
||||
const [ignoreVersionUpdate, setIgnoreVersionUpdate] = useLocalStorage<string>(
|
||||
`UpdateChecker::${storageKey}::ignoreUpdate`,
|
||||
);
|
||||
const [updateClosedTimestamp, setUpdateClosedTimestamp] = useLocalStorage(
|
||||
`UpdateChecker::${storageKey}::closeTimestamp`,
|
||||
0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const remainingTimeTillNextUpdateCheck = (interval - (Date.now() - lastUpdateCheck)) % interval;
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined;
|
||||
const scheduleUpdateCheck = (timeoutMS: number) => {
|
||||
timeout = setTimeout(() => {
|
||||
checkForUpdate().catch(defaultPromiseErrorHandler(`UpdateChecker(${storageKey})::checkForUpdate`));
|
||||
setLastUpdateCheck(Date.now());
|
||||
scheduleUpdateCheck(interval);
|
||||
}, timeoutMS);
|
||||
};
|
||||
|
||||
scheduleUpdateCheck(remainingTimeTillNextUpdateCheck);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [storageKey, checkForUpdate, interval]);
|
||||
|
||||
const ignoreUpdate = useCallback(() => {
|
||||
setIgnoreVersionUpdate(version);
|
||||
}, [storageKey, version]);
|
||||
|
||||
const remindLater = useCallback(() => {
|
||||
setUpdateClosedTimestamp(Date.now());
|
||||
}, [storageKey]);
|
||||
|
||||
const wasRecentlyClosed = Date.now() - updateClosedTimestamp < UPDATE_REMINDER_THRESHOLD;
|
||||
const wasUpdateIgnored = !!ignoreVersionUpdate && ignoreVersionUpdate === version;
|
||||
const handleUpdate = !wasRecentlyClosed && !wasUpdateIgnored;
|
||||
|
||||
return useMemo(() => ({ handleUpdate, ignoreUpdate, remindLater }), [handleUpdate, ignoreUpdate, remindLater]);
|
||||
};
|
||||
19
src/modules/app-updates/services/AppUpdateChecker.tsx
Normal file
19
src/modules/app-updates/services/AppUpdateChecker.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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 { epochToDate } from '@/util/DateHelper.ts';
|
||||
import { GetAboutQuery } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
type AboutServer = GetAboutQuery['aboutServer'];
|
||||
|
||||
export const getVersion = (aboutServer: AboutServer) => {
|
||||
if (aboutServer.buildType === 'Stable') return `${aboutServer.version}`;
|
||||
return `${aboutServer.version}-${aboutServer.revision}`;
|
||||
};
|
||||
|
||||
export const getBuildTime = (aboutServer: AboutServer) => epochToDate(Number(aboutServer.buildTime)).toString();
|
||||
Reference in New Issue
Block a user