feat(stage-ui,stage-web): PWA update ready prompt

This commit is contained in:
Neko Ayaka
2025-07-12 00:42:40 +08:00
parent 01704888ad
commit 76233f883a
16 changed files with 200 additions and 10 deletions
+1
View File
@@ -58,6 +58,7 @@
"gpuu": "^1.0.4",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"nanoid": "^5.1.5",
"node-vibrant": "^4.0.3",
"nprogress": "^0.2.0",
"ofetch": "^1.4.1",
+8 -3
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { OnboardingDialog } from '@proj-airi/stage-ui/components'
import { OnboardingDialog, ToasterRoot } from '@proj-airi/stage-ui/components'
import { useOnboardingStore, useSettings } from '@proj-airi/stage-ui/stores'
import { StageTransitionGroup } from '@proj-airi/ui-transitions'
import { useDark } from '@vueuse/core'
@@ -7,10 +7,13 @@ import { storeToRefs } from 'pinia'
import { computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { Toaster } from 'vue-sonner'
import { toast, Toaster } from 'vue-sonner'
import { usePWAStore } from './stores/pwa'
import 'vue-sonner/style.css'
usePWAStore()
const i18n = useI18n()
const settings = storeToRefs(useSettings())
const onboardingStore = useOnboardingStore()
@@ -79,7 +82,9 @@ function handleSetupSkipped() {
<RouterView />
</StageTransitionGroup>
<Toaster position="top-right" />
<ToasterRoot @close="id => toast.dismiss(id)">
<Toaster />
</ToasterRoot>
<!-- First Time Setup Dialog -->
<OnboardingDialog
+4 -1
View File
@@ -42,11 +42,14 @@ router.afterEach(() => {
router.isReady()
.then(async () => {
if (import.meta.env.SSR) {
return
}
if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE) {
return
}
const { registerSW } = await import('virtual:pwa-register')
const { registerSW } = await import('./modules/pwa')
registerSW({ immediate: true })
})
.catch(() => {})
+3
View File
@@ -0,0 +1,3 @@
import { registerSW } from 'virtual:pwa-register'
export { registerSW }
+36
View File
@@ -0,0 +1,36 @@
import { ToasterPWAUpdateReady } from '@proj-airi/stage-ui/components'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { h, markRaw, onMounted, ref } from 'vue'
import { toast } from 'vue-sonner'
export const usePWAStore = defineStore('pwa', () => {
const updateReadyHooks = ref<(() => void)[]>([])
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('md')
onMounted(async () => {
if (import.meta.env.SSR) {
return
}
if (import.meta.env.VITE_APP_TARGET_HUGGINGFACE_SPACE) {
return
}
const { registerSW } = await import('../modules/pwa')
const updateSW = registerSW({
onNeedRefresh: () => {
const id = nanoid()
toast(markRaw(h(ToasterPWAUpdateReady, { id, onUpdate: () => updateSW() })), {
id,
duration: 30000,
position: isMobile.value ? 'top-center' : 'bottom-right',
})
},
})
updateReadyHooks.value.push(updateSW)
})
})
+6
View File
@@ -42,3 +42,9 @@ prompt:
- <{'|'}DELAY:3{'|'}> (Delay for 3 seconds)
And the last, do what ever you want!
toaster:
pwaUpdateReady:
message: New version ready, reload now?
action:
notNow: Not now
ok: OK
@@ -30,3 +30,9 @@ prompt:
- <{'|'}DELAY:3{'|'}> (延迟 3 秒)
最后,做任何你想做的事!
toaster:
pwaUpdateReady:
message: 新版本就绪,现在更新?
action:
notNow: 稍后
ok: 好的
+2
View File
@@ -85,6 +85,7 @@
"gpuu": "^1.0.4",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
"nanoid": "^5.1.5",
"pinia": "^3.0.3",
"pixi-filters": "4",
"pixi-live2d-display": "^0.4.0",
@@ -124,6 +125,7 @@
"@unocss/reset": "^66.3.3",
"@vitejs/plugin-vue": "^6.0.0",
"histoire": "1.0.0-alpha.2",
"unplugin-yaml": "^3.0.1",
"vite": "^6.3.5",
"vue": "^3.5.17"
}
@@ -46,7 +46,7 @@ const variantClasses: Record<ButtonVariant, {
default: 'bg-primary-100 hover:bg-primary-200 active:bg-primary-300 dark:bg-primary-900/40 dark:hover:bg-primary-900/60 dark:active:bg-primary-900/40 focus:ring-primary-300/60 dark:focus:ring-primary-600/30 border-2 border-solid border-primary-200/30 dark:border-primary-900/40 text-primary-950 dark:text-primary-100',
},
secondary: {
default: 'bg-neutral-50 hover:bg-neutral-200 active:bg-neutral-300 dark:bg-neutral-600/40 dark:hover:bg-neutral-600/60 dark:active:bg-neutral-600/40 focus:ring-neutral-300/60 dark:focus:ring-neutral-600/30 border-2 border-solid border-neutral-300/50 dark:border-neutral-600/50 text-neutral-950 dark:text-neutral-100',
default: 'bg-neutral-50 hover:bg-neutral-200 active:bg-neutral-300 dark:bg-neutral-600/40 dark:hover:bg-neutral-600/60 dark:active:bg-neutral-600/40 focus:ring-neutral-300/60 dark:focus:ring-neutral-600/30 border-2 border-solid border-neutral-200/50 dark:border-neutral-700/50 text-neutral-950 dark:text-neutral-100',
},
danger: {
default: 'bg-red-100 hover:bg-red-200 active:bg-red-300 dark:bg-red-900/40 dark:hover:bg-red-900/60 dark:active:bg-red-900/40 focus:ring-red-300/30 dark:focus:ring-red-600/60 dark:focus:ring-red-600/30 border-2 border-solid border-red-200/30 dark:border-red-900/30 text-red-950 dark:text-red-100',
@@ -1,5 +1,27 @@
<script setup lang="ts">
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { nanoid } from 'nanoid'
import { h, markRaw } from 'vue'
import { toast, Toaster } from 'vue-sonner'
import ToasterPWAUpdateReady from './ToasterPWAUpdateReady.vue'
import ToasterRoot from './ToasterRoot.vue'
import 'vue-sonner/style.css'
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
function handlePopToast() {
const id = nanoid()
// eslint-disable-next-line no-console
toast(markRaw(h(ToasterPWAUpdateReady, { id, onUpdate: () => console.debug('Update initiated', id) })), {
id,
duration: 30000,
position: isMobile.value ? 'top-center' : 'bottom-right',
})
}
</script>
<template>
@@ -16,7 +38,20 @@ import ToasterPWAUpdateReady from './ToasterPWAUpdateReady.vue'
id="default"
title="Default"
>
<ToasterPWAUpdateReady />
<div class="flex flex-col gap-4">
<div class="border-1 border-red">
<ToasterRoot @close="id => toast.dismiss(id)">
<ToasterPWAUpdateReady />
<Toaster />
</ToasterRoot>
</div>
<div>
<button class="border-2 border-neutral-200 rounded-lg border-solid px-2 py-1 text-nowrap dark:border-neutral-700" @click="handlePopToast">
Pop one
</button>
</div>
</div>
</Variant>
</Story>
</template>
@@ -1,13 +1,49 @@
<script setup lang="ts">
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '../../Misc/Button.vue'
import { ToasterRootInjectionKey } from './constants'
const props = defineProps<{ id?: string }>()
const emits = defineEmits<{ (e: 'update'): void }>()
const { t } = useI18n()
const toastRoot = inject(ToasterRootInjectionKey, { close: (id: string) => console.warn('No toast root provided, cannot close toast', id) })
function handleUpdate() {
emits('update')
toastRoot.close(props.id || '')
}
function handleNotNow() {
toastRoot.close(props.id || '')
}
</script>
<template>
<div w-full>
<div>
{{ t('toaster.pwaUpdateReady.message') }}
<div
w-full flex flex-col gap-1 rounded-xl
>
<div mb-1 text-nowrap>
{{ t('base.toaster.pwaUpdateReady.message') }}
</div>
<div w-full flex items-center gap-2>
<Button w-full size="sm" variant="secondary" @click="() => handleNotNow()">
<div i-solar:close-circle-line-duotone />
<div text-nowrap>
{{ t('base.toaster.pwaUpdateReady.action.notNow') }}
</div>
</Button>
<Button w-full size="sm" @click="() => handleUpdate()">
<div i-solar:check-circle-line-duotone />
<div text-nowrap>
{{ t('base.toaster.pwaUpdateReady.action.ok') }}
</div>
</Button>
</div>
</div>
</template>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { provide } from 'vue'
import { ToasterRootInjectionKey } from './constants'
const emits = defineEmits<{
(e: 'close', id: string): void
}>()
provide(ToasterRootInjectionKey, {
close: (id: string) => emits('close', id),
})
</script>
<template>
<slot />
</template>
<style>
[data-sonner-toast=""] [data-content=""] {
width: 100%;
}
</style>
@@ -0,0 +1,3 @@
import type { InjectionKey } from 'vue'
export const ToasterRootInjectionKey: InjectionKey<{ close: (id: string) => void }> = Symbol('ToasterRoot')
@@ -1 +1,2 @@
export { default as ToasterPWAUpdateReady } from './ToasterPWAUpdateReady.vue'
export { default as ToasterRoot } from './ToasterRoot.vue'
+8 -1
View File
@@ -1,19 +1,26 @@
import { join } from 'node:path'
import { join, resolve } from 'node:path'
import Vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import Yaml from 'unplugin-yaml/vite'
import Inspect from 'vite-plugin-inspect'
import { defineConfig } from 'vite'
// For Histoire
export default defineConfig({
resolve: {
alias: {
'@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')),
},
},
server: {
fs: {
allow: [join('..', '..')],
},
},
plugins: [
Yaml(),
Vue(),
Unocss(),
Inspect(),
+23
View File
@@ -694,6 +694,9 @@ importers:
localforage:
specifier: ^1.10.0
version: 1.10.0
nanoid:
specifier: ^5.1.5
version: 5.1.5
node-vibrant:
specifier: ^4.0.3
version: 4.0.3(encoding@0.1.13)
@@ -1138,6 +1141,9 @@ importers:
localforage:
specifier: ^1.10.0
version: 1.10.0
nanoid:
specifier: ^5.1.5
version: 5.1.5
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))
@@ -1250,6 +1256,9 @@ importers:
histoire:
specifier: 1.0.0-alpha.2
version: 1.0.0-alpha.2(@types/node@24.0.10)(bufferutil@4.0.9)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(utf-8-validate@5.0.10)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
unplugin-yaml:
specifier: ^3.0.1
version: 3.0.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(astro@5.10.1(@types/node@24.0.10)(encoding@0.1.13)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(rollup@4.44.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.20)(rollup@4.44.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0))
vite:
specifier: ^6.3.5
version: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
@@ -24488,6 +24497,20 @@ snapshots:
rollup: 4.44.2
vite: rolldown-vite@6.3.21(@types/node@24.0.10)(esbuild@0.25.5)(jiti@2.4.2)(less@4.3.0)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
unplugin-yaml@3.0.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(astro@5.10.1(@types/node@24.0.10)(encoding@0.1.13)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(rollup@4.44.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.20)(rollup@4.44.2)(vite@6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.44.2)
unplugin: 2.3.5
yaml: 2.8.0
optionalDependencies:
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.44.2)
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.44.2)
astro: 5.10.1(@types/node@24.0.10)(encoding@0.1.13)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(rollup@4.44.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0)
esbuild: 0.25.5
rolldown: 1.0.0-beta.20
rollup: 4.44.2
vite: 6.3.5(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
unplugin-yaml@3.0.1(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.44.2))(astro@5.10.1(@types/node@24.0.10)(encoding@0.1.13)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(rollup@4.44.2)(terser@5.43.1)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.0))(esbuild@0.25.5)(rolldown@1.0.0-beta.20)(rollup@4.44.2)(vite@7.0.2(@types/node@24.0.10)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)):
dependencies:
'@rollup/pluginutils': 5.2.0(rollup@4.44.2)