Globally inform about available webUI updates
In case the automatic webUI updates setting was disabled the user was never informed about available webUI updates
This commit is contained in:
@@ -310,6 +310,7 @@
|
|||||||
"dont_show_dialog_again": "Don't show this dialog again",
|
"dont_show_dialog_again": "Don't show this dialog again",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
|
"ignore": "Ignore",
|
||||||
"latest": "Latest",
|
"latest": "Latest",
|
||||||
"log_in": "Log in",
|
"log_in": "Log in",
|
||||||
"log_out": "Log out",
|
"log_out": "Log out",
|
||||||
@@ -319,6 +320,7 @@
|
|||||||
"options": "Options",
|
"options": "Options",
|
||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
|
"remind_later": "Remind later",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"reset_to_default": "Reset to Default",
|
"reset_to_default": "Reset to Default",
|
||||||
@@ -765,6 +767,7 @@
|
|||||||
"label": {
|
"label": {
|
||||||
"channel": "$t(settings.webui.title.webui) channel",
|
"channel": "$t(settings.webui.title.webui) channel",
|
||||||
"github": "$t(global.label.github) $t(settings.webui.title.webui)",
|
"github": "$t(global.label.github) $t(settings.webui.title.webui)",
|
||||||
|
"info": "$t(settings.webui.title.webui) version {{version}} ({{channel}}) available for download",
|
||||||
"update_failure": "Could not update $t(settings.webui.title.webui)",
|
"update_failure": "Could not update $t(settings.webui.title.webui)",
|
||||||
"update_success": "Updated $t(settings.webui.title.webui) to version {{version}}.",
|
"update_success": "Updated $t(settings.webui.title.webui) to version {{version}}.",
|
||||||
"updated": "Updated version",
|
"updated": "Updated version",
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import DialogContent from '@mui/material/DialogContent';
|
|||||||
import DialogContentText from '@mui/material/DialogContentText';
|
import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
|
import { UpdateState, WebUiChannel, WebUiUpdateStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||||
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
import { makeToast } from '@/components/util/Toast.tsx';
|
import { makeToast } from '@/components/util/Toast.tsx';
|
||||||
import { ABOUT_WEBUI, WEBUI_UPDATE_CHECK } from '@/lib/graphql/Fragments.ts';
|
import { ABOUT_WEBUI, WEBUI_UPDATE_CHECK } from '@/lib/graphql/Fragments.ts';
|
||||||
|
import { useUpdateChecker } from '@/util/useUpdateChecker.tsx';
|
||||||
|
|
||||||
export const WebUIUpdateChecker = () => {
|
export const WebUIUpdateChecker = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -27,6 +29,14 @@ export const WebUIUpdateChecker = () => {
|
|||||||
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
|
const [webUIVersion, setWebUIVersion] = useLocalStorage<string>('webUIVersion');
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const serverSettings = requestManager.useGetServerSettings();
|
||||||
|
const isAutoUpdateEnabled = !serverSettings.data?.settings.webUIUpdateCheckInterval;
|
||||||
|
|
||||||
|
const { data: webUIUpdateData, refetch: checkForUpdate } = requestManager.useCheckForWebUIUpdate({
|
||||||
|
skip: isAutoUpdateEnabled,
|
||||||
|
notifyOnNetworkStatusChange: true,
|
||||||
|
});
|
||||||
|
|
||||||
const { data: webUIUpdateStatusData } = requestManager.useGetWebUIUpdateStatus();
|
const { data: webUIUpdateStatusData } = requestManager.useGetWebUIUpdateStatus();
|
||||||
const { state: webUIUpdateState, ...updateStatus } = (webUIUpdateStatusData?.getWebUIUpdateStatus ?? {
|
const { state: webUIUpdateState, ...updateStatus } = (webUIUpdateStatusData?.getWebUIUpdateStatus ?? {
|
||||||
state: UpdateState.Idle,
|
state: UpdateState.Idle,
|
||||||
@@ -34,6 +44,8 @@ export const WebUIUpdateChecker = () => {
|
|||||||
info: undefined,
|
info: undefined,
|
||||||
}) satisfies OptionalProperty<WebUiUpdateStatus, 'info'>;
|
}) satisfies OptionalProperty<WebUiUpdateStatus, 'info'>;
|
||||||
|
|
||||||
|
const updateChecker = useUpdateChecker('webUI', checkForUpdate, webUIUpdateData?.checkForWebUIUpdate.tag);
|
||||||
|
|
||||||
const changelogUrl =
|
const changelogUrl =
|
||||||
updateStatus.info?.channel === WebUiChannel.Stable
|
updateStatus.info?.channel === WebUiChannel.Stable
|
||||||
? `https://github.com/Suwayomi/Suwayomi-WebUI/releases/tag/${updateStatus.info?.tag}`
|
? `https://github.com/Suwayomi/Suwayomi-WebUI/releases/tag/${updateStatus.info?.tag}`
|
||||||
@@ -91,6 +103,67 @@ export const WebUIUpdateChecker = () => {
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
}, [webUIUpdateState]);
|
}, [webUIUpdateState]);
|
||||||
|
|
||||||
|
const isUpdateAvailable = updateChecker.handleUpdate && webUIUpdateData?.checkForWebUIUpdate.updateAvailable;
|
||||||
|
if (isUpdateAvailable) {
|
||||||
|
const isUpdateInProgress = webUIUpdateState === UpdateState.Downloading;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open>
|
||||||
|
<DialogTitle>{t('global.update.label.available')}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>
|
||||||
|
{t('settings.about.webui.label.info', {
|
||||||
|
version: webUIUpdateData.checkForWebUIUpdate.tag,
|
||||||
|
channel: webUIUpdateData.checkForWebUIUpdate.channel,
|
||||||
|
})}
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Stack sx={{ width: '100%' }} direction="row" justifyContent="space-between">
|
||||||
|
<Button href={changelogUrl} target="_blank">
|
||||||
|
{t('global.button.changelog')}
|
||||||
|
</Button>
|
||||||
|
<Stack direction="row">
|
||||||
|
<Button
|
||||||
|
disabled={isUpdateInProgress}
|
||||||
|
onClick={() => {
|
||||||
|
updateChecker.remindLater();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('global.button.remind_later')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isUpdateInProgress}
|
||||||
|
onClick={() => {
|
||||||
|
updateChecker.ignoreUpdate();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('global.button.ignore')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isUpdateInProgress}
|
||||||
|
onClick={() => {
|
||||||
|
requestManager
|
||||||
|
.updateWebUI()
|
||||||
|
.response.catch(() =>
|
||||||
|
makeToast(t('settings.about.webui.label.update_failure'), 'error'),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
{isUpdateInProgress
|
||||||
|
? t('global.update.label.updating', { progress: updateStatus.progress })
|
||||||
|
: t('extension.action.label.update')}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const handleUpdate = open && webUIUpdateState === UpdateState.Idle;
|
const handleUpdate = open && webUIUpdateState === UpdateState.Idle;
|
||||||
if (!handleUpdate) {
|
if (!handleUpdate) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,18 +6,27 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
import { defaultPromiseErrorHandler } from '@/util/defaultPromiseErrorHandler.ts';
|
||||||
import { useLocalStorage } from '@/util/useStorage.tsx';
|
import { useLocalStorage } from '@/util/useStorage.tsx';
|
||||||
|
|
||||||
const UPDATE_CHECK_INTERVAL = 1000 * 60 * 60 * 24; // 1 day
|
const UPDATE_CHECK_INTERVAL = 1000 * 60 * 60; // 1 hour
|
||||||
|
const UPDATE_REMINDER_THRESHOLD = 1000 * 60 * 60; // 1 hour
|
||||||
|
|
||||||
export const useUpdateChecker = (
|
export const useUpdateChecker = (
|
||||||
storageKey: string,
|
storageKey: string,
|
||||||
checkForUpdate: () => Promise<unknown>,
|
checkForUpdate: () => Promise<unknown>,
|
||||||
|
version?: string,
|
||||||
interval: number = UPDATE_CHECK_INTERVAL,
|
interval: number = UPDATE_CHECK_INTERVAL,
|
||||||
): void => {
|
): { handleUpdate: boolean; ignoreUpdate: () => void; remindLater: () => void } => {
|
||||||
const [lastUpdateCheck, setLastUpdateCheck] = useLocalStorage(`UpdateChecker::${storageKey}`, 0);
|
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(() => {
|
useEffect(() => {
|
||||||
const remainingTimeTillNextUpdateCheck = (interval - (Date.now() - lastUpdateCheck)) % interval;
|
const remainingTimeTillNextUpdateCheck = (interval - (Date.now() - lastUpdateCheck)) % interval;
|
||||||
@@ -35,4 +44,18 @@ export const useUpdateChecker = (
|
|||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [storageKey, checkForUpdate, interval]);
|
}, [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]);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user