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:
schroda
2024-04-21 15:02:27 +02:00
parent ad4f59f324
commit f61ed31bd5
3 changed files with 103 additions and 4 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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]);
}; };