diff --git a/app/src/components/ReloadService.tsx b/app/src/components/ReloadService.tsx new file mode 100644 index 00000000..d031ff8d --- /dev/null +++ b/app/src/components/ReloadService.tsx @@ -0,0 +1,53 @@ +import { useRegisterSW } from 'virtual:pwa-register/react' +import {version} from "../conf.ts"; +import {useTranslation} from "react-i18next"; +import {useToast} from "./ui/use-toast.ts"; +import {useEffect} from "react"; +import {ToastAction} from "./ui/toast.tsx"; + +function ReloadPrompt() { + const { t } = useTranslation(); + const { toast } = useToast(); + + const { + offlineReady: [offlineReady, setOfflineReady], + needRefresh: [needRefresh, setNeedRefresh], + updateServiceWorker, + } = useRegisterSW({ + onRegisteredSW() { + console.debug(`[service] service worker registered (version ${version})`); + }, + onRegisterError(error) { + console.log(`[service] service worker registration failed: ${error.message}`); + }, + }); + + useEffect(() => { + if (offlineReady) { + toast({ + title: t('service.offline-title'), + description: t('service.offline'), + }) + } + + if (needRefresh) { + toast({ + title: t('service.title'), + description: t('service.description'), + action: ( + updateServiceWorker(true)}> + {t('service.update')} + + ), + }); + + setOfflineReady(false); + setNeedRefresh(false); + } + }, []); + + return <>; +} + +export default ReloadPrompt; + diff --git a/app/src/conf.ts b/app/src/conf.ts index cd565090..cd686147 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -1,6 +1,6 @@ import axios from "axios"; -export const version: string = "3.2.2"; +export const version: string = "3.2.3"; export const deploy: boolean = true; export let rest_api: string = "http://localhost:8094"; export let ws_api: string = "ws://localhost:8094"; diff --git a/app/src/i18n.ts b/app/src/i18n.ts index d48675a8..f3706706 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -162,6 +162,13 @@ const resources = { "copied-description": "API key has been copied to clipboard", "learn-more": "Learn more", }, + service: { + "title": "New Version Available", + "description": "A new version is available. Do you want to update now?", + "update": "Update", + "offline-title": "Offline Mode", + "offline": "App is currently offline.", + } }, }, cn: { @@ -310,6 +317,13 @@ const resources = { "copied-description": "API 密钥已复制到剪贴板", "learn-more": "了解更多", }, + service: { + "title": "发现新版本", + "description": "发现新版本,是否立即更新?", + "update": "更新", + "offline-title": "离线模式", + "offline": "应用当前处于离线状态。", + } }, }, ru: { @@ -469,6 +483,13 @@ const resources = { "copied-description": "Ключ API скопирован в буфер обмена", "learn-more": "Узнать больше", }, + service: { + "title": "Доступна новая версия", + "description": "Доступна новая версия. Хотите обновить сейчас?", + "update": "Обновить", + "offline-title": "Режим оффлайн", + "offline": "Приложение в настоящее время находится в автономном режиме.", + } }, }, }; diff --git a/app/src/main.tsx b/app/src/main.tsx index f061c07d..8e8c01ac 100644 --- a/app/src/main.tsx +++ b/app/src/main.tsx @@ -5,11 +5,12 @@ import "./conf.ts"; import "./i18n.ts"; import "./assets/main.less"; import "./assets/globals.less"; -import "./service.ts"; import "./conf.ts"; +import ReloadPrompt from "./components/ReloadService.tsx"; ReactDOM.createRoot(document.getElementById("root")!).render( + , ); diff --git a/app/src/service.ts b/app/src/service.ts deleted file mode 100644 index c97718d3..00000000 --- a/app/src/service.ts +++ /dev/null @@ -1,24 +0,0 @@ -// @ts-ignore -import { registerSW } from "virtual:pwa-register"; -import { version } from "./conf.ts"; - -export const updateSW = registerSW({ - onRegisteredSW(url: string, registration: ServiceWorkerRegistration) { - if (!(!registration.installing && navigator)) return; - if ("connection" in navigator && !navigator.onLine) return; - - console.debug( - "[service] checking for update (current version: %s)", - version, - ); - fetch(url, { - headers: { "Service-Worker": "script", "Cache-Control": "no-cache" }, - cache: "no-store", - }).then(async (resp) => { - if (resp?.status === 200) { - await registration.update(); - if (registration.onupdatefound) console.debug("[service] update found"); - } - }); - }, -}); diff --git a/app/src/types/service.d.ts b/app/src/types/service.d.ts new file mode 100644 index 00000000..5ef1ae1a --- /dev/null +++ b/app/src/types/service.d.ts @@ -0,0 +1,15 @@ +declare module 'virtual:pwa-register/react' { + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error + // @ts-expect-error ignore when React is not installed + import type { Dispatch, SetStateAction } from 'react' + import type { RegisterSWOptions } from 'vite-plugin-pwa/types' + + export type { RegisterSWOptions } + + export function useRegisterSW(options?: RegisterSWOptions): { + needRefresh: [boolean, Dispatch>] + offlineReady: [boolean, Dispatch>] + updateServiceWorker: (reloadPage?: boolean) => Promise + onRegistered: (registration: ServiceWorkerRegistration) => void + } +} diff --git a/app/tsconfig.json b/app/tsconfig.json index 2d44c3d1..c6772b12 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -5,6 +5,10 @@ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, + "types": [ + "vite-plugin-pwa/react", + "./types/*.d.ts", + ], /* Bundler mode */ "moduleResolution": "bundler", @@ -25,5 +29,5 @@ "paths": { "@/*": ["./src/*"] }, - "references": [{ "path": "./tsconfig.node.json" }] + "references": [{ "path": "./tsconfig.node.json" }], }