@slidev/client 0.51.0-beta.1 → 0.51.0-beta.3
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/useHideCursorIdle.ts +44 -0
- package/composables/useNav.ts +3 -3
- package/composables/useWakeLock.ts +10 -6
- package/constants.ts +0 -1
- package/internals/DevicesSelectors.vue +2 -0
- package/internals/FormItem.vue +6 -3
- package/internals/FormSlider.vue +68 -0
- package/internals/MenuButton.vue +1 -1
- package/internals/NavControls.vue +2 -1
- package/internals/QuickOverview.vue +24 -4
- package/internals/SegmentControl.vue +1 -1
- package/internals/SelectList.vue +1 -5
- package/internals/Settings.vue +101 -37
- package/internals/SlideContainer.vue +33 -18
- package/internals/SlidesShow.vue +4 -5
- package/internals/SyncControls.vue +43 -13
- package/logic/color.ts +2 -0
- package/logic/snapshot.ts +73 -52
- package/package.json +3 -4
- package/pages/export.vue +16 -10
- package/pages/play.vue +24 -2
- package/setup/root.ts +2 -3
- package/state/snapshot.ts +1 -1
- package/state/storage.ts +27 -0
- package/styles/index.css +1 -1
- package/logic/hmr.ts +0 -3
package/builtin/VSwitch.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { recomputeAllPoppers } from 'floating-vue'
|
|
|
3
3
|
import { defineComponent, h, onMounted, onUnmounted, ref, TransitionGroup, watchEffect } from 'vue'
|
|
4
4
|
import { CLASS_VCLICK_CURRENT, CLASS_VCLICK_DISPLAY_NONE, CLASS_VCLICK_PRIOR, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
|
|
5
5
|
import { useSlideContext } from '../context'
|
|
6
|
-
import { skipTransition } from '../logic/hmr'
|
|
7
6
|
import { resolveTransition } from '../logic/transition'
|
|
8
7
|
import { makeId } from '../logic/utils'
|
|
8
|
+
import { hmrSkipTransition } from '../state'
|
|
9
9
|
|
|
10
10
|
export default defineComponent({
|
|
11
11
|
props: {
|
|
@@ -77,7 +77,7 @@ export default defineComponent({
|
|
|
77
77
|
|
|
78
78
|
function onAfterLeave() {
|
|
79
79
|
// Refer to SlidesShow.vue
|
|
80
|
-
|
|
80
|
+
hmrSkipTransition.value = true
|
|
81
81
|
recomputeAllPoppers()
|
|
82
82
|
}
|
|
83
83
|
const transitionProps = transition && {
|
|
@@ -107,7 +107,7 @@ export default defineComponent({
|
|
|
107
107
|
}, slot?.()))
|
|
108
108
|
}
|
|
109
109
|
return transitionProps
|
|
110
|
-
? h(TransitionGroup,
|
|
110
|
+
? h(TransitionGroup, hmrSkipTransition.value ? {} : transitionProps, () => children)
|
|
111
111
|
: h(tag, children)
|
|
112
112
|
}
|
|
113
113
|
},
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { useEventListener } from '@vueuse/core'
|
|
3
|
+
import { computed, watch } from 'vue'
|
|
4
|
+
import { hideCursorIdle } from '../state'
|
|
5
|
+
|
|
6
|
+
const TIMEOUT = 2000
|
|
7
|
+
|
|
8
|
+
export function useHideCursorIdle(
|
|
9
|
+
enabled: Ref<boolean>,
|
|
10
|
+
) {
|
|
11
|
+
const shouldHide = computed(() => enabled.value && hideCursorIdle.value)
|
|
12
|
+
|
|
13
|
+
function hide() {
|
|
14
|
+
document.body.style.cursor = 'none'
|
|
15
|
+
}
|
|
16
|
+
function show() {
|
|
17
|
+
document.body.style.cursor = ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
21
|
+
|
|
22
|
+
// If disabled, immediately show the cursor
|
|
23
|
+
watch(
|
|
24
|
+
shouldHide,
|
|
25
|
+
(value) => {
|
|
26
|
+
if (!value)
|
|
27
|
+
show()
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
useEventListener(
|
|
32
|
+
document.body,
|
|
33
|
+
['pointermove', 'pointerdown'],
|
|
34
|
+
() => {
|
|
35
|
+
show()
|
|
36
|
+
if (timer)
|
|
37
|
+
clearTimeout(timer)
|
|
38
|
+
if (!shouldHide.value)
|
|
39
|
+
return
|
|
40
|
+
timer = setTimeout(hide, TIMEOUT)
|
|
41
|
+
},
|
|
42
|
+
{ passive: true },
|
|
43
|
+
)
|
|
44
|
+
}
|
package/composables/useNav.ts
CHANGED
|
@@ -9,10 +9,10 @@ import { computed, ref, watch } from 'vue'
|
|
|
9
9
|
import { useRoute, useRouter } from 'vue-router'
|
|
10
10
|
import { CLICKS_MAX } from '../constants'
|
|
11
11
|
import { configs } from '../env'
|
|
12
|
-
import { skipTransition } from '../logic/hmr'
|
|
13
12
|
import { useRouteQuery } from '../logic/route'
|
|
14
13
|
import { getSlide, getSlidePath } from '../logic/slides'
|
|
15
14
|
import { getCurrentTransition } from '../logic/transition'
|
|
15
|
+
import { hmrSkipTransition } from '../state'
|
|
16
16
|
import { createClicksContextBase } from './useClicks'
|
|
17
17
|
import { useTocTree } from './useTocTree'
|
|
18
18
|
|
|
@@ -184,7 +184,7 @@ export function useNavBase(
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
async function go(no: number | string, clicks: number = 0, force = false) {
|
|
187
|
-
|
|
187
|
+
hmrSkipTransition.value = false
|
|
188
188
|
const pageChanged = currentSlideNo.value !== no
|
|
189
189
|
const clicksChanged = clicks !== queryClicks.value
|
|
190
190
|
const meta = getSlide(no)?.meta
|
|
@@ -304,7 +304,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
|
|
|
304
304
|
return v
|
|
305
305
|
},
|
|
306
306
|
set(v) {
|
|
307
|
-
|
|
307
|
+
hmrSkipTransition.value = false
|
|
308
308
|
queryClicksRaw.value = v.toString()
|
|
309
309
|
},
|
|
310
310
|
})
|
|
@@ -5,10 +5,14 @@ import { wakeLockEnabled } from '../state'
|
|
|
5
5
|
export function useWakeLock() {
|
|
6
6
|
const { request, release } = useVueUseWakeLock()
|
|
7
7
|
|
|
8
|
-
watch(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
watch(
|
|
9
|
+
wakeLockEnabled,
|
|
10
|
+
(enabled) => {
|
|
11
|
+
if (enabled)
|
|
12
|
+
request('screen')
|
|
13
|
+
else
|
|
14
|
+
release()
|
|
15
|
+
},
|
|
16
|
+
{ immediate: true },
|
|
17
|
+
)
|
|
14
18
|
}
|
package/constants.ts
CHANGED
|
@@ -49,11 +49,13 @@ ensureDevicesListPermissions()
|
|
|
49
49
|
title="Camera"
|
|
50
50
|
:items="camerasItems"
|
|
51
51
|
/>
|
|
52
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
52
53
|
<SelectList
|
|
53
54
|
v-model="currentMic"
|
|
54
55
|
title="Microphone"
|
|
55
56
|
:items="microphonesItems"
|
|
56
57
|
/>
|
|
58
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
57
59
|
<SelectList
|
|
58
60
|
v-if="mimeTypeItems.length"
|
|
59
61
|
v-model="mimeType"
|
package/internals/FormItem.vue
CHANGED
|
@@ -6,6 +6,7 @@ defineProps<{
|
|
|
6
6
|
nested?: boolean | number
|
|
7
7
|
div?: boolean
|
|
8
8
|
description?: string
|
|
9
|
+
dot?: boolean
|
|
9
10
|
}>()
|
|
10
11
|
|
|
11
12
|
const emit = defineEmits<{
|
|
@@ -19,17 +20,19 @@ function reset() {
|
|
|
19
20
|
|
|
20
21
|
<template>
|
|
21
22
|
<component :is="div ? 'div' : 'label'" flex="~ row gap-2 items-center" select-none>
|
|
22
|
-
<div w-30 h-
|
|
23
|
+
<div w-30 h-8 flex="~ gap-1 items-center">
|
|
23
24
|
<div
|
|
24
25
|
v-if="nested" i-ri-corner-down-right-line op40
|
|
25
26
|
:style="typeof nested === 'number' ? { marginLeft: `${nested * 0.5 + 0.5}rem` } : { marginLeft: '0.25rem' }"
|
|
26
27
|
/>
|
|
27
|
-
<div v-if="!description" op75 @dblclick="reset">
|
|
28
|
+
<div v-if="!description" op75 relative @dblclick="reset">
|
|
28
29
|
{{ title }}
|
|
30
|
+
<div v-if="dot" w-1.5 h-1.5 bg-primary rounded absolute top-0 right--2 />
|
|
29
31
|
</div>
|
|
30
32
|
<Tooltip v-else distance="10">
|
|
31
|
-
<div op75 text-right @dblclick="reset">
|
|
33
|
+
<div op75 text-right relative @dblclick="reset">
|
|
32
34
|
{{ title }}
|
|
35
|
+
<div v-if="dot" w-1.5 h-1.5 bg-primary rounded absolute top-0 right--2 />
|
|
33
36
|
</div>
|
|
34
37
|
<template #popper>
|
|
35
38
|
<div text-sm min-w-90 v-html="description" />
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
max: number
|
|
4
|
+
min: number
|
|
5
|
+
step: number
|
|
6
|
+
unit?: string
|
|
7
|
+
default?: number
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const value = defineModel<number>('modelValue', {
|
|
11
|
+
type: Number,
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div relative h-22px w-60 flex-auto @dblclick="props.default !== undefined ? value = props.default : null">
|
|
17
|
+
<input
|
|
18
|
+
v-model.number="value" type="range" class="slider"
|
|
19
|
+
v-bind="props"
|
|
20
|
+
absolute bottom-0 left-0 right-0 top-0 z-10 w-full align-top
|
|
21
|
+
>
|
|
22
|
+
<span
|
|
23
|
+
v-if="props.default != null"
|
|
24
|
+
border="r main" absolute bottom-0 top-0 h-full w-1px op75
|
|
25
|
+
:style="{
|
|
26
|
+
left: `${(props.default - min) / (max - min) * 100}%`,
|
|
27
|
+
}"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div relative h-22px>
|
|
31
|
+
<input v-model.number="value" type="number" v-bind="props" border="~ base rounded" m0 w-20 bg-gray:5 pl2 align-top text-sm>
|
|
32
|
+
<span v-if="props.unit" pointer-events-none absolute right-1 top-0.5 text-xs op25>{{ props.unit }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.slider {
|
|
38
|
+
appearance: none;
|
|
39
|
+
height: 22px;
|
|
40
|
+
outline: none;
|
|
41
|
+
opacity: 0.7;
|
|
42
|
+
-webkit-transition: 0.2s;
|
|
43
|
+
transition: opacity 0.2s;
|
|
44
|
+
--uno: border border-main rounded of-hidden bg-gray/5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.slider:hover {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.slider::-webkit-slider-thumb {
|
|
52
|
+
-webkit-appearance: none;
|
|
53
|
+
appearance: none;
|
|
54
|
+
width: 5px;
|
|
55
|
+
height: 22px;
|
|
56
|
+
background: var(--slidev-theme-primary);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
z-index: 10;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.slider::-moz-range-thumb {
|
|
62
|
+
width: 5px;
|
|
63
|
+
height: 22px;
|
|
64
|
+
background: var(--slidev-theme-primary);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
z-index: 10;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
package/internals/MenuButton.vue
CHANGED
|
@@ -30,7 +30,7 @@ onClickOutside(el, () => {
|
|
|
30
30
|
<KeepAlive>
|
|
31
31
|
<div
|
|
32
32
|
v-if="value"
|
|
33
|
-
class="bg-main text-main shadow-xl absolute bottom-10 left-0 z-menu"
|
|
33
|
+
class="bg-main text-main shadow-xl absolute bottom-10 left-0 z-menu py2"
|
|
34
34
|
border="~ main rounded-md"
|
|
35
35
|
>
|
|
36
36
|
<slot name="menu" />
|
|
@@ -5,7 +5,7 @@ import { useDrawings } from '../composables/useDrawings'
|
|
|
5
5
|
import { useNav } from '../composables/useNav'
|
|
6
6
|
import { configs } from '../env'
|
|
7
7
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
8
|
-
import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
8
|
+
import { activeElement, breakpoints, fullscreen, hasViewerCssFilter, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
9
9
|
import { downloadPDF } from '../utils'
|
|
10
10
|
import IconButton from './IconButton.vue'
|
|
11
11
|
import MenuButton from './MenuButton.vue'
|
|
@@ -167,6 +167,7 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
167
167
|
<template #button>
|
|
168
168
|
<IconButton title="More Options">
|
|
169
169
|
<div class="i-carbon:settings-adjust" />
|
|
170
|
+
<div v-if="hasViewerCssFilter" w-2 h-2 bg-primary rounded-full absolute top-0.5 right-0.5 />
|
|
170
171
|
</IconButton>
|
|
171
172
|
</template>
|
|
172
173
|
<template #menu>
|
|
@@ -4,15 +4,18 @@ import { computed, ref, watchEffect } from 'vue'
|
|
|
4
4
|
import { createFixedClicks } from '../composables/useClicks'
|
|
5
5
|
import { useNav } from '../composables/useNav'
|
|
6
6
|
import { CLICKS_MAX } from '../constants'
|
|
7
|
-
import {
|
|
7
|
+
import { pathPrefix } from '../env'
|
|
8
8
|
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
|
|
9
|
+
import { isScreenshotSupported } from '../logic/screenshot'
|
|
10
|
+
import { snapshotManager } from '../logic/snapshot'
|
|
9
11
|
import { breakpoints, showOverview, windowSize } from '../state'
|
|
10
12
|
import DrawingPreview from './DrawingPreview.vue'
|
|
11
13
|
import IconButton from './IconButton.vue'
|
|
12
14
|
import SlideContainer from './SlideContainer.vue'
|
|
13
15
|
import SlideWrapper from './SlideWrapper.vue'
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const nav = useNav()
|
|
18
|
+
const { currentSlideNo, go: goSlide, slides } = nav
|
|
16
19
|
|
|
17
20
|
function close() {
|
|
18
21
|
showOverview.value = false
|
|
@@ -48,6 +51,12 @@ const rowCount = computed(() => {
|
|
|
48
51
|
|
|
49
52
|
const keyboardBuffer = ref<string>('')
|
|
50
53
|
|
|
54
|
+
async function captureSlidesOverview() {
|
|
55
|
+
showOverview.value = false
|
|
56
|
+
await snapshotManager.startCapturing(nav)
|
|
57
|
+
showOverview.value = true
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
useEventListener('keypress', (e) => {
|
|
52
61
|
if (!showOverview.value) {
|
|
53
62
|
keyboardBuffer.value = ''
|
|
@@ -129,7 +138,7 @@ watchEffect(() => {
|
|
|
129
138
|
<SlideContainer
|
|
130
139
|
:key="route.no"
|
|
131
140
|
:no="route.no"
|
|
132
|
-
:use-snapshot="
|
|
141
|
+
:use-snapshot="true"
|
|
133
142
|
:width="cardWidth"
|
|
134
143
|
class="pointer-events-none"
|
|
135
144
|
>
|
|
@@ -157,7 +166,10 @@ watchEffect(() => {
|
|
|
157
166
|
</div>
|
|
158
167
|
</div>
|
|
159
168
|
</Transition>
|
|
160
|
-
<div
|
|
169
|
+
<div
|
|
170
|
+
v-show="showOverview"
|
|
171
|
+
class="fixed top-4 right-4 z-modal text-gray-400 flex flex-col items-center gap-2"
|
|
172
|
+
>
|
|
161
173
|
<IconButton title="Close" class="text-2xl" @click="close">
|
|
162
174
|
<div class="i-carbon:close" />
|
|
163
175
|
</IconButton>
|
|
@@ -172,5 +184,13 @@ watchEffect(() => {
|
|
|
172
184
|
>
|
|
173
185
|
<div class="i-carbon:list-boxes" />
|
|
174
186
|
</IconButton>
|
|
187
|
+
<IconButton
|
|
188
|
+
v-if="__DEV__ && isScreenshotSupported"
|
|
189
|
+
title="Capture slides as images"
|
|
190
|
+
class="text-2xl"
|
|
191
|
+
@click="captureSlidesOverview"
|
|
192
|
+
>
|
|
193
|
+
<div class="i-carbon:drop-photo" />
|
|
194
|
+
</IconButton>
|
|
175
195
|
</div>
|
|
176
196
|
</template>
|
package/internals/SelectList.vue
CHANGED
|
@@ -46,13 +46,9 @@ const value = useVModel(props, 'modelValue', emit, { passive: true })
|
|
|
46
46
|
<style lang="postcss" scoped>
|
|
47
47
|
.item {
|
|
48
48
|
@apply flex rounded whitespace-nowrap py-1 gap-1 px-2 cursor-default hover:bg-gray-400 hover:bg-opacity-10;
|
|
49
|
-
|
|
50
|
-
svg {
|
|
51
|
-
@apply mr-1 -ml-2 my-auto;
|
|
52
|
-
}
|
|
53
49
|
}
|
|
54
50
|
|
|
55
51
|
.title {
|
|
56
|
-
@apply text-sm op75
|
|
52
|
+
@apply text-sm op75 px3 py1 select-none text-nowrap font-bold;
|
|
57
53
|
}
|
|
58
54
|
</style>
|
package/internals/Settings.vue
CHANGED
|
@@ -1,50 +1,114 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { SelectionItem } from './types'
|
|
3
2
|
import { useWakeLock } from '@vueuse/core'
|
|
4
3
|
import { useNav } from '../composables/useNav'
|
|
5
|
-
import { slideScale, wakeLockEnabled } from '../state'
|
|
6
|
-
import
|
|
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'
|
|
7
9
|
|
|
8
10
|
const { isPresenter } = useNav()
|
|
9
|
-
|
|
10
|
-
const scaleItems: SelectionItem<number>[] = [
|
|
11
|
-
{
|
|
12
|
-
display: 'Fit',
|
|
13
|
-
value: 0,
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
display: '1:1',
|
|
17
|
-
value: 1,
|
|
18
|
-
},
|
|
19
|
-
]
|
|
20
|
-
|
|
21
11
|
const { isSupported } = useWakeLock()
|
|
22
|
-
|
|
23
|
-
const wakeLockItems: SelectionItem<boolean>[] = [
|
|
24
|
-
{
|
|
25
|
-
display: 'Enabled',
|
|
26
|
-
value: true,
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
display: 'Disabled',
|
|
30
|
-
value: false,
|
|
31
|
-
},
|
|
32
|
-
]
|
|
33
12
|
</script>
|
|
34
13
|
|
|
35
14
|
<template>
|
|
36
|
-
<div text-sm select-none flex="~ col gap-
|
|
37
|
-
<
|
|
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
|
|
38
90
|
v-if="!isPresenter"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
44
102
|
v-if="__SLIDEV_FEATURE_WAKE_LOCK__ && isSupported"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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>
|
|
49
113
|
</div>
|
|
50
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="{
|
|
@@ -35,6 +35,41 @@ const shouldSend = computed({
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
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
|
+
})
|
|
38
73
|
</script>
|
|
39
74
|
|
|
40
75
|
<template>
|
|
@@ -47,24 +82,19 @@ const shouldSend = computed({
|
|
|
47
82
|
</template>
|
|
48
83
|
<template #menu>
|
|
49
84
|
<div text-sm flex="~ col gap-2">
|
|
50
|
-
<div
|
|
85
|
+
<div px3 ws-nowrap>
|
|
51
86
|
<span op75>Slides navigation syncing for </span>
|
|
52
87
|
<span font-bold text-primary>{{ isPresenter ? 'presenter' : 'viewer' }}</span>
|
|
53
88
|
</div>
|
|
89
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
54
90
|
<SelectList
|
|
55
|
-
v-model="
|
|
56
|
-
title="
|
|
57
|
-
:items="[
|
|
58
|
-
{ value: true, display: 'On' },
|
|
59
|
-
{ value: false, display: 'Off' },
|
|
60
|
-
]"
|
|
61
|
-
/>
|
|
62
|
-
<SelectList
|
|
63
|
-
v-model="shouldReceive"
|
|
64
|
-
title="Receive Changes"
|
|
91
|
+
v-model="state"
|
|
92
|
+
title="Sync Mode"
|
|
65
93
|
:items="[
|
|
66
|
-
{ value:
|
|
67
|
-
{ value:
|
|
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' },
|
|
68
98
|
]"
|
|
69
99
|
/>
|
|
70
100
|
</div>
|
package/logic/color.ts
CHANGED
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.51.0-beta.
|
|
4
|
+
"version": "0.51.0-beta.3",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
"file-saver": "^2.0.5",
|
|
46
46
|
"floating-vue": "^5.2.2",
|
|
47
47
|
"fuse.js": "^7.0.0",
|
|
48
|
-
"html-to-image": "^1.11.11",
|
|
49
48
|
"katex": "^0.16.18",
|
|
50
49
|
"lz-string": "^1.5.0",
|
|
51
50
|
"mermaid": "^11.4.1",
|
|
@@ -61,8 +60,8 @@
|
|
|
61
60
|
"vue": "^3.5.13",
|
|
62
61
|
"vue-router": "^4.5.0",
|
|
63
62
|
"yaml": "^2.6.1",
|
|
64
|
-
"@slidev/parser": "0.51.0-beta.
|
|
65
|
-
"@slidev/types": "0.51.0-beta.
|
|
63
|
+
"@slidev/parser": "0.51.0-beta.3",
|
|
64
|
+
"@slidev/types": "0.51.0-beta.3"
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"vite": "^6.0.6"
|
package/pages/export.vue
CHANGED
|
@@ -3,8 +3,7 @@ import type { ScreenshotSession } from '../logic/screenshot'
|
|
|
3
3
|
import { sleep } from '@antfu/utils'
|
|
4
4
|
import { parseRangeString } from '@slidev/parser/utils'
|
|
5
5
|
import { useHead } from '@unhead/vue'
|
|
6
|
-
import { provideLocal, useElementSize,
|
|
7
|
-
|
|
6
|
+
import { provideLocal, useElementSize, useStyleTag, watchDebounced } from '@vueuse/core'
|
|
8
7
|
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
9
8
|
import { useRouter } from 'vue-router'
|
|
10
9
|
import { useDarkMode } from '../composables/useDarkMode'
|
|
@@ -16,8 +15,9 @@ import ExportPdfTip from '../internals/ExportPdfTip.vue'
|
|
|
16
15
|
import FormCheckbox from '../internals/FormCheckbox.vue'
|
|
17
16
|
import FormItem from '../internals/FormItem.vue'
|
|
18
17
|
import PrintSlide from '../internals/PrintSlide.vue'
|
|
18
|
+
import SegmentControl from '../internals/SegmentControl.vue'
|
|
19
19
|
import { isScreenshotSupported, startScreenshotSession } from '../logic/screenshot'
|
|
20
|
-
import { skipExportPdfTip } from '../state'
|
|
20
|
+
import { captureDelay, skipExportPdfTip } from '../state'
|
|
21
21
|
import Play from './play.vue'
|
|
22
22
|
|
|
23
23
|
const { slides, isPrintWithClicks, hasNext, go, next, currentSlideNo, clicks, printRange } = useNav()
|
|
@@ -29,7 +29,6 @@ const scale = computed(() => containerWidth.value / slideWidth.value)
|
|
|
29
29
|
const contentMarginBottom = computed(() => `${contentHeight.value * (scale.value - 1)}px`)
|
|
30
30
|
const rangesRaw = ref('')
|
|
31
31
|
const initialWait = ref(1000)
|
|
32
|
-
const delay = useLocalStorage('slidev-export-capture-delay', 400, { listenToStorageChanges: false })
|
|
33
32
|
type ScreenshotResult = { slideIndex: number, clickIndex: number, dataUrl: string }[]
|
|
34
33
|
const screenshotSession = ref<ScreenshotSession | null>(null)
|
|
35
34
|
const capturedImages = ref<ScreenshotResult | null>(null)
|
|
@@ -70,7 +69,7 @@ async function capturePngs() {
|
|
|
70
69
|
|
|
71
70
|
go(1, 0, true)
|
|
72
71
|
|
|
73
|
-
await sleep(initialWait.value +
|
|
72
|
+
await sleep(initialWait.value + captureDelay.value)
|
|
74
73
|
while (true) {
|
|
75
74
|
if (!screenshotSession.value) {
|
|
76
75
|
break
|
|
@@ -81,9 +80,9 @@ async function capturePngs() {
|
|
|
81
80
|
dataUrl: screenshotSession.value.screenshot(document.getElementById('slide-content')!),
|
|
82
81
|
})
|
|
83
82
|
if (hasNext.value) {
|
|
84
|
-
await sleep(
|
|
83
|
+
await sleep(captureDelay.value)
|
|
85
84
|
next()
|
|
86
|
-
await sleep(
|
|
85
|
+
await sleep(captureDelay.value)
|
|
87
86
|
}
|
|
88
87
|
else {
|
|
89
88
|
break
|
|
@@ -227,8 +226,15 @@ if (import.meta.hot) {
|
|
|
227
226
|
<FormItem title="Range">
|
|
228
227
|
<input v-model="rangesRaw" type="text" :placeholder="`1-${slides.length}`">
|
|
229
228
|
</FormItem>
|
|
230
|
-
<FormItem title="
|
|
231
|
-
<
|
|
229
|
+
<FormItem title="Color Mode">
|
|
230
|
+
<SegmentControl
|
|
231
|
+
v-model="isDark"
|
|
232
|
+
:options="[
|
|
233
|
+
{ value: false, label: 'Light' },
|
|
234
|
+
{ value: true, label: 'Dark' },
|
|
235
|
+
]"
|
|
236
|
+
:disabled="isColorSchemaConfigured"
|
|
237
|
+
/>
|
|
232
238
|
</FormItem>
|
|
233
239
|
<FormItem title="With clicks">
|
|
234
240
|
<FormCheckbox v-model="isPrintWithClicks" />
|
|
@@ -273,7 +279,7 @@ if (import.meta.hot) {
|
|
|
273
279
|
Pre-capture Slides as Images
|
|
274
280
|
</button>
|
|
275
281
|
<FormItem title="Delay" description="Delay between capturing each slide in milliseconds.<br>Increase this value if slides are captured incompletely. <br>(Not related to PDF export)">
|
|
276
|
-
<input v-model="
|
|
282
|
+
<input v-model="captureDelay" type="number" step="50" min="50">
|
|
277
283
|
</FormItem>
|
|
278
284
|
</div>
|
|
279
285
|
</div>
|
package/pages/play.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useStyleTag } from '@vueuse/core'
|
|
3
3
|
import { computed, ref, shallowRef } from 'vue'
|
|
4
4
|
import { useDrawings } from '../composables/useDrawings'
|
|
5
|
+
import { useHideCursorIdle } from '../composables/useHideCursorIdle'
|
|
5
6
|
import { useNav } from '../composables/useNav'
|
|
6
7
|
import { useSwipeControls } from '../composables/useSwipeControls'
|
|
7
8
|
import { useWakeLock } from '../composables/useWakeLock'
|
|
@@ -12,9 +13,9 @@ import SlideContainer from '../internals/SlideContainer.vue'
|
|
|
12
13
|
import SlidesShow from '../internals/SlidesShow.vue'
|
|
13
14
|
import { onContextMenu } from '../logic/contextMenu'
|
|
14
15
|
import { registerShortcuts } from '../logic/shortcuts'
|
|
15
|
-
import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor } from '../state'
|
|
16
|
+
import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor, viewerCssFilter, viewerCssFilterDefaults } from '../state'
|
|
16
17
|
|
|
17
|
-
const { next, prev, isPrintMode } = useNav()
|
|
18
|
+
const { next, prev, isPrintMode, isPresenter } = useNav()
|
|
18
19
|
const { isDrawing } = useDrawings()
|
|
19
20
|
|
|
20
21
|
const root = ref<HTMLDivElement>()
|
|
@@ -35,6 +36,7 @@ useSwipeControls(root)
|
|
|
35
36
|
registerShortcuts()
|
|
36
37
|
if (__SLIDEV_FEATURE_WAKE_LOCK__)
|
|
37
38
|
useWakeLock()
|
|
39
|
+
useHideCursorIdle(computed(() => !isPresenter.value && !isPrintMode.value))
|
|
38
40
|
|
|
39
41
|
if (import.meta.hot) {
|
|
40
42
|
useStyleTag(computed(() => showEditor.value
|
|
@@ -59,6 +61,25 @@ const persistNav = computed(() => isScreenVertical.value || showEditor.value)
|
|
|
59
61
|
const SideEditor = shallowRef<any>()
|
|
60
62
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
61
63
|
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
64
|
+
|
|
65
|
+
const contentStyle = computed(() => {
|
|
66
|
+
let filter = ''
|
|
67
|
+
|
|
68
|
+
if (viewerCssFilter.value.brightness !== viewerCssFilterDefaults.brightness)
|
|
69
|
+
filter += `brightness(${viewerCssFilter.value.brightness}) `
|
|
70
|
+
if (viewerCssFilter.value.contrast !== viewerCssFilterDefaults.contrast)
|
|
71
|
+
filter += `contrast(${viewerCssFilter.value.contrast}) `
|
|
72
|
+
if (viewerCssFilter.value.sepia !== viewerCssFilterDefaults.sepia)
|
|
73
|
+
filter += `sepia(${viewerCssFilter.value.sepia}) `
|
|
74
|
+
if (viewerCssFilter.value.hueRotate !== viewerCssFilterDefaults.hueRotate)
|
|
75
|
+
filter += `hue-rotate(${viewerCssFilter.value.hueRotate}deg) `
|
|
76
|
+
if (viewerCssFilter.value.invert)
|
|
77
|
+
filter += 'invert(1) '
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
filter,
|
|
81
|
+
}
|
|
82
|
+
})
|
|
62
83
|
</script>
|
|
63
84
|
|
|
64
85
|
<template>
|
|
@@ -69,6 +90,7 @@ if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
|
69
90
|
<SlideContainer
|
|
70
91
|
:style="{ background: 'var(--slidev-slide-container-background, black)' }"
|
|
71
92
|
is-main
|
|
93
|
+
:content-style="contentStyle"
|
|
72
94
|
@pointerdown="onClick"
|
|
73
95
|
@contextmenu="onContextMenu"
|
|
74
96
|
>
|
package/setup/root.ts
CHANGED
|
@@ -8,10 +8,9 @@ import { useNav } from '../composables/useNav'
|
|
|
8
8
|
import { usePrintStyles } from '../composables/usePrintStyles'
|
|
9
9
|
import { injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionSlidevContext, TRUST_ORIGINS } from '../constants'
|
|
10
10
|
import { configs, slidesTitle } from '../env'
|
|
11
|
-
import { skipTransition } from '../logic/hmr'
|
|
12
11
|
import { getSlidePath } from '../logic/slides'
|
|
13
12
|
import { makeId } from '../logic/utils'
|
|
14
|
-
import { syncDirections } from '../state'
|
|
13
|
+
import { hmrSkipTransition, syncDirections } from '../state'
|
|
15
14
|
import { initDrawingState } from '../state/drawings'
|
|
16
15
|
import { initSharedState, onPatch, patch } from '../state/shared'
|
|
17
16
|
|
|
@@ -101,7 +100,7 @@ export default function setupRoot() {
|
|
|
101
100
|
if ((+state.page === +currentSlideNo.value && +clicksContext.value.current === +state.clicks))
|
|
102
101
|
return
|
|
103
102
|
// if (state.lastUpdate?.type === 'presenter') {
|
|
104
|
-
|
|
103
|
+
hmrSkipTransition.value = false
|
|
105
104
|
router.replace({
|
|
106
105
|
path: getSlidePath(state.page, isPresenter.value),
|
|
107
106
|
query: {
|
package/state/snapshot.ts
CHANGED
package/state/storage.ts
CHANGED
|
@@ -8,6 +8,13 @@ export const showInfoDialog = ref(false)
|
|
|
8
8
|
export const showGotoDialog = ref(false)
|
|
9
9
|
export const showOverview = ref(false)
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Skip slides transition when triggered by HMR.
|
|
13
|
+
* Will reset automatically after user navigations
|
|
14
|
+
*/
|
|
15
|
+
export const hmrSkipTransition = ref(false)
|
|
16
|
+
export const disableTransition = ref(false)
|
|
17
|
+
|
|
11
18
|
export const shortcutsEnabled = ref(true)
|
|
12
19
|
export const breakpoints = useBreakpoints({
|
|
13
20
|
xs: 460,
|
|
@@ -26,7 +33,9 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
|
|
|
26
33
|
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
27
34
|
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
28
35
|
export const wakeLockEnabled = useLocalStorage('slidev-wake-lock', true)
|
|
36
|
+
export const hideCursorIdle = useLocalStorage('slidev-hide-cursor-idle', true)
|
|
29
37
|
export const skipExportPdfTip = useLocalStorage('slidev-skip-export-pdf-tip', false)
|
|
38
|
+
export const captureDelay = useLocalStorage('slidev-export-capture-delay', 400, { listenToStorageChanges: false })
|
|
30
39
|
|
|
31
40
|
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
32
41
|
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
@@ -39,6 +48,24 @@ export const activeDragElement = shallowRef<DragElementState | null>(null)
|
|
|
39
48
|
export const presenterNotesFontSize = useLocalStorage('slidev-presenter-font-size', 1, { listenToStorageChanges: false })
|
|
40
49
|
export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { listenToStorageChanges: false })
|
|
41
50
|
|
|
51
|
+
export const viewerCssFilterDefaults = {
|
|
52
|
+
invert: false,
|
|
53
|
+
contrast: 1,
|
|
54
|
+
brightness: 1,
|
|
55
|
+
hueRotate: 0,
|
|
56
|
+
saturate: 1,
|
|
57
|
+
sepia: 0,
|
|
58
|
+
}
|
|
59
|
+
export const viewerCssFilter = useLocalStorage(
|
|
60
|
+
'slidev-viewer-css-filter',
|
|
61
|
+
viewerCssFilterDefaults,
|
|
62
|
+
{ listenToStorageChanges: false, mergeDefaults: true, deep: true },
|
|
63
|
+
)
|
|
64
|
+
export const hasViewerCssFilter = computed(() => {
|
|
65
|
+
return (Object.keys(viewerCssFilterDefaults) as (keyof typeof viewerCssFilterDefaults)[])
|
|
66
|
+
.some(k => viewerCssFilter.value[k] !== viewerCssFilterDefaults[k])
|
|
67
|
+
})
|
|
68
|
+
|
|
42
69
|
export function togglePresenterLayout() {
|
|
43
70
|
presenterLayout.value = presenterLayout.value + 1
|
|
44
71
|
if (presenterLayout.value > 3)
|
package/styles/index.css
CHANGED
|
@@ -22,7 +22,7 @@ html {
|
|
|
22
22
|
user-select: none;
|
|
23
23
|
outline: none;
|
|
24
24
|
cursor: pointer;
|
|
25
|
-
@apply inline-flex items-center justify-center opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
25
|
+
@apply inline-flex items-center justify-center opacity-75 transition duration-200 ease-in-out align-middle rounded p-1 relative;
|
|
26
26
|
@apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
|
|
27
27
|
@apply focus-visible:(opacity-100 outline outline-2 outline-offset-2 outline-black dark:outline-white);
|
|
28
28
|
@apply md:p-2;
|
package/logic/hmr.ts
DELETED