@slidev/client 0.50.0-beta.8 → 0.50.0
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/composables/useClicks.ts +6 -5
- package/composables/useDragElements.ts +30 -24
- package/composables/useNav.ts +17 -15
- package/composables/usePrintStyles.ts +28 -0
- package/constants.ts +1 -0
- package/internals/ClicksSlider.vue +1 -1
- package/internals/ContextMenu.vue +1 -1
- package/internals/DragControl.vue +1 -1
- package/internals/DrawingControls.vue +1 -1
- package/internals/ExportPdfTip.vue +90 -0
- package/internals/FormCheckbox.vue +16 -0
- package/internals/FormItem.vue +41 -0
- package/internals/IconButton.vue +7 -2
- package/internals/MenuButton.vue +1 -1
- package/internals/Modal.vue +1 -1
- package/internals/NavControls.vue +10 -4
- package/internals/PrintContainer.vue +2 -21
- package/internals/PrintSlide.vue +4 -3
- package/internals/PrintSlideClick.vue +11 -3
- package/internals/QuickOverview.vue +2 -2
- package/internals/Settings.vue +5 -2
- package/internals/SideEditor.vue +1 -1
- package/internals/SlidesShow.vue +7 -3
- package/internals/WebCamera.vue +2 -2
- package/layouts/error.vue +5 -1
- package/logic/dark.ts +11 -0
- package/logic/screenshot.ts +61 -0
- package/logic/shortcuts.ts +36 -35
- package/logic/slides.ts +2 -1
- package/main.ts +7 -3
- package/modules/v-mark.ts +6 -0
- package/package.json +18 -16
- package/pages/export.vue +369 -0
- package/pages/overview.vue +1 -1
- package/pages/play.vue +1 -4
- package/pages/presenter.vue +9 -0
- package/pages/print.vue +0 -2
- package/setup/monaco.ts +14 -14
- package/setup/root.ts +6 -2
- package/setup/routes.ts +23 -12
- package/state/index.ts +2 -1
- package/styles/index.css +9 -4
- package/uno.config.ts +14 -0
- package/internals/PrintStyle.vue +0 -16
|
@@ -109,7 +109,7 @@ watchEffect(() => {
|
|
|
109
109
|
>
|
|
110
110
|
<div
|
|
111
111
|
v-if="showOverview"
|
|
112
|
-
class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-
|
|
112
|
+
class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-modal bg-main !bg-opacity-75 p-16 py-20 overflow-y-auto backdrop-blur-5px select-none"
|
|
113
113
|
@click="close"
|
|
114
114
|
>
|
|
115
115
|
<div
|
|
@@ -157,7 +157,7 @@ watchEffect(() => {
|
|
|
157
157
|
</div>
|
|
158
158
|
</div>
|
|
159
159
|
</Transition>
|
|
160
|
-
<div v-if="showOverview" class="fixed top-4 right-4 z-
|
|
160
|
+
<div v-if="showOverview" class="fixed top-4 right-4 z-modal text-gray-400 flex flex-col items-center gap-2">
|
|
161
161
|
<IconButton title="Close" class="text-2xl" @click="close">
|
|
162
162
|
<div class="i-carbon:close" />
|
|
163
163
|
</IconButton>
|
package/internals/Settings.vue
CHANGED
|
@@ -30,8 +30,11 @@ const wakeLockItems: SelectionItem<boolean>[] = [
|
|
|
30
30
|
</script>
|
|
31
31
|
|
|
32
32
|
<template>
|
|
33
|
-
<div class="text-sm select-none">
|
|
33
|
+
<div class="text-sm select-none mb-2">
|
|
34
34
|
<SelectList v-model="slideScale" title="Scale" :items="scaleItems" />
|
|
35
|
-
<SelectList
|
|
35
|
+
<SelectList
|
|
36
|
+
v-if="__SLIDEV_FEATURE_WAKE_LOCK__ && isSupported"
|
|
37
|
+
v-model="wakeLockEnabled" title="Wake lock" :items="wakeLockItems"
|
|
38
|
+
/>
|
|
36
39
|
</div>
|
|
37
40
|
</template>
|
package/internals/SideEditor.vue
CHANGED
|
@@ -121,7 +121,7 @@ throttledWatch(
|
|
|
121
121
|
|
|
122
122
|
<template>
|
|
123
123
|
<div
|
|
124
|
-
v-if="resize" class="fixed bg-gray-400 select-none opacity-0 hover:opacity-10 z-
|
|
124
|
+
v-if="resize" class="fixed bg-gray-400 select-none opacity-0 hover:opacity-10 z-dragging"
|
|
125
125
|
:class="vertical ? 'left-0 right-0 w-full h-10px' : 'top-0 bottom-0 w-10px h-full'" :style="{
|
|
126
126
|
opacity: handlerDown ? '0.3' : undefined,
|
|
127
127
|
bottom: vertical ? `${editorHeight - 5}px` : undefined,
|
package/internals/SlidesShow.vue
CHANGED
|
@@ -26,6 +26,7 @@ const {
|
|
|
26
26
|
isPrintMode,
|
|
27
27
|
isPrintWithClicks,
|
|
28
28
|
clicksDirection,
|
|
29
|
+
printRange,
|
|
29
30
|
} = useNav()
|
|
30
31
|
|
|
31
32
|
function preloadRoute(route: SlideRoute) {
|
|
@@ -55,7 +56,10 @@ const DrawingLayer = shallowRef<any>()
|
|
|
55
56
|
if (__SLIDEV_FEATURE_DRAWINGS__ || __SLIDEV_FEATURE_DRAWINGS_PERSIST__)
|
|
56
57
|
import('./DrawingLayer.vue').then(v => DrawingLayer.value = v.default)
|
|
57
58
|
|
|
58
|
-
const loadedRoutes = computed(() =>
|
|
59
|
+
const loadedRoutes = computed(() => isPrintMode.value
|
|
60
|
+
? printRange.value.map(no => slides.value[no - 1])
|
|
61
|
+
: slides.value.filter(r => r.meta?.__preloaded || r === currentSlideRoute.value),
|
|
62
|
+
)
|
|
59
63
|
|
|
60
64
|
function onAfterLeave() {
|
|
61
65
|
// After transition, we disable it so HMR won't trigger it again
|
|
@@ -72,8 +76,8 @@ function onAfterLeave() {
|
|
|
72
76
|
|
|
73
77
|
<!-- Slides -->
|
|
74
78
|
<component
|
|
75
|
-
:is="hasViewTransition ? 'div' : TransitionGroup"
|
|
76
|
-
v-bind="skipTransition ? {} : currentTransition"
|
|
79
|
+
:is="hasViewTransition && !isPrintMode ? 'div' : TransitionGroup"
|
|
80
|
+
v-bind="skipTransition || isPrintMode ? {} : currentTransition"
|
|
77
81
|
id="slideshow"
|
|
78
82
|
tag="div"
|
|
79
83
|
:class="{
|
package/internals/WebCamera.vue
CHANGED
|
@@ -63,7 +63,7 @@ onMounted(fixPosition)
|
|
|
63
63
|
<template>
|
|
64
64
|
<div
|
|
65
65
|
v-if="streamCamera && showAvatar && currentCamera !== 'none'"
|
|
66
|
-
class="fixed z-
|
|
66
|
+
class="fixed z-camera"
|
|
67
67
|
:style="containerStyle"
|
|
68
68
|
>
|
|
69
69
|
<div
|
|
@@ -83,7 +83,7 @@ onMounted(fixPosition)
|
|
|
83
83
|
|
|
84
84
|
<div
|
|
85
85
|
ref="handler"
|
|
86
|
-
class="absolute bottom-0 right-0 rounded-full bg-main shadow opacity-0 shadow z-
|
|
86
|
+
class="absolute bottom-0 right-0 rounded-full bg-main shadow opacity-0 shadow z-dragging hover:opacity-100 dark:border dark:border-true-gray-700"
|
|
87
87
|
:style="handleStyle"
|
|
88
88
|
:class="handlerDown ? '!opacity-100' : ''"
|
|
89
89
|
/>
|
package/layouts/error.vue
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="px-4 py-10 text-center text-red-700 dark:text-red-500 font-bold font-mono">
|
|
3
|
-
|
|
3
|
+
{{
|
|
4
|
+
__SLIDEV_HAS_SERVER__
|
|
5
|
+
? 'An error occurred on this slide. Check the terminal for more information.'
|
|
6
|
+
: 'Failed to fetch this slide. Please check your network connection.'
|
|
7
|
+
}}
|
|
4
8
|
</div>
|
|
5
9
|
</template>
|
package/logic/dark.ts
CHANGED
|
@@ -26,12 +26,23 @@ export const isDark = computed<boolean>({
|
|
|
26
26
|
export const toggleDark = useToggle(isDark)
|
|
27
27
|
|
|
28
28
|
if (isClient) {
|
|
29
|
+
const CSS_DISABLE_TRANS = '*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}'
|
|
30
|
+
|
|
29
31
|
watch(
|
|
30
32
|
isDark,
|
|
31
33
|
(v) => {
|
|
34
|
+
const style = window!.document.createElement('style')
|
|
35
|
+
style.appendChild(document.createTextNode(CSS_DISABLE_TRANS))
|
|
36
|
+
window!.document.head.appendChild(style)
|
|
37
|
+
|
|
32
38
|
const html = document.querySelector('html')!
|
|
33
39
|
html.classList.toggle('dark', v)
|
|
34
40
|
html.classList.toggle('light', !v)
|
|
41
|
+
|
|
42
|
+
// Calling getComputedStyle forces the browser to redraw
|
|
43
|
+
// @ts-expect-error unused variable
|
|
44
|
+
const _ = window!.getComputedStyle(style!).opacity
|
|
45
|
+
document.head.removeChild(style!)
|
|
35
46
|
},
|
|
36
47
|
{ immediate: true },
|
|
37
48
|
)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export async function startScreenshotSession(width: number, height: number) {
|
|
4
|
+
const canvas = document.createElement('canvas')
|
|
5
|
+
canvas.width = width
|
|
6
|
+
canvas.height = height
|
|
7
|
+
const context = canvas.getContext('2d')!
|
|
8
|
+
const video = document.createElement('video')
|
|
9
|
+
video.width = width
|
|
10
|
+
video.height = height
|
|
11
|
+
|
|
12
|
+
const captureStream = ref<MediaStream | null>(await navigator.mediaDevices.getDisplayMedia({
|
|
13
|
+
video: {
|
|
14
|
+
// Use a rather small frame rate
|
|
15
|
+
frameRate: 10,
|
|
16
|
+
// @ts-expect-error missing types
|
|
17
|
+
cursor: 'never',
|
|
18
|
+
},
|
|
19
|
+
selfBrowserSurface: 'include',
|
|
20
|
+
preferCurrentTab: true,
|
|
21
|
+
}))
|
|
22
|
+
captureStream.value!.addEventListener('inactive', dispose)
|
|
23
|
+
|
|
24
|
+
video.srcObject = captureStream.value!
|
|
25
|
+
video.play()
|
|
26
|
+
|
|
27
|
+
function screenshot(element: HTMLElement) {
|
|
28
|
+
if (!captureStream.value)
|
|
29
|
+
throw new Error('captureStream inactive')
|
|
30
|
+
context.clearRect(0, 0, width, height)
|
|
31
|
+
const { left, top, width: elWidth } = element.getBoundingClientRect()
|
|
32
|
+
context.drawImage(
|
|
33
|
+
video,
|
|
34
|
+
left * window.devicePixelRatio,
|
|
35
|
+
top * window.devicePixelRatio,
|
|
36
|
+
elWidth * window.devicePixelRatio,
|
|
37
|
+
elWidth / width * height * window.devicePixelRatio,
|
|
38
|
+
0,
|
|
39
|
+
0,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
)
|
|
43
|
+
return canvas.toDataURL('image/png')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dispose() {
|
|
47
|
+
captureStream.value?.getTracks().forEach(track => track.stop())
|
|
48
|
+
captureStream.value = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isActive: computed(() => !!captureStream.value),
|
|
53
|
+
screenshot,
|
|
54
|
+
dispose,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ScreenshotSession = Awaited<ReturnType<typeof startScreenshotSession>>
|
|
59
|
+
|
|
60
|
+
const chromeVersion = window.navigator.userAgent.match(/Chrome\/(\d+)/)?.[1]
|
|
61
|
+
export const isScreenshotSupported = chromeVersion ? Number(chromeVersion) >= 94 : false
|
package/logic/shortcuts.ts
CHANGED
|
@@ -4,46 +4,15 @@ import type { Ref } from 'vue'
|
|
|
4
4
|
import { onKeyStroke } from '@vueuse/core'
|
|
5
5
|
import { and, not } from '@vueuse/math'
|
|
6
6
|
import { watch } from 'vue'
|
|
7
|
+
import { useNav } from '../composables/useNav'
|
|
7
8
|
import setupShortcuts from '../setup/shortcuts'
|
|
8
9
|
import { fullscreen, isInputting, isOnFocus, magicKeys, shortcutsEnabled } from '../state'
|
|
9
10
|
|
|
10
|
-
const _shortcut = and(not(isInputting), not(isOnFocus), shortcutsEnabled)
|
|
11
|
-
|
|
12
|
-
export function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
|
|
13
|
-
if (typeof key === 'string')
|
|
14
|
-
key = magicKeys[key]
|
|
15
|
-
|
|
16
|
-
const source = and(key, _shortcut)
|
|
17
|
-
let count = 0
|
|
18
|
-
let timer: any
|
|
19
|
-
const trigger = () => {
|
|
20
|
-
clearTimeout(timer)
|
|
21
|
-
if (!source.value) {
|
|
22
|
-
count = 0
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
if (autoRepeat) {
|
|
26
|
-
timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
|
|
27
|
-
count++
|
|
28
|
-
}
|
|
29
|
-
fn()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return watch(source, trigger, { flush: 'sync' })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function strokeShortcut(key: KeyFilter, fn: Fn) {
|
|
36
|
-
return onKeyStroke(key, (ev) => {
|
|
37
|
-
if (!_shortcut.value)
|
|
38
|
-
return
|
|
39
|
-
if (!ev.repeat)
|
|
40
|
-
fn()
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
11
|
export function registerShortcuts() {
|
|
45
|
-
const
|
|
12
|
+
const { isPrintMode } = useNav()
|
|
13
|
+
const enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode), shortcutsEnabled)
|
|
46
14
|
|
|
15
|
+
const allShortcuts = setupShortcuts()
|
|
47
16
|
const shortcuts = new Map<string | Ref<boolean>, ShortcutOptions>(
|
|
48
17
|
allShortcuts.map((options: ShortcutOptions) => [options.key, options]),
|
|
49
18
|
)
|
|
@@ -54,4 +23,36 @@ export function registerShortcuts() {
|
|
|
54
23
|
})
|
|
55
24
|
|
|
56
25
|
strokeShortcut('f', () => fullscreen.toggle())
|
|
26
|
+
|
|
27
|
+
function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
|
|
28
|
+
if (typeof key === 'string')
|
|
29
|
+
key = magicKeys[key]
|
|
30
|
+
|
|
31
|
+
const source = and(key, enabled)
|
|
32
|
+
let count = 0
|
|
33
|
+
let timer: any
|
|
34
|
+
const trigger = () => {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
if (!source.value) {
|
|
37
|
+
count = 0
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (autoRepeat) {
|
|
41
|
+
timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
|
|
42
|
+
count++
|
|
43
|
+
}
|
|
44
|
+
fn()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return watch(source, trigger, { flush: 'sync' })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function strokeShortcut(key: KeyFilter, fn: Fn) {
|
|
51
|
+
return onKeyStroke(key, (ev) => {
|
|
52
|
+
if (!enabled.value)
|
|
53
|
+
return
|
|
54
|
+
if (!ev.repeat)
|
|
55
|
+
fn()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
57
58
|
}
|
package/logic/slides.ts
CHANGED
|
@@ -15,11 +15,12 @@ export function getSlide(no: number | string) {
|
|
|
15
15
|
export function getSlidePath(
|
|
16
16
|
route: SlideRoute | number | string,
|
|
17
17
|
presenter: boolean,
|
|
18
|
+
exporting: boolean = false,
|
|
18
19
|
) {
|
|
19
20
|
if (typeof route === 'number' || typeof route === 'string')
|
|
20
21
|
route = getSlide(route)!
|
|
21
22
|
const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
|
|
22
|
-
return presenter ? `/presenter/${no}` : `/${no}`
|
|
23
|
+
return exporting ? `/export/${no}` : presenter ? `/presenter/${no}` : `/${no}`
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function useIsSlideActive() {
|
package/main.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { createApp } from 'vue'
|
|
|
4
4
|
import App from './App.vue'
|
|
5
5
|
import setupMain from './setup/main'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
async function main() {
|
|
8
|
+
const app = createApp(App)
|
|
9
|
+
await setupMain(app)
|
|
10
|
+
app.mount('#app')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
main()
|
package/modules/v-mark.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { RawAtValue } from '@slidev/types'
|
|
|
3
3
|
import type { App } from 'vue'
|
|
4
4
|
import { annotate } from '@slidev/rough-notation'
|
|
5
5
|
import { computed, watchEffect } from 'vue'
|
|
6
|
+
import { useNav } from '../composables/useNav'
|
|
6
7
|
import { resolveClick } from './v-click'
|
|
7
8
|
|
|
8
9
|
export interface RoughDirectiveOptions extends Partial<RoughAnnotationConfig> {
|
|
@@ -78,6 +79,8 @@ export function createVMarkDirective() {
|
|
|
78
79
|
name: 'v-mark',
|
|
79
80
|
|
|
80
81
|
mounted: (el, binding) => {
|
|
82
|
+
const { isPrintMode } = useNav()
|
|
83
|
+
|
|
81
84
|
const options = computed(() => {
|
|
82
85
|
const bindingOptions = (typeof binding.value === 'object' && !Array.isArray(binding.value))
|
|
83
86
|
? { ...binding.value }
|
|
@@ -109,6 +112,9 @@ export function createVMarkDirective() {
|
|
|
109
112
|
}
|
|
110
113
|
options.type ||= 'underline'
|
|
111
114
|
|
|
115
|
+
if (isPrintMode.value)
|
|
116
|
+
options.animationDuration = 1 /* millisecond */
|
|
117
|
+
|
|
112
118
|
return options
|
|
113
119
|
})
|
|
114
120
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.50.0
|
|
4
|
+
"version": "0.50.0",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,15 +29,15 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@antfu/utils": "^0.7.10",
|
|
32
|
-
"@iconify-json/carbon": "^1.2.
|
|
33
|
-
"@iconify-json/ph": "^1.2.
|
|
34
|
-
"@iconify-json/svg-spinners": "^1.2.
|
|
35
|
-
"@shikijs/monaco": "^1.24.
|
|
36
|
-
"@shikijs/vitepress-twoslash": "^1.24.
|
|
32
|
+
"@iconify-json/carbon": "^1.2.5",
|
|
33
|
+
"@iconify-json/ph": "^1.2.2",
|
|
34
|
+
"@iconify-json/svg-spinners": "^1.2.2",
|
|
35
|
+
"@shikijs/monaco": "^1.24.2",
|
|
36
|
+
"@shikijs/vitepress-twoslash": "^1.24.2",
|
|
37
37
|
"@slidev/rough-notation": "^0.1.0",
|
|
38
38
|
"@typescript/ata": "^0.9.7",
|
|
39
|
-
"@unhead/vue": "^1.11.
|
|
40
|
-
"@unocss/reset": "^0.65.
|
|
39
|
+
"@unhead/vue": "^1.11.14",
|
|
40
|
+
"@unocss/reset": "^0.65.1",
|
|
41
41
|
"@vueuse/core": "^12.0.0",
|
|
42
42
|
"@vueuse/math": "^12.0.0",
|
|
43
43
|
"@vueuse/motion": "^2.2.6",
|
|
@@ -46,23 +46,25 @@
|
|
|
46
46
|
"floating-vue": "^5.2.2",
|
|
47
47
|
"fuse.js": "^7.0.0",
|
|
48
48
|
"html-to-image": "^1.11.11",
|
|
49
|
-
"katex": "^0.16.
|
|
49
|
+
"katex": "^0.16.17",
|
|
50
50
|
"lz-string": "^1.5.0",
|
|
51
51
|
"mermaid": "^11.4.1",
|
|
52
52
|
"monaco-editor": "0.51.0",
|
|
53
|
-
"
|
|
53
|
+
"nanotar": "^0.1.1",
|
|
54
|
+
"pptxgenjs": "^3.12.0",
|
|
55
|
+
"prettier": "^3.4.2",
|
|
54
56
|
"recordrtc": "^5.6.2",
|
|
55
|
-
"shiki": "^1.24.
|
|
56
|
-
"shiki-magic-move": "^0.5.
|
|
57
|
+
"shiki": "^1.24.2",
|
|
58
|
+
"shiki-magic-move": "^0.5.2",
|
|
57
59
|
"typescript": "5.6.3",
|
|
58
|
-
"unocss": "^0.65.
|
|
60
|
+
"unocss": "^0.65.1",
|
|
59
61
|
"vue": "^3.5.13",
|
|
60
62
|
"vue-router": "^4.5.0",
|
|
61
63
|
"yaml": "^2.6.1",
|
|
62
|
-
"@slidev/parser": "0.50.0
|
|
63
|
-
"@slidev/types": "0.50.0
|
|
64
|
+
"@slidev/parser": "0.50.0",
|
|
65
|
+
"@slidev/types": "0.50.0"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
|
-
"vite": "^6.0.
|
|
68
|
+
"vite": "^6.0.3"
|
|
67
69
|
}
|
|
68
70
|
}
|