@slidev/client 52.16.0 → 52.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/App.vue +2 -0
- package/builtin/AutoFitText.vue +54 -35
- package/composables/usePreloadImages.ts +40 -1
- package/constants.ts +1 -0
- package/env.ts +1 -1
- package/internals/PwaIndicator.vue +50 -0
- package/logic/slidePath.test.ts +29 -0
- package/logic/slidePath.ts +10 -0
- package/logic/slides.ts +2 -3
- package/package.json +19 -18
- package/setup/main.ts +10 -4
- package/setup/pwa.ts +28 -0
- package/shim.d.ts +2 -0
package/App.vue
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { watchEffect } from 'vue'
|
|
3
3
|
import { themeVars } from './env'
|
|
4
|
+
import PwaIndicator from './internals/PwaIndicator.vue'
|
|
4
5
|
import setupRoot from './setup/root'
|
|
5
6
|
|
|
6
7
|
setupRoot()
|
|
@@ -13,4 +14,5 @@ watchEffect(() => {
|
|
|
13
14
|
|
|
14
15
|
<template>
|
|
15
16
|
<RouterView />
|
|
17
|
+
<PwaIndicator v-if="__SLIDEV_FEATURE_PWA__" />
|
|
16
18
|
</template>
|
package/builtin/AutoFitText.vue
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
Think this component as the TextBox you that will see
|
|
2
|
+
Think of this component as the TextBox that you will see
|
|
5
3
|
in PowerPoint or Keynote. It will automatically resize
|
|
6
|
-
the font size based on
|
|
4
|
+
the font size based on its content to fit them in,
|
|
5
|
+
powered by fitty (https://github.com/rikschennink/fitty).
|
|
7
6
|
|
|
8
7
|
Usage:
|
|
9
8
|
|
|
@@ -11,12 +10,14 @@ Usage:
|
|
|
11
10
|
|
|
12
11
|
or
|
|
13
12
|
|
|
14
|
-
<AutoFitText :max="
|
|
13
|
+
<AutoFitText :max="100" :min="30" v-model="text"/>
|
|
15
14
|
-->
|
|
16
15
|
|
|
17
16
|
<script setup lang="ts">
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import type { FittyInstance } from 'fitty'
|
|
18
|
+
import { useResizeObserver, useVModel } from '@vueuse/core'
|
|
19
|
+
import fitty from 'fitty'
|
|
20
|
+
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
|
20
21
|
|
|
21
22
|
const props = defineProps({
|
|
22
23
|
modelValue: {
|
|
@@ -28,40 +29,59 @@ const props = defineProps({
|
|
|
28
29
|
min: {
|
|
29
30
|
default: 30,
|
|
30
31
|
},
|
|
32
|
+
multiLine: {
|
|
33
|
+
type: Boolean,
|
|
34
|
+
default: true,
|
|
35
|
+
},
|
|
31
36
|
})
|
|
32
37
|
|
|
33
|
-
const emit = defineEmits
|
|
34
|
-
|
|
35
|
-
}>()
|
|
36
|
-
const container = ref<HTMLDivElement>()
|
|
37
|
-
const inner = ref<HTMLDivElement>()
|
|
38
|
-
const size = ref(100)
|
|
39
|
-
const fontSize = computed(() => `${size.value}px`)
|
|
38
|
+
const emit = defineEmits(['update:modelValue'])
|
|
39
|
+
|
|
40
40
|
const value = useVModel(props, 'modelValue', emit)
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
42
|
+
const container = ref<HTMLDivElement>()
|
|
43
|
+
const inner = ref<HTMLDivElement>()
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
const wrap = ref('nowrap')
|
|
45
|
+
let instance: FittyInstance | undefined
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
function init() {
|
|
48
|
+
instance?.unsubscribe()
|
|
49
|
+
instance = undefined
|
|
50
|
+
if (!inner.value)
|
|
50
51
|
return
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
instance = fitty(inner.value, {
|
|
53
|
+
minSize: props.min,
|
|
54
|
+
maxSize: props.max,
|
|
55
|
+
multiLine: props.multiLine,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onMounted(() => {
|
|
60
|
+
init()
|
|
61
|
+
// Refit once web fonts are loaded, as glyph metrics may change
|
|
62
|
+
document.fonts?.ready.then(() => instance?.fit())
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
onUnmounted(() => {
|
|
66
|
+
instance?.unsubscribe()
|
|
67
|
+
instance = undefined
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
watch(() => [props.min, props.max, props.multiLine], init)
|
|
71
|
+
|
|
72
|
+
// Fitty observes content mutations and window resizes on its own,
|
|
73
|
+
// but not resizes of the container itself
|
|
74
|
+
useResizeObserver(container, () => {
|
|
75
|
+
if (!container.value || container.value.clientWidth <= 0)
|
|
53
76
|
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
newSize = Math.max(props.min, Math.min(props.max, newSize))
|
|
64
|
-
size.value = newSize
|
|
77
|
+
// If fitty was initialized while the element was hidden (e.g. on a
|
|
78
|
+
// preloaded slide), its internal state is invalid and refitting can
|
|
79
|
+
// never recover, so reinitialize it once the container is visible.
|
|
80
|
+
// A successful fit always leaves an inline font-size on the element.
|
|
81
|
+
if (inner.value && !inner.value.style.fontSize)
|
|
82
|
+
init()
|
|
83
|
+
else
|
|
84
|
+
instance?.fit()
|
|
65
85
|
})
|
|
66
86
|
</script>
|
|
67
87
|
|
|
@@ -78,11 +98,10 @@ watch([container, value, containerSize.width, innerSize.width], async () => {
|
|
|
78
98
|
<style scoped>
|
|
79
99
|
.slidev-auto-fit-text {
|
|
80
100
|
overflow: auto;
|
|
81
|
-
font-size: v-bind(fontSize);
|
|
82
|
-
white-space: v-bind(wrap);
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
.slidev-auto-fit-text-inner {
|
|
86
104
|
display: inline-block;
|
|
105
|
+
white-space: nowrap;
|
|
87
106
|
}
|
|
88
107
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SlideRoute } from '@slidev/types'
|
|
2
2
|
import type { ComputedRef, Ref } from 'vue'
|
|
3
|
-
import { watchEffect } from 'vue'
|
|
3
|
+
import { computed, ref, watchEffect } from 'vue'
|
|
4
4
|
import configs from '#slidev/configs'
|
|
5
5
|
|
|
6
6
|
const loaded = new Set<string>()
|
|
@@ -14,6 +14,23 @@ function resolveUrl(url: string): string {
|
|
|
14
14
|
return `${base}${url.startsWith('/') ? url : `/${url}`}`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const RETRY_LIMIT = 2
|
|
18
|
+
const retries = new Map<string, number>()
|
|
19
|
+
|
|
20
|
+
const preloadedCount = ref(0)
|
|
21
|
+
const totalImagesCount = ref(0)
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Progress of slide-image preloading, from 0 to 1 (1 when every preloadable
|
|
25
|
+
* image in the deck has loaded, or when the deck has no images).
|
|
26
|
+
*/
|
|
27
|
+
export const preloadProgress = computed(() =>
|
|
28
|
+
totalImagesCount.value === 0 ? 1 : preloadedCount.value / totalImagesCount.value,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** Whether all preloadable slide images have finished loading. */
|
|
32
|
+
export const preloadComplete = computed(() => preloadProgress.value >= 1)
|
|
33
|
+
|
|
17
34
|
function preloadImage(url: string): void {
|
|
18
35
|
const resolved = resolveUrl(url)
|
|
19
36
|
if (loaded.has(resolved) || loading.has(resolved))
|
|
@@ -23,9 +40,18 @@ function preloadImage(url: string): void {
|
|
|
23
40
|
img.onload = () => {
|
|
24
41
|
loading.delete(resolved)
|
|
25
42
|
loaded.add(resolved)
|
|
43
|
+
retries.delete(resolved)
|
|
44
|
+
preloadedCount.value = loaded.size
|
|
26
45
|
}
|
|
27
46
|
img.onerror = () => {
|
|
28
47
|
loading.delete(resolved)
|
|
48
|
+
// Retry transient preload failures (e.g. flaky network) instead of silently
|
|
49
|
+
// giving up; bounded so genuinely-missing assets aren't hammered.
|
|
50
|
+
const attempts = retries.get(resolved) ?? 0
|
|
51
|
+
if (attempts < RETRY_LIMIT) {
|
|
52
|
+
retries.set(resolved, attempts + 1)
|
|
53
|
+
setTimeout(preloadImage, 1000 * (attempts + 1), url)
|
|
54
|
+
}
|
|
29
55
|
}
|
|
30
56
|
img.src = resolved
|
|
31
57
|
}
|
|
@@ -50,6 +76,19 @@ export function usePreloadImages(
|
|
|
50
76
|
|
|
51
77
|
const ahead = (typeof config === 'object' && config?.ahead) || 3
|
|
52
78
|
|
|
79
|
+
// Track total preloadable images across the deck for progress reporting
|
|
80
|
+
watchEffect(() => {
|
|
81
|
+
const all = slides.value
|
|
82
|
+
if (!all?.length)
|
|
83
|
+
return
|
|
84
|
+
const urls = new Set<string>()
|
|
85
|
+
for (const route of all) {
|
|
86
|
+
for (const url of route.meta?.slide?.images ?? [])
|
|
87
|
+
urls.add(resolveUrl(url))
|
|
88
|
+
}
|
|
89
|
+
totalImagesCount.value = urls.size
|
|
90
|
+
})
|
|
91
|
+
|
|
53
92
|
// Preload current + prev + next + look-ahead window
|
|
54
93
|
watchEffect(() => {
|
|
55
94
|
const current = currentRoute.value
|
package/constants.ts
CHANGED
package/env.ts
CHANGED
|
@@ -4,7 +4,7 @@ import configs from '#slidev/configs'
|
|
|
4
4
|
|
|
5
5
|
export { configs }
|
|
6
6
|
|
|
7
|
-
export const mode =
|
|
7
|
+
export const mode = import.meta.env.DEV ? 'dev' : 'build'
|
|
8
8
|
|
|
9
9
|
export const slideAspect = computed(() => configs.aspectRatio)
|
|
10
10
|
export const slideWidth = computed(() => configs.canvasWidth)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { pwaCaching, pwaReady } from '../setup/pwa'
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<template>
|
|
6
|
+
<Transition name="slidev-pwa-fade">
|
|
7
|
+
<div
|
|
8
|
+
v-if="pwaCaching || pwaReady"
|
|
9
|
+
class="slidev-pwa-indicator"
|
|
10
|
+
:class="{ ready: pwaReady }"
|
|
11
|
+
>
|
|
12
|
+
<span v-if="pwaCaching">Caching for offline…</span>
|
|
13
|
+
<span v-else-if="pwaReady">Ready offline</span>
|
|
14
|
+
</div>
|
|
15
|
+
</Transition>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<style scoped>
|
|
19
|
+
.slidev-pwa-indicator {
|
|
20
|
+
position: fixed;
|
|
21
|
+
bottom: 1rem;
|
|
22
|
+
right: 1rem;
|
|
23
|
+
z-index: 9999;
|
|
24
|
+
padding: 0.4em 0.75em;
|
|
25
|
+
border-radius: 0.4em;
|
|
26
|
+
font-size: 0.75rem;
|
|
27
|
+
font-family: sans-serif;
|
|
28
|
+
line-height: 1.4;
|
|
29
|
+
background: rgba(0, 0, 0, 0.72);
|
|
30
|
+
color: #ccc;
|
|
31
|
+
pointer-events: none;
|
|
32
|
+
user-select: none;
|
|
33
|
+
backdrop-filter: blur(4px);
|
|
34
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.slidev-pwa-indicator.ready {
|
|
38
|
+
color: #7dce82;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.slidev-pwa-fade-enter-active,
|
|
42
|
+
.slidev-pwa-fade-leave-active {
|
|
43
|
+
transition: opacity 0.35s ease;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.slidev-pwa-fade-enter-from,
|
|
47
|
+
.slidev-pwa-fade-leave-to {
|
|
48
|
+
opacity: 0;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { SlideRoute } from '@slidev/types'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { getSlideRoutePath } from './slidePath'
|
|
4
|
+
|
|
5
|
+
function route(no: number, routeAlias?: string): SlideRoute {
|
|
6
|
+
return {
|
|
7
|
+
no,
|
|
8
|
+
meta: {
|
|
9
|
+
slide: {
|
|
10
|
+
frontmatter: {
|
|
11
|
+
routeAlias,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} as unknown as SlideRoute
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('getSlideRoutePath', () => {
|
|
19
|
+
it('returns router paths without Vite base', () => {
|
|
20
|
+
expect(getSlideRoutePath(route(2), false)).toBe('/2')
|
|
21
|
+
expect(getSlideRoutePath(route(2), true)).toBe('/presenter/2')
|
|
22
|
+
expect(getSlideRoutePath(route(2), false, true)).toBe('/export/2')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('uses route aliases', () => {
|
|
26
|
+
expect(getSlideRoutePath(route(2, 'intro'), false)).toBe('/intro')
|
|
27
|
+
expect(getSlideRoutePath(route(2, 'intro'), true)).toBe('/presenter/intro')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SlideRoute } from '@slidev/types'
|
|
2
|
+
|
|
3
|
+
export function getSlideRoutePath(
|
|
4
|
+
route: SlideRoute,
|
|
5
|
+
presenter: boolean,
|
|
6
|
+
exporting: boolean = false,
|
|
7
|
+
) {
|
|
8
|
+
const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
|
|
9
|
+
return exporting ? `/export/${no}` : presenter ? `/presenter/${no}` : `/${no}`
|
|
10
|
+
}
|
package/logic/slides.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { tryOnMounted } from '@vueuse/core'
|
|
|
3
3
|
import { computed, watch } from 'vue'
|
|
4
4
|
import { slides } from '#slidev/slides'
|
|
5
5
|
import { useSlideContext } from '../context'
|
|
6
|
+
import { getSlideRoutePath } from './slidePath'
|
|
6
7
|
|
|
7
8
|
export { slides }
|
|
8
9
|
|
|
@@ -19,9 +20,7 @@ export function getSlidePath(
|
|
|
19
20
|
) {
|
|
20
21
|
if (typeof route === 'number' || typeof route === 'string')
|
|
21
22
|
route = getSlide(route)!
|
|
22
|
-
|
|
23
|
-
const path = exporting ? `export/${no}` : presenter ? `presenter/${no}` : `${no}`
|
|
24
|
-
return `${import.meta.env.BASE_URL}${path}`
|
|
23
|
+
return getSlideRoutePath(route, presenter, exporting)
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function useIsSlideActive() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "52.
|
|
4
|
+
"version": "52.17.1",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -30,44 +30,45 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@antfu/utils": "^9.3.0",
|
|
32
32
|
"@fix-webm-duration/fix": "^1.0.1",
|
|
33
|
-
"@iconify-json/carbon": "^1.2.
|
|
33
|
+
"@iconify-json/carbon": "^1.2.24",
|
|
34
34
|
"@iconify-json/ph": "^1.2.2",
|
|
35
35
|
"@iconify-json/svg-spinners": "^1.2.4",
|
|
36
|
-
"@shikijs/engine-javascript": "^4.
|
|
37
|
-
"@shikijs/magic-move": "^4.
|
|
38
|
-
"@shikijs/monaco": "^4.
|
|
39
|
-
"@shikijs/vitepress-twoslash": "^4.
|
|
36
|
+
"@shikijs/engine-javascript": "^4.3.1",
|
|
37
|
+
"@shikijs/magic-move": "^4.3.1",
|
|
38
|
+
"@shikijs/monaco": "^4.3.1",
|
|
39
|
+
"@shikijs/vitepress-twoslash": "^4.3.1",
|
|
40
40
|
"@slidev/rough-notation": "^0.1.0",
|
|
41
41
|
"@typescript/ata": "^0.9.8",
|
|
42
|
-
"@unhead/vue": "^3.1.
|
|
43
|
-
"@unocss/extractor-mdc": "^66.7.
|
|
44
|
-
"@unocss/preset-mini": "^66.7.
|
|
45
|
-
"@unocss/reset": "^66.7.
|
|
42
|
+
"@unhead/vue": "^3.1.7",
|
|
43
|
+
"@unocss/extractor-mdc": "^66.7.5",
|
|
44
|
+
"@unocss/preset-mini": "^66.7.5",
|
|
45
|
+
"@unocss/reset": "^66.7.5",
|
|
46
46
|
"@vueuse/core": "^14.3.0",
|
|
47
47
|
"@vueuse/math": "^14.3.0",
|
|
48
48
|
"@vueuse/motion": "^3.0.3",
|
|
49
49
|
"ansis": "^4.3.1",
|
|
50
50
|
"drauu": "^1.0.0",
|
|
51
51
|
"file-saver": "^2.0.5",
|
|
52
|
+
"fitty": "^2.4.2",
|
|
52
53
|
"floating-vue": "^5.2.2",
|
|
53
|
-
"fuse.js": "^7.4.
|
|
54
|
+
"fuse.js": "^7.4.2",
|
|
54
55
|
"katex": "^0.17.0",
|
|
55
56
|
"lz-string": "^1.5.0",
|
|
56
|
-
"mermaid": "^11.
|
|
57
|
+
"mermaid": "^11.16.0",
|
|
57
58
|
"monaco-editor": "^0.55.1",
|
|
58
59
|
"nanotar": "^0.3.0",
|
|
59
60
|
"pptxgenjs": "^4.0.1",
|
|
60
61
|
"recordrtc": "^5.6.2",
|
|
61
|
-
"shiki": "^4.
|
|
62
|
+
"shiki": "^4.3.1",
|
|
62
63
|
"typescript": "^6.0.3",
|
|
63
|
-
"unocss": "^66.7.
|
|
64
|
-
"vue": "^3.5.
|
|
64
|
+
"unocss": "^66.7.5",
|
|
65
|
+
"vue": "^3.5.39",
|
|
65
66
|
"vue-router": "^5.1.0",
|
|
66
67
|
"yaml": "^2.9.0",
|
|
67
|
-
"@slidev/parser": "52.
|
|
68
|
-
"@slidev/types": "52.
|
|
68
|
+
"@slidev/parser": "52.17.1",
|
|
69
|
+
"@slidev/types": "52.17.1"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
|
-
"vite": "^8.
|
|
72
|
+
"vite": "^8.1.3"
|
|
72
73
|
}
|
|
73
74
|
}
|
package/setup/main.ts
CHANGED
|
@@ -2,12 +2,13 @@ import type { AppContext } from '@slidev/types'
|
|
|
2
2
|
import type { App } from 'vue'
|
|
3
3
|
import TwoSlashFloatingVue from '@shikijs/vitepress-twoslash/client'
|
|
4
4
|
import { createHead } from '@unhead/vue/client'
|
|
5
|
-
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
|
5
|
+
import { createMemoryHistory, createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
|
6
6
|
import setups from '#slidev/setups/main'
|
|
7
7
|
import { createVClickDirectives } from '../modules/v-click'
|
|
8
8
|
import { createVDragDirective } from '../modules/v-drag'
|
|
9
9
|
import { createVMarkDirective } from '../modules/v-mark'
|
|
10
10
|
import { createVMotionDirectives } from '../modules/v-motion'
|
|
11
|
+
import { setupPWA } from '../setup/pwa'
|
|
11
12
|
import setupRoutes from '../setup/routes'
|
|
12
13
|
import '../styles'
|
|
13
14
|
|
|
@@ -21,9 +22,11 @@ export default async function setupMain(app: App) {
|
|
|
21
22
|
window.addEventListener('resize', setMaxHeight)
|
|
22
23
|
|
|
23
24
|
const router = createRouter({
|
|
24
|
-
history:
|
|
25
|
-
?
|
|
26
|
-
:
|
|
25
|
+
history: __SLIDEV_MEMORY_ROUTE__
|
|
26
|
+
? createMemoryHistory(import.meta.env.BASE_URL)
|
|
27
|
+
: __SLIDEV_HASH_ROUTE__
|
|
28
|
+
? createWebHashHistory(import.meta.env.BASE_URL)
|
|
29
|
+
: createWebHistory(import.meta.env.BASE_URL),
|
|
27
30
|
routes: setupRoutes(),
|
|
28
31
|
})
|
|
29
32
|
|
|
@@ -42,4 +45,7 @@ export default async function setupMain(app: App) {
|
|
|
42
45
|
|
|
43
46
|
for (const setup of setups)
|
|
44
47
|
await setup(context)
|
|
48
|
+
|
|
49
|
+
if (__SLIDEV_FEATURE_PWA__)
|
|
50
|
+
setupPWA()
|
|
45
51
|
}
|
package/setup/pwa.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const pwaCaching = ref(false)
|
|
4
|
+
export const pwaReady = ref(false)
|
|
5
|
+
|
|
6
|
+
export async function setupPWA() {
|
|
7
|
+
if (!__SLIDEV_FEATURE_PWA__)
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
const { registerSW } = await import('virtual:pwa-register')
|
|
11
|
+
|
|
12
|
+
pwaCaching.value = true
|
|
13
|
+
|
|
14
|
+
registerSW({
|
|
15
|
+
immediate: true,
|
|
16
|
+
onOfflineReady() {
|
|
17
|
+
pwaCaching.value = false
|
|
18
|
+
pwaReady.value = true
|
|
19
|
+
// Auto-dismiss "ready" indicator after 4 seconds
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
pwaReady.value = false
|
|
22
|
+
}, 4000)
|
|
23
|
+
},
|
|
24
|
+
onRegisteredSW(_swUrl, _registration) {
|
|
25
|
+
// SW registered; caching is in progress until onOfflineReady fires
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
}
|