diff --git a/docs/.vitepress/crowdin/en-US/component/pwa.json b/docs/.vitepress/crowdin/en-US/component/pwa.json index 86d69c54f7..e75a597323 100644 --- a/docs/.vitepress/crowdin/en-US/component/pwa.json +++ b/docs/.vitepress/crowdin/en-US/component/pwa.json @@ -1,5 +1,6 @@ { "message": "New content available, click on refresh button to update.", "refresh": "Refresh", + "always-refresh": "Always Refresh", "close": "Close" } diff --git a/docs/.vitepress/lang.js b/docs/.vitepress/lang.js index 0a5d3620fb..11c385cefd 100644 --- a/docs/.vitepress/lang.js +++ b/docs/.vitepress/lang.js @@ -25,4 +25,10 @@ ? toPath : toPath.concat('/') } + if (navigator && navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage({ + type: 'LANG', + lang: userPreferredLang, + }) + } })() diff --git a/docs/.vitepress/sw.ts b/docs/.vitepress/sw.ts new file mode 100644 index 0000000000..646770d4c7 --- /dev/null +++ b/docs/.vitepress/sw.ts @@ -0,0 +1,189 @@ +import { cacheNames, clientsClaim } from 'workbox-core' +import type { ManifestEntry } from 'workbox-build' + +declare let self: ServiceWorkerGlobalScope & { + __WB_MANIFEST: ManifestEntry[] +} +const manifest = self.__WB_MANIFEST +const cacheName = cacheNames.runtime +const defaultLang = manifest.some((item) => { + return item.url.includes(navigator.language) +}) + ? navigator.language + : 'en-US' + +let userPreferredLang = '' +let cacheEntries: RequestInfo[] = [] +let cacheManifestURLs: string[] = [] +let manifestURLs: string[] = [] + +class LangDB { + private db: IDBDatabase | undefined + private databaseName = 'PWA_DB' + private version = 1 + private storeNames = 'lang' + + constructor() { + this.initDB() + } + + private initDB() { + return new Promise((resolve) => { + const request = indexedDB.open(this.databaseName, this.version) + + request.onsuccess = (event) => { + this.db = (event.target as IDBOpenDBRequest).result + resolve(true) + } + + request.onupgradeneeded = (event) => { + this.db = (event.target as IDBOpenDBRequest).result + + if (!this.db.objectStoreNames.contains(this.storeNames)) { + this.db.createObjectStore(this.storeNames, { keyPath: 'id' }) + } + } + }) + } + + private async initLang() { + this.db!.transaction(this.storeNames, 'readwrite') + .objectStore(this.storeNames) + .add({ id: 1, lang: defaultLang }) + } + + async getLang() { + if (!this.db) await this.initDB() + + return new Promise((resolve) => { + const request = this.db!.transaction(this.storeNames) + .objectStore(this.storeNames) + .get(1) + + request.onsuccess = () => { + if (request.result) { + resolve(request.result.lang) + } else { + this.initLang() + resolve(defaultLang) + } + } + + request.onerror = () => { + resolve(defaultLang) + } + }) + } + + async setLang(lang: string) { + if (userPreferredLang !== lang) { + userPreferredLang = lang + cacheEntries = [] + cacheManifestURLs = [] + manifestURLs = [] + + if (!this.db) await this.initDB() + + this.db!.transaction(this.storeNames, 'readwrite') + .objectStore(this.storeNames) + .put({ id: 1, lang }) + } + } +} + +async function initManifest() { + userPreferredLang = userPreferredLang || (await langDB.getLang()) + // match the data that needs to be cached + // NOTE: When the structure of the document dist files changes, it needs to be changed here + const cacheList = [ + userPreferredLang, + `assets/(${userPreferredLang}|app|index|style|chunks)`, + 'images', + 'android-chrome', + 'apple-touch-icon', + 'manifest.webmanifest', + ] + const regExp = new RegExp(`^(${cacheList.join('|')})`) + + for (const item of manifest) { + const url = new URL(item.url, self.location.origin) + manifestURLs.push(url.href) + + if (regExp.test(item.url) || /^\/$/.test(item.url)) { + const request = new Request(url.href, { credentials: 'same-origin' }) + cacheEntries.push(request) + cacheManifestURLs.push(url.href) + } + } +} + +const langDB = new LangDB() + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(cacheName).then(async (cache) => { + if (!cacheEntries.length) await initManifest() + + return cache.addAll(cacheEntries) + }) + ) +}) + +self.addEventListener('activate', (event: ExtendableEvent) => { + // clean up outdated runtime cache + event.waitUntil( + caches.open(cacheName).then(async (cache) => { + if (!cacheManifestURLs.length) await initManifest() + + cache.keys().then((keys) => { + keys.forEach((request) => { + // clean up those who are not listed in cacheManifestURLs + !cacheManifestURLs.includes(request.url) && cache.delete(request) + }) + }) + }) + ) +}) + +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then(async (response) => { + // when the cache is hit, it returns directly to the cache + if (response) return response + if (!manifestURLs.length) await initManifest() + const requestClone = event.request.clone() + + // otherwise create a new fetch request + return fetch(requestClone) + .then((response) => { + const responseClone = response.clone() + + if (response.type !== 'basic' && response.status !== 200) { + return response + } + + // cache the data contained in the manifestURLs list + manifestURLs.includes(requestClone.url) && + caches.open(cacheName).then((cache) => { + cache.put(requestClone, responseClone) + }) + return response + }) + .catch((err) => { + throw new Error(`Failed to load resource ${requestClone.url}, ${err}`) + }) + }) + ) +}) + +self.addEventListener('message', (event) => { + if (event.data) { + if (event.data.type === 'SKIP_WAITING') { + self.skipWaiting() + } else if (event.data.type === 'LANG') { + langDB.setLang(event.data.lang) + } + } +}) + +clientsClaim() diff --git a/docs/.vitepress/vitepress/components/vp-reload-prompt.vue b/docs/.vitepress/vitepress/components/vp-reload-prompt.vue index 52d0e21e17..5391efbea7 100644 --- a/docs/.vitepress/vitepress/components/vp-reload-prompt.vue +++ b/docs/.vitepress/vitepress/components/vp-reload-prompt.vue @@ -1,5 +1,6 @@