@slidev/client 0.50.0 → 0.51.0-beta.2
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/builtin/VSwitch.ts +3 -3
- package/composables/useClicks.ts +1 -1
- package/composables/useHideCursorIdle.ts +35 -0
- package/composables/useNav.ts +3 -3
- package/composables/useTimer.ts +1 -1
- package/composables/useWakeLock.ts +10 -6
- package/constants.ts +0 -1
- package/internals/Badge.vue +48 -0
- package/internals/{DevicesList.vue → DevicesSelectors.vue} +14 -4
- package/internals/FormItem.vue +6 -3
- package/internals/FormSlider.vue +68 -0
- package/internals/MenuButton.vue +2 -2
- package/internals/NavControls.vue +16 -9
- package/internals/QuickOverview.vue +24 -4
- package/internals/RecordingControls.vue +2 -2
- package/internals/RecordingDialog.vue +4 -14
- package/internals/ScreenCaptureMirror.vue +45 -0
- package/internals/SegmentControl.vue +29 -0
- package/internals/SelectList.vue +1 -9
- package/internals/Settings.vue +104 -30
- package/internals/SlideContainer.vue +33 -18
- package/internals/SlidesShow.vue +4 -5
- package/internals/SyncControls.vue +103 -0
- package/logic/color.ts +64 -0
- package/logic/snapshot.ts +73 -52
- package/package.json +12 -13
- package/pages/export.vue +24 -22
- package/pages/notes.vue +3 -3
- package/pages/play.vue +24 -2
- package/pages/presenter.vue +37 -18
- package/setup/root.ts +31 -24
- package/state/drawings.ts +5 -1
- package/state/index.ts +1 -56
- package/state/shared.ts +0 -7
- package/state/snapshot.ts +1 -1
- package/state/storage.ts +97 -0
- package/styles/index.css +11 -1
- package/uno.config.ts +1 -0
- package/logic/hmr.ts +0 -3
package/internals/Settings.vue
CHANGED
|
@@ -1,40 +1,114 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { SelectionItem } from './types'
|
|
3
2
|
import { useWakeLock } from '@vueuse/core'
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
value: 0,
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
display: '1:1',
|
|
14
|
-
value: 1,
|
|
15
|
-
},
|
|
16
|
-
]
|
|
3
|
+
import { useNav } from '../composables/useNav'
|
|
4
|
+
import { hideCursorIdle, slideScale, viewerCssFilter, viewerCssFilterDefaults, wakeLockEnabled } from '../state'
|
|
5
|
+
import FormCheckbox from './FormCheckbox.vue'
|
|
6
|
+
import FormItem from './FormItem.vue'
|
|
7
|
+
import FormSlider from './FormSlider.vue'
|
|
8
|
+
import SegmentControl from './SegmentControl.vue'
|
|
17
9
|
|
|
10
|
+
const { isPresenter } = useNav()
|
|
18
11
|
const { isSupported } = useWakeLock()
|
|
19
|
-
|
|
20
|
-
const wakeLockItems: SelectionItem<boolean>[] = [
|
|
21
|
-
{
|
|
22
|
-
display: 'Enabled',
|
|
23
|
-
value: true,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
display: 'Disabled',
|
|
27
|
-
value: false,
|
|
28
|
-
},
|
|
29
|
-
]
|
|
30
12
|
</script>
|
|
31
13
|
|
|
32
14
|
<template>
|
|
33
|
-
<div
|
|
34
|
-
<
|
|
35
|
-
|
|
15
|
+
<div text-sm select-none flex="~ col gap-1" min-w-30 px4>
|
|
16
|
+
<FormItem
|
|
17
|
+
title="Invert"
|
|
18
|
+
:dot="viewerCssFilter.invert !== viewerCssFilterDefaults.invert"
|
|
19
|
+
@reset="viewerCssFilter.invert = viewerCssFilterDefaults.invert"
|
|
20
|
+
>
|
|
21
|
+
<FormCheckbox v-model="viewerCssFilter.invert" />
|
|
22
|
+
</FormItem>
|
|
23
|
+
<FormItem
|
|
24
|
+
title="Brightness"
|
|
25
|
+
:dot="viewerCssFilter.brightness !== viewerCssFilterDefaults.brightness"
|
|
26
|
+
@reset="viewerCssFilter.brightness = viewerCssFilterDefaults.brightness"
|
|
27
|
+
>
|
|
28
|
+
<FormSlider
|
|
29
|
+
v-model="viewerCssFilter.brightness"
|
|
30
|
+
:max="1.5"
|
|
31
|
+
:min="0.5"
|
|
32
|
+
:step="0.02"
|
|
33
|
+
:default="viewerCssFilterDefaults.brightness"
|
|
34
|
+
/>
|
|
35
|
+
</FormItem>
|
|
36
|
+
<FormItem
|
|
37
|
+
title="Contrast"
|
|
38
|
+
:dot="viewerCssFilter.contrast !== viewerCssFilterDefaults.contrast"
|
|
39
|
+
@reset="viewerCssFilter.contrast = viewerCssFilterDefaults.contrast"
|
|
40
|
+
>
|
|
41
|
+
<FormSlider
|
|
42
|
+
v-model="viewerCssFilter.contrast"
|
|
43
|
+
:max="1.5"
|
|
44
|
+
:min="0.5"
|
|
45
|
+
:step="0.02"
|
|
46
|
+
:default="viewerCssFilterDefaults.contrast"
|
|
47
|
+
/>
|
|
48
|
+
</FormItem>
|
|
49
|
+
<FormItem
|
|
50
|
+
title="Saturation"
|
|
51
|
+
:dot="viewerCssFilter.saturate !== viewerCssFilterDefaults.saturate"
|
|
52
|
+
@reset="viewerCssFilter.saturate = viewerCssFilterDefaults.saturate"
|
|
53
|
+
>
|
|
54
|
+
<FormSlider
|
|
55
|
+
v-model="viewerCssFilter.saturate"
|
|
56
|
+
:max="1.5"
|
|
57
|
+
:min="0.5"
|
|
58
|
+
:step="0.02"
|
|
59
|
+
:default="viewerCssFilterDefaults.saturate"
|
|
60
|
+
/>
|
|
61
|
+
</FormItem>
|
|
62
|
+
<FormItem
|
|
63
|
+
title="Sepia"
|
|
64
|
+
:dot="viewerCssFilter.sepia !== viewerCssFilterDefaults.sepia"
|
|
65
|
+
@reset="viewerCssFilter.sepia = viewerCssFilterDefaults.sepia"
|
|
66
|
+
>
|
|
67
|
+
<FormSlider
|
|
68
|
+
v-model="viewerCssFilter.sepia"
|
|
69
|
+
:max="2"
|
|
70
|
+
:min="-2"
|
|
71
|
+
:step="0.02"
|
|
72
|
+
:default="viewerCssFilterDefaults.sepia"
|
|
73
|
+
/>
|
|
74
|
+
</FormItem>
|
|
75
|
+
<FormItem
|
|
76
|
+
title="Hue Rotate"
|
|
77
|
+
:dot="viewerCssFilter.hueRotate !== viewerCssFilterDefaults.hueRotate"
|
|
78
|
+
@reset="viewerCssFilter.hueRotate = viewerCssFilterDefaults.hueRotate"
|
|
79
|
+
>
|
|
80
|
+
<FormSlider
|
|
81
|
+
v-model="viewerCssFilter.hueRotate"
|
|
82
|
+
:max="180"
|
|
83
|
+
:min="-180"
|
|
84
|
+
:step="0.1"
|
|
85
|
+
:default="viewerCssFilterDefaults.hueRotate"
|
|
86
|
+
/>
|
|
87
|
+
</FormItem>
|
|
88
|
+
<div class="h-1px opacity-5 bg-current w-full my2" />
|
|
89
|
+
<FormItem
|
|
90
|
+
v-if="!isPresenter"
|
|
91
|
+
title="Slide Scale"
|
|
92
|
+
>
|
|
93
|
+
<SegmentControl
|
|
94
|
+
v-model="slideScale"
|
|
95
|
+
:options="[
|
|
96
|
+
{ label: 'Fit', value: 0 },
|
|
97
|
+
{ label: '1:1', value: 1 },
|
|
98
|
+
]"
|
|
99
|
+
/>
|
|
100
|
+
</FormItem>
|
|
101
|
+
<FormItem
|
|
36
102
|
v-if="__SLIDEV_FEATURE_WAKE_LOCK__ && isSupported"
|
|
37
|
-
|
|
38
|
-
|
|
103
|
+
title="Wake Lock"
|
|
104
|
+
>
|
|
105
|
+
<FormCheckbox v-model="wakeLockEnabled" />
|
|
106
|
+
</FormItem>
|
|
107
|
+
<FormItem
|
|
108
|
+
v-if="!isPresenter"
|
|
109
|
+
title="Hide Idle Cursor"
|
|
110
|
+
>
|
|
111
|
+
<FormCheckbox v-model="hideCursorIdle" />
|
|
112
|
+
</FormItem>
|
|
39
113
|
</div>
|
|
40
114
|
</template>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { provideLocal, useElementSize, useStyleTag } from '@vueuse/core'
|
|
3
|
-
import { computed,
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
4
|
import { useNav } from '../composables/useNav'
|
|
5
5
|
import { injectionSlideElement, injectionSlideScale } from '../constants'
|
|
6
6
|
import { slideAspect, slideHeight, slideWidth } from '../env'
|
|
7
|
+
import { isDark } from '../logic/dark'
|
|
7
8
|
import { snapshotManager } from '../logic/snapshot'
|
|
8
9
|
import { slideScale } from '../state'
|
|
9
10
|
|
|
@@ -26,6 +27,10 @@ const props = defineProps({
|
|
|
26
27
|
type: Boolean,
|
|
27
28
|
default: false,
|
|
28
29
|
},
|
|
30
|
+
contentStyle: {
|
|
31
|
+
type: Object,
|
|
32
|
+
default: () => ({}),
|
|
33
|
+
},
|
|
29
34
|
})
|
|
30
35
|
|
|
31
36
|
const { isPrintMode } = useNav()
|
|
@@ -44,6 +49,7 @@ const scale = computed(() => {
|
|
|
44
49
|
})
|
|
45
50
|
|
|
46
51
|
const contentStyle = computed(() => ({
|
|
52
|
+
...props.contentStyle,
|
|
47
53
|
'height': `${slideHeight.value}px`,
|
|
48
54
|
'width': `${slideWidth.value}px`,
|
|
49
55
|
'transform': `translate(-50%, -50%) scale(${scale.value})`,
|
|
@@ -65,32 +71,41 @@ provideLocal(injectionSlideScale, scale)
|
|
|
65
71
|
provideLocal(injectionSlideElement, slideElement)
|
|
66
72
|
|
|
67
73
|
const snapshot = computed(() => {
|
|
68
|
-
if (
|
|
74
|
+
if (props.no == null || !props.useSnapshot)
|
|
69
75
|
return undefined
|
|
70
|
-
return snapshotManager.getSnapshot(props.no)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
onMounted(() => {
|
|
74
|
-
if (container.value && props.useSnapshot && props.no != null) {
|
|
75
|
-
snapshotManager.captureSnapshot(props.no, container.value)
|
|
76
|
-
}
|
|
76
|
+
return snapshotManager.getSnapshot(props.no, isDark.value)
|
|
77
77
|
})
|
|
78
78
|
</script>
|
|
79
79
|
|
|
80
80
|
<template>
|
|
81
|
-
<div
|
|
82
|
-
|
|
81
|
+
<div
|
|
82
|
+
v-if="!snapshot"
|
|
83
|
+
:id="isMain ? 'slide-container' : undefined"
|
|
84
|
+
ref="container"
|
|
85
|
+
class="slidev-slide-container"
|
|
86
|
+
:style="containerStyle"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
:id="isMain ? 'slide-content' : undefined"
|
|
90
|
+
ref="slideElement"
|
|
91
|
+
class="slidev-slide-content"
|
|
92
|
+
:style="contentStyle"
|
|
93
|
+
>
|
|
83
94
|
<slot />
|
|
84
95
|
</div>
|
|
85
96
|
<slot name="controls" />
|
|
86
97
|
</div>
|
|
87
|
-
<!-- Image
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
98
|
+
<!-- Image Snapshot -->
|
|
99
|
+
<div v-else class="slidev-slide-container w-full h-full relative">
|
|
100
|
+
<img
|
|
101
|
+
:src="snapshot"
|
|
102
|
+
class="w-full h-full object-cover"
|
|
103
|
+
:style="containerStyle"
|
|
104
|
+
>
|
|
105
|
+
<div absolute bottom-1 right-1 p0.5 text-cyan:75 bg-cyan:10 rounded title="Snapshot">
|
|
106
|
+
<div class="i-carbon-camera" />
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
94
109
|
</template>
|
|
95
110
|
|
|
96
111
|
<style scoped lang="postcss">
|
package/internals/SlidesShow.vue
CHANGED
|
@@ -7,8 +7,7 @@ import { createFixedClicks } from '../composables/useClicks'
|
|
|
7
7
|
import { useNav } from '../composables/useNav'
|
|
8
8
|
import { useViewTransition } from '../composables/useViewTransition'
|
|
9
9
|
import { CLICKS_MAX } from '../constants'
|
|
10
|
-
import {
|
|
11
|
-
import { activeDragElement } from '../state'
|
|
10
|
+
import { activeDragElement, disableTransition, hmrSkipTransition } from '../state'
|
|
12
11
|
import DragControl from './DragControl.vue'
|
|
13
12
|
import SlideWrapper from './SlideWrapper.vue'
|
|
14
13
|
|
|
@@ -64,7 +63,7 @@ const loadedRoutes = computed(() => isPrintMode.value
|
|
|
64
63
|
function onAfterLeave() {
|
|
65
64
|
// After transition, we disable it so HMR won't trigger it again
|
|
66
65
|
// We will turn it back on `nav.go` so the normal navigation would still work
|
|
67
|
-
|
|
66
|
+
hmrSkipTransition.value = true
|
|
68
67
|
// recompute poppers after transition
|
|
69
68
|
recomputeAllPoppers()
|
|
70
69
|
}
|
|
@@ -76,8 +75,8 @@ function onAfterLeave() {
|
|
|
76
75
|
|
|
77
76
|
<!-- Slides -->
|
|
78
77
|
<component
|
|
79
|
-
:is="hasViewTransition && !isPrintMode ? 'div' : TransitionGroup"
|
|
80
|
-
v-bind="
|
|
78
|
+
:is="(hasViewTransition && !isPrintMode && !hmrSkipTransition && !disableTransition) ? 'div' : TransitionGroup"
|
|
79
|
+
v-bind="(hmrSkipTransition || disableTransition || isPrintMode) ? {} : currentTransition"
|
|
81
80
|
id="slideshow"
|
|
82
81
|
tag="div"
|
|
83
82
|
:class="{
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useNav } from '../composables/useNav'
|
|
4
|
+
import { syncDirections } from '../state'
|
|
5
|
+
import IconButton from './IconButton.vue'
|
|
6
|
+
import MenuButton from './MenuButton.vue'
|
|
7
|
+
import SelectList from './SelectList.vue'
|
|
8
|
+
|
|
9
|
+
const { isPresenter } = useNav()
|
|
10
|
+
|
|
11
|
+
const shouldReceive = computed({
|
|
12
|
+
get: () => isPresenter.value
|
|
13
|
+
? syncDirections.value.presenterReceive
|
|
14
|
+
: syncDirections.value.viewerReceive,
|
|
15
|
+
set(v) {
|
|
16
|
+
if (isPresenter.value) {
|
|
17
|
+
syncDirections.value.presenterReceive = v
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
syncDirections.value.viewerReceive = v
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const shouldSend = computed({
|
|
26
|
+
get: () => isPresenter.value
|
|
27
|
+
? syncDirections.value.presenterSend
|
|
28
|
+
: syncDirections.value.viewerSend,
|
|
29
|
+
set(v) {
|
|
30
|
+
if (isPresenter.value) {
|
|
31
|
+
syncDirections.value.presenterSend = v
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
syncDirections.value.viewerSend = v
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const state = computed({
|
|
40
|
+
get: () => {
|
|
41
|
+
if (shouldReceive.value && shouldSend.value) {
|
|
42
|
+
return 'bidirectional'
|
|
43
|
+
}
|
|
44
|
+
if (shouldReceive.value && !shouldSend.value) {
|
|
45
|
+
return 'receive-only'
|
|
46
|
+
}
|
|
47
|
+
if (!shouldReceive.value && shouldSend.value) {
|
|
48
|
+
return 'send-only'
|
|
49
|
+
}
|
|
50
|
+
return 'off'
|
|
51
|
+
},
|
|
52
|
+
set(v) {
|
|
53
|
+
switch (v) {
|
|
54
|
+
case 'bidirectional':
|
|
55
|
+
shouldReceive.value = true
|
|
56
|
+
shouldSend.value = true
|
|
57
|
+
break
|
|
58
|
+
case 'receive-only':
|
|
59
|
+
shouldReceive.value = true
|
|
60
|
+
shouldSend.value = false
|
|
61
|
+
break
|
|
62
|
+
case 'send-only':
|
|
63
|
+
shouldReceive.value = false
|
|
64
|
+
shouldSend.value = true
|
|
65
|
+
break
|
|
66
|
+
case 'off':
|
|
67
|
+
shouldReceive.value = false
|
|
68
|
+
shouldSend.value = false
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<MenuButton>
|
|
77
|
+
<template #button>
|
|
78
|
+
<IconButton title="Change sync settings">
|
|
79
|
+
<div class="i-ph:arrow-up-bold mx--1.2 scale-x-80" :class="shouldSend ? 'text-green6 dark:text-green' : 'op30'" />
|
|
80
|
+
<div class="i-ph:arrow-down-bold mx--1.2 scale-x-80" :class="shouldReceive ? 'text-green6 dark:text-green' : 'op30'" />
|
|
81
|
+
</IconButton>
|
|
82
|
+
</template>
|
|
83
|
+
<template #menu>
|
|
84
|
+
<div text-sm flex="~ col gap-2">
|
|
85
|
+
<div px3 ws-nowrap>
|
|
86
|
+
<span op75>Slides navigation syncing for </span>
|
|
87
|
+
<span font-bold text-primary>{{ isPresenter ? 'presenter' : 'viewer' }}</span>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
90
|
+
<SelectList
|
|
91
|
+
v-model="state"
|
|
92
|
+
title="Sync Mode"
|
|
93
|
+
:items="[
|
|
94
|
+
{ value: 'bidirectional', display: 'Bidirectional Sync' },
|
|
95
|
+
{ value: 'receive-only', display: 'Receive Only' },
|
|
96
|
+
{ value: 'send-only', display: 'Send Only' },
|
|
97
|
+
{ value: 'off', display: 'Disable' },
|
|
98
|
+
]"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
</MenuButton>
|
|
103
|
+
</template>
|
package/logic/color.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isDark } from './dark'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Predefined color map for matching the branding
|
|
5
|
+
*
|
|
6
|
+
* Accpet a 6-digit hex color string or a hue number
|
|
7
|
+
* Hue numbers are preferred because they will adapt better contrast in light/dark mode
|
|
8
|
+
*
|
|
9
|
+
* Hue numbers reference:
|
|
10
|
+
* - 0: red
|
|
11
|
+
* - 30: orange
|
|
12
|
+
* - 60: yellow
|
|
13
|
+
* - 120: green
|
|
14
|
+
* - 180: cyan
|
|
15
|
+
* - 240: blue
|
|
16
|
+
* - 270: purple
|
|
17
|
+
*/
|
|
18
|
+
const predefinedColorMap = {
|
|
19
|
+
error: 0,
|
|
20
|
+
client: 60,
|
|
21
|
+
Light: 60,
|
|
22
|
+
Dark: 240,
|
|
23
|
+
} as Record<string, number>
|
|
24
|
+
|
|
25
|
+
export function getHashColorFromString(
|
|
26
|
+
name: string,
|
|
27
|
+
opacity: number | string = 1,
|
|
28
|
+
) {
|
|
29
|
+
if (predefinedColorMap[name])
|
|
30
|
+
return getHsla(predefinedColorMap[name], opacity)
|
|
31
|
+
|
|
32
|
+
let hash = 0
|
|
33
|
+
for (let i = 0; i < name.length; i++)
|
|
34
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
|
35
|
+
const hue = hash % 360
|
|
36
|
+
return getHsla(hue, opacity)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getHsla(
|
|
40
|
+
hue: number,
|
|
41
|
+
opacity: number | string = 1,
|
|
42
|
+
) {
|
|
43
|
+
const saturation = hue === -1
|
|
44
|
+
? 0
|
|
45
|
+
: isDark.value ? 50 : 100
|
|
46
|
+
const lightness = isDark.value ? 60 : 20
|
|
47
|
+
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getPluginColor(name: string, opacity = 1): string {
|
|
51
|
+
if (predefinedColorMap[name]) {
|
|
52
|
+
const color = predefinedColorMap[name]
|
|
53
|
+
if (typeof color === 'number') {
|
|
54
|
+
return getHsla(color, opacity)
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (opacity === 1)
|
|
58
|
+
return color
|
|
59
|
+
const opacityHex = Math.floor(opacity * 255).toString(16).padStart(2, '0')
|
|
60
|
+
return color + opacityHex
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return getHashColorFromString(name, opacity)
|
|
64
|
+
}
|
package/logic/snapshot.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
import type { SlidevContextNavFull } from '../composables/useNav'
|
|
2
|
+
import type { ScreenshotSession } from './screenshot'
|
|
3
|
+
import { sleep } from '@antfu/utils'
|
|
4
|
+
import { slideHeight, slideWidth } from '../env'
|
|
5
|
+
import { captureDelay, disableTransition } from '../state'
|
|
1
6
|
import { snapshotState } from '../state/snapshot'
|
|
7
|
+
import { isDark } from './dark'
|
|
8
|
+
import { startScreenshotSession } from './screenshot'
|
|
2
9
|
import { getSlide } from './slides'
|
|
3
10
|
|
|
11
|
+
const chromeVersion = window.navigator.userAgent.match(/Chrome\/(\d+)/)?.[1]
|
|
12
|
+
export const isScreenshotSupported = chromeVersion ? Number(chromeVersion) >= 94 : false
|
|
13
|
+
|
|
14
|
+
const initialWait = 100
|
|
15
|
+
|
|
4
16
|
export class SlideSnapshotManager {
|
|
5
|
-
private
|
|
17
|
+
private _screenshotSession: ScreenshotSession | null = null
|
|
6
18
|
|
|
7
|
-
getSnapshot(slideNo: number) {
|
|
8
|
-
const
|
|
19
|
+
getSnapshot(slideNo: number, isDark: boolean) {
|
|
20
|
+
const id = slideNo + (isDark ? '-dark' : '-light')
|
|
21
|
+
const data = snapshotState.state[id]
|
|
9
22
|
if (!data) {
|
|
10
23
|
return
|
|
11
24
|
}
|
|
@@ -18,67 +31,75 @@ export class SlideSnapshotManager {
|
|
|
18
31
|
}
|
|
19
32
|
}
|
|
20
33
|
|
|
21
|
-
async
|
|
34
|
+
private async saveSnapshot(slideNo: number, dataUrl: string, isDark: boolean) {
|
|
22
35
|
if (!__DEV__)
|
|
23
|
-
return
|
|
24
|
-
if (this.getSnapshot(slideNo)) {
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
if (this._capturePromises.has(slideNo)) {
|
|
28
|
-
await this._capturePromises.get(slideNo)
|
|
29
|
-
}
|
|
30
|
-
const promise = this._captureSnapshot(slideNo, el, delay)
|
|
31
|
-
.finally(() => {
|
|
32
|
-
this._capturePromises.delete(slideNo)
|
|
33
|
-
})
|
|
34
|
-
this._capturePromises.set(slideNo, promise)
|
|
35
|
-
await promise
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private async _captureSnapshot(slideNo: number, el: HTMLElement, delay: number) {
|
|
39
|
-
if (!__DEV__)
|
|
40
|
-
return
|
|
36
|
+
return false
|
|
41
37
|
const slide = getSlide(slideNo)
|
|
42
38
|
if (!slide)
|
|
43
|
-
return
|
|
39
|
+
return false
|
|
44
40
|
|
|
41
|
+
const id = slideNo + (isDark ? '-dark' : '-light')
|
|
45
42
|
const revision = slide.meta.slide.revision
|
|
43
|
+
snapshotState.patch(id, {
|
|
44
|
+
revision,
|
|
45
|
+
image: dataUrl,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!el.querySelector('.slidev-slide-loading'))
|
|
51
|
-
break
|
|
52
|
-
await new Promise(r => setTimeout(r, 100))
|
|
53
|
-
}
|
|
49
|
+
async startCapturing(nav: SlidevContextNavFull) {
|
|
50
|
+
if (!__DEV__)
|
|
51
|
+
return false
|
|
54
52
|
|
|
55
|
-
//
|
|
56
|
-
|
|
53
|
+
// TODO: show a dialog to confirm
|
|
54
|
+
|
|
55
|
+
if (this._screenshotSession) {
|
|
56
|
+
this._screenshotSession.dispose()
|
|
57
|
+
this._screenshotSession = null
|
|
58
|
+
}
|
|
57
59
|
|
|
58
|
-
// Capture the snapshot
|
|
59
|
-
const toImage = await import('html-to-image')
|
|
60
60
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
this._screenshotSession = await startScreenshotSession(
|
|
62
|
+
slideWidth.value,
|
|
63
|
+
slideHeight.value,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
disableTransition.value = true
|
|
67
|
+
nav.go(1, 0, true)
|
|
68
|
+
|
|
69
|
+
await sleep(initialWait + captureDelay.value)
|
|
70
|
+
while (true) {
|
|
71
|
+
if (!this._screenshotSession) {
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
this.saveSnapshot(
|
|
75
|
+
nav.currentSlideNo.value,
|
|
76
|
+
this._screenshotSession.screenshot(document.getElementById('slide-content')!),
|
|
77
|
+
isDark.value,
|
|
78
|
+
)
|
|
79
|
+
if (nav.hasNext.value) {
|
|
80
|
+
await sleep(captureDelay.value)
|
|
81
|
+
nav.nextSlide(true)
|
|
82
|
+
await sleep(captureDelay.value)
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
break
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// eslint-disable-next-line no-console
|
|
78
|
-
console.info('[Slidev] Snapshot captured for slide', slideNo)
|
|
88
|
+
|
|
89
|
+
// TODO: show a message when done
|
|
90
|
+
|
|
91
|
+
return true
|
|
79
92
|
}
|
|
80
93
|
catch (e) {
|
|
81
|
-
console.error(
|
|
94
|
+
console.error(e)
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
disableTransition.value = false
|
|
99
|
+
if (this._screenshotSession) {
|
|
100
|
+
this._screenshotSession.dispose()
|
|
101
|
+
this._screenshotSession = null
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
}
|
|
84
105
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.51.0-beta.2",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,21 +32,20 @@
|
|
|
32
32
|
"@iconify-json/carbon": "^1.2.5",
|
|
33
33
|
"@iconify-json/ph": "^1.2.2",
|
|
34
34
|
"@iconify-json/svg-spinners": "^1.2.2",
|
|
35
|
-
"@shikijs/monaco": "^1.24.
|
|
36
|
-
"@shikijs/vitepress-twoslash": "^1.24.
|
|
35
|
+
"@shikijs/monaco": "^1.24.4",
|
|
36
|
+
"@shikijs/vitepress-twoslash": "^1.24.4",
|
|
37
37
|
"@slidev/rough-notation": "^0.1.0",
|
|
38
38
|
"@typescript/ata": "^0.9.7",
|
|
39
39
|
"@unhead/vue": "^1.11.14",
|
|
40
|
-
"@unocss/reset": "^0.65.
|
|
41
|
-
"@vueuse/core": "^12.
|
|
42
|
-
"@vueuse/math": "^12.
|
|
40
|
+
"@unocss/reset": "^0.65.3",
|
|
41
|
+
"@vueuse/core": "^12.2.0",
|
|
42
|
+
"@vueuse/math": "^12.2.0",
|
|
43
43
|
"@vueuse/motion": "^2.2.6",
|
|
44
44
|
"drauu": "^0.4.2",
|
|
45
45
|
"file-saver": "^2.0.5",
|
|
46
46
|
"floating-vue": "^5.2.2",
|
|
47
47
|
"fuse.js": "^7.0.0",
|
|
48
|
-
"
|
|
49
|
-
"katex": "^0.16.17",
|
|
48
|
+
"katex": "^0.16.18",
|
|
50
49
|
"lz-string": "^1.5.0",
|
|
51
50
|
"mermaid": "^11.4.1",
|
|
52
51
|
"monaco-editor": "0.51.0",
|
|
@@ -54,17 +53,17 @@
|
|
|
54
53
|
"pptxgenjs": "^3.12.0",
|
|
55
54
|
"prettier": "^3.4.2",
|
|
56
55
|
"recordrtc": "^5.6.2",
|
|
57
|
-
"shiki": "^1.24.
|
|
56
|
+
"shiki": "^1.24.4",
|
|
58
57
|
"shiki-magic-move": "^0.5.2",
|
|
59
58
|
"typescript": "5.6.3",
|
|
60
|
-
"unocss": "^0.65.
|
|
59
|
+
"unocss": "^0.65.3",
|
|
61
60
|
"vue": "^3.5.13",
|
|
62
61
|
"vue-router": "^4.5.0",
|
|
63
62
|
"yaml": "^2.6.1",
|
|
64
|
-
"@slidev/parser": "0.
|
|
65
|
-
"@slidev/types": "0.
|
|
63
|
+
"@slidev/parser": "0.51.0-beta.2",
|
|
64
|
+
"@slidev/types": "0.51.0-beta.2"
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
|
-
"vite": "^6.0.
|
|
67
|
+
"vite": "^6.0.6"
|
|
69
68
|
}
|
|
70
69
|
}
|