@slidev/client 0.51.0-beta.1 → 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.
@@ -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
- skipTransition.value = true
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, skipTransition.value ? {} : transitionProps, () => children)
110
+ ? h(TransitionGroup, hmrSkipTransition.value ? {} : transitionProps, () => children)
111
111
  : h(tag, children)
112
112
  }
113
113
  },
@@ -0,0 +1,35 @@
1
+ import type { Ref } from 'vue'
2
+ import { useEventListener } from '@vueuse/core'
3
+ import { computed } 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
+ useEventListener(
23
+ document.body,
24
+ ['pointermove', 'pointerdown'],
25
+ () => {
26
+ show()
27
+ if (!shouldHide.value)
28
+ return
29
+ if (timer)
30
+ clearTimeout(timer)
31
+ timer = setTimeout(hide, TIMEOUT)
32
+ },
33
+ { passive: true },
34
+ )
35
+ }
@@ -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
- skipTransition.value = false
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
- skipTransition.value = false
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(wakeLockEnabled, (enabled) => {
9
- if (enabled)
10
- request('screen')
11
- else
12
- release()
13
- }, { immediate: true })
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
@@ -83,5 +83,4 @@ export const HEADMATTER_FIELDS = [
83
83
  'mdc',
84
84
  'contextMenu',
85
85
  'wakeLock',
86
- 'overviewSnapshots',
87
86
  ]
@@ -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"
@@ -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-10 flex="~ gap-1 items-center">
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>
@@ -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 { configs, pathPrefix } from '../env'
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 { currentSlideNo, go: goSlide, slides } = useNav()
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="configs.overviewSnapshots"
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 v-if="showOverview" class="fixed top-4 right-4 z-modal text-gray-400 flex flex-col items-center gap-2">
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>
@@ -12,7 +12,7 @@ defineEmits<{
12
12
  </script>
13
13
 
14
14
  <template>
15
- <div flex="~ gap-1 items-center" rounded bg-gray:2 p1>
15
+ <div flex="~ gap-1 items-center" rounded bg-gray:4 p1 m--1>
16
16
  <Badge
17
17
  v-for="option in options"
18
18
  :key="option.value"
@@ -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 px4 pt2 pb1 select-none text-nowrap font-bold border-t border-main;
52
+ @apply text-sm op75 px3 py1 select-none text-nowrap font-bold;
57
53
  }
58
54
  </style>
@@ -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 SelectList from './SelectList.vue'
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-2" min-w-30>
37
- <SelectList
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
- v-model="slideScale"
40
- title="Scale"
41
- :items="scaleItems"
42
- />
43
- <SelectList
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
- v-model="wakeLockEnabled"
46
- title="Wake lock"
47
- :items="wakeLockItems"
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, onMounted, ref } from 'vue'
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 (!props.useSnapshot || props.no == null)
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 v-if="!snapshot" :id="isMain ? 'slide-container' : undefined" ref="container" class="slidev-slide-container" :style="containerStyle">
82
- <div :id="isMain ? 'slide-content' : undefined" ref="slideElement" class="slidev-slide-content" :style="contentStyle">
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 preview -->
88
- <img
89
- v-else
90
- :src="snapshot"
91
- class="w-full object-cover"
92
- :style="containerStyle"
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">
@@ -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 { skipTransition } from '../logic/hmr'
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
- skipTransition.value = true
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="skipTransition || isPrintMode ? {} : currentTransition"
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 px4 pt3 pb1 ws-nowrap>
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="shouldSend"
56
- title="Send Changes"
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: true, display: 'On' },
67
- { value: false, display: 'Off' },
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
@@ -18,6 +18,8 @@ import { isDark } from './dark'
18
18
  const predefinedColorMap = {
19
19
  error: 0,
20
20
  client: 60,
21
+ Light: 60,
22
+ Dark: 240,
21
23
  } as Record<string, number>
22
24
 
23
25
  export function getHashColorFromString(
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 _capturePromises = new Map<number, Promise<void>>()
17
+ private _screenshotSession: ScreenshotSession | null = null
6
18
 
7
- getSnapshot(slideNo: number) {
8
- const data = snapshotState.state[slideNo]
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 captureSnapshot(slideNo: number, el: HTMLElement, delay = 1000) {
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
- // Retry until the slide is loaded
48
- let retries = 100
49
- while (retries-- > 0) {
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
- // Artificial delay for the content to be loaded
56
- await new Promise(r => setTimeout(r, delay))
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
- const dataUrl = await toImage.toPng(el, {
62
- width: el.offsetWidth,
63
- height: el.offsetHeight,
64
- skipFonts: true,
65
- cacheBust: true,
66
- pixelRatio: 1.5,
67
- })
68
- if (revision !== slide.meta.slide.revision) {
69
- // eslint-disable-next-line no-console
70
- console.info('[Slidev] Slide', slideNo, 'changed, discarding the snapshot')
71
- return
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
- snapshotState.patch(slideNo, {
74
- revision,
75
- image: dataUrl,
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('[Slidev] Failed to capture snapshot for slide', slideNo, e)
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.1",
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",
@@ -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.1",
65
- "@slidev/types": "0.51.0-beta.1"
63
+ "@slidev/parser": "0.51.0-beta.2",
64
+ "@slidev/types": "0.51.0-beta.2"
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, useLocalStorage, useStyleTag, watchDebounced } from '@vueuse/core'
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 + delay.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(delay.value)
83
+ await sleep(captureDelay.value)
85
84
  next()
86
- await sleep(delay.value)
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="Dark mode">
231
- <FormCheckbox v-model="isDark" :disabled="isColorSchemaConfigured" />
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="delay" type="number" step="50" min="50">
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
- skipTransition.value = false
103
+ hmrSkipTransition.value = false
105
104
  router.replace({
106
105
  path: getSlidePath(state.page, isPresenter.value),
107
106
  query: {
package/state/snapshot.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import serverSnapshotState from 'server-reactive:snapshots?diff'
2
2
  import { createSyncState } from './syncState'
3
3
 
4
- export type SnapshotState = Record<number, {
4
+ export type SnapshotState = Record<string, {
5
5
  revision: string
6
6
  image: string
7
7
  }>
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
@@ -1,3 +0,0 @@
1
- import { ref } from 'vue'
2
-
3
- export const skipTransition = ref(false)