feat(stage-ui,stage-web): PWA update ready prompt
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
|
||||
export { registerSW }
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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: 好的
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Generated
+23
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user