@slidev/client 0.48.0-beta.12 → 0.48.0-beta.13
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/ShikiMagicMove.vue +48 -0
- package/composables/useClicks.ts +18 -11
- package/context.ts +5 -2
- package/internals/DrawingLayer.vue +2 -3
- package/internals/NoteDisplay.vue +73 -18
- package/internals/NoteEditor.vue +5 -3
- package/internals/NoteStatic.vue +3 -2
- package/internals/OverviewClicksSlider.vue +4 -5
- package/internals/PrintSlide.vue +8 -2
- package/internals/SlidesOverview.vue +1 -1
- package/package.json +5 -4
- package/pages/notes.vue +1 -0
- package/pages/overview.vue +23 -9
- package/pages/play.vue +4 -4
- package/pages/presenter.vue +9 -9
- package/state/index.ts +1 -1
- package/styles/index.css +20 -4
- /package/internals/{Editor.vue → SideEditor.vue} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
|
|
3
|
+
import type { KeyedTokensInfo } from 'shiki-magic-move/types'
|
|
4
|
+
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
|
5
|
+
import { useSlideContext } from '../context'
|
|
6
|
+
import { makeId } from '../logic/utils'
|
|
7
|
+
|
|
8
|
+
import 'shiki-magic-move/style.css'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
steps: KeyedTokensInfo[]
|
|
12
|
+
at?: string | number
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const { $clicksContext: clicks, $scale: scale } = useSlideContext()
|
|
16
|
+
const id = makeId()
|
|
17
|
+
const index = ref(0)
|
|
18
|
+
|
|
19
|
+
onUnmounted(() => {
|
|
20
|
+
clicks!.unregister(id)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
if (!clicks || clicks.disabled)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
const { start, end, delta } = clicks.resolve(props.at || '+1', props.steps.length - 1)
|
|
28
|
+
clicks.register(id, { max: end, delta })
|
|
29
|
+
|
|
30
|
+
watchEffect(() => {
|
|
31
|
+
if (clicks.disabled)
|
|
32
|
+
index.value = props.steps.length - 1
|
|
33
|
+
else
|
|
34
|
+
index.value = Math.min(Math.max(0, clicks.current - start + 1), props.steps.length - 1)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="slidev-code-wrapper slidev-code-magic-move">
|
|
41
|
+
<ShikiMagicMovePrecompiled
|
|
42
|
+
class="slidev-code relative shiki"
|
|
43
|
+
:steps="steps"
|
|
44
|
+
:step="index"
|
|
45
|
+
:options="{ globalScale: scale }"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
package/composables/useClicks.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { sum } from '@antfu/utils'
|
|
2
2
|
import type { ClicksContext } from '@slidev/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
|
-
import { ref, shallowReactive } from 'vue'
|
|
4
|
+
import { computed, ref, shallowReactive } from 'vue'
|
|
5
5
|
import type { RouteRecordRaw } from 'vue-router'
|
|
6
6
|
import { currentRoute, isPrintMode, isPrintWithClicks, queryClicks, routeForceRefresh } from '../logic/nav'
|
|
7
7
|
import { normalizeAtProp } from '../logic/utils'
|
|
8
8
|
import { CLICKS_MAX } from '../constants'
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
* @internal
|
|
12
|
-
*/
|
|
13
|
-
export function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
|
|
10
|
+
function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): ClicksContext {
|
|
14
11
|
const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
|
|
15
12
|
const map: ClicksContext['map'] = shallowReactive(new Map())
|
|
16
13
|
|
|
@@ -19,7 +16,10 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
|
|
|
19
16
|
return isPrintMode.value && !isPrintWithClicks.value
|
|
20
17
|
},
|
|
21
18
|
get current() {
|
|
22
|
-
return
|
|
19
|
+
return current.value
|
|
20
|
+
},
|
|
21
|
+
set current(value) {
|
|
22
|
+
current.value = value
|
|
23
23
|
},
|
|
24
24
|
relativeOffsets,
|
|
25
25
|
map,
|
|
@@ -67,8 +67,8 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
67
67
|
if (route?.meta?.__clicksContext)
|
|
68
68
|
return route.meta.__clicksContext
|
|
69
69
|
const thisPath = +(route?.path ?? CLICKS_MAX)
|
|
70
|
-
const
|
|
71
|
-
()
|
|
70
|
+
const current = computed({
|
|
71
|
+
get() {
|
|
72
72
|
const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
|
|
73
73
|
if (currentPath === thisPath)
|
|
74
74
|
return queryClicks.value
|
|
@@ -77,6 +77,14 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
77
77
|
else
|
|
78
78
|
return 0
|
|
79
79
|
},
|
|
80
|
+
set(v) {
|
|
81
|
+
const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
|
|
82
|
+
if (currentPath === thisPath)
|
|
83
|
+
queryClicks.value = v
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
const context = useClicksContextBase(
|
|
87
|
+
current,
|
|
80
88
|
route?.meta?.clicks,
|
|
81
89
|
)
|
|
82
90
|
if (route?.meta)
|
|
@@ -84,7 +92,6 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
84
92
|
return context
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0):
|
|
88
|
-
|
|
89
|
-
return [current, useClicksContextBase(() => current.value, route?.meta?.clicks)]
|
|
95
|
+
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): ClicksContext {
|
|
96
|
+
return useClicksContextBase(ref(currentInit), route?.meta?.clicks)
|
|
90
97
|
}
|
package/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallowRef, toRef } from 'vue'
|
|
1
|
+
import { ref, shallowRef, toRef } from 'vue'
|
|
2
2
|
import { injectLocal, objectOmit, provideLocal } from '@vueuse/core'
|
|
3
3
|
import { useFixedClicks } from './composables/useClicks'
|
|
4
4
|
import {
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
injectionFrontmatter,
|
|
10
10
|
injectionRenderContext,
|
|
11
11
|
injectionRoute,
|
|
12
|
+
injectionSlideScale,
|
|
12
13
|
injectionSlidevContext,
|
|
13
14
|
} from './constants'
|
|
14
15
|
|
|
15
|
-
const clicksContextFallback = shallowRef(useFixedClicks()
|
|
16
|
+
const clicksContextFallback = shallowRef(useFixedClicks())
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Get the current slide context, should be called inside the setup function of a component inside slide
|
|
@@ -26,6 +27,7 @@ export function useSlideContext() {
|
|
|
26
27
|
const $renderContext = injectLocal(injectionRenderContext)!
|
|
27
28
|
const $frontmatter = injectLocal(injectionFrontmatter, {})
|
|
28
29
|
const $route = injectLocal(injectionRoute, undefined)
|
|
30
|
+
const $scale = injectLocal(injectionSlideScale, ref(1))!
|
|
29
31
|
|
|
30
32
|
return {
|
|
31
33
|
$slidev,
|
|
@@ -36,6 +38,7 @@ export function useSlideContext() {
|
|
|
36
38
|
$route,
|
|
37
39
|
$renderContext,
|
|
38
40
|
$frontmatter,
|
|
41
|
+
$scale,
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { injectLocal } from '@vueuse/core'
|
|
4
3
|
import { drauu, drawingEnabled, loadCanvas } from '../logic/drawings'
|
|
5
|
-
import {
|
|
4
|
+
import { useSlideContext } from '../context'
|
|
6
5
|
|
|
7
|
-
const scale =
|
|
6
|
+
const scale = useSlideContext().$scale
|
|
8
7
|
const svg = ref<SVGSVGElement>()
|
|
9
8
|
|
|
10
9
|
onMounted(() => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
4
|
import { CLICKS_MAX } from '../constants'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
@@ -7,45 +8,99 @@ const props = defineProps<{
|
|
|
7
8
|
noteHtml?: string
|
|
8
9
|
note?: string
|
|
9
10
|
placeholder?: string
|
|
10
|
-
|
|
11
|
+
clicksContext?: ClicksContext
|
|
11
12
|
}>()
|
|
12
13
|
|
|
13
14
|
defineEmits(['click'])
|
|
14
15
|
|
|
15
|
-
const withClicks = computed(() => props.
|
|
16
|
+
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
|
|
16
17
|
const noteDisplay = ref<HTMLElement | null>(null)
|
|
17
18
|
|
|
19
|
+
const CLASS_FADE = 'slidev-note-fade'
|
|
20
|
+
const CLASS_MARKER = 'slidev-note-click-mark'
|
|
21
|
+
|
|
18
22
|
function highlightNote() {
|
|
19
|
-
if (!noteDisplay.value || !withClicks.value || props.
|
|
23
|
+
if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
|
|
20
24
|
return
|
|
21
25
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const disabled = +props.clicks < 0 || +props.clicks >= CLICKS_MAX
|
|
26
|
+
const current = +props.clicksContext?.current ?? CLICKS_MAX
|
|
27
|
+
const disabled = current < 0 || current >= CLICKS_MAX
|
|
25
28
|
if (disabled) {
|
|
26
|
-
|
|
29
|
+
Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
30
|
+
.forEach(el => el.classList.remove(CLASS_FADE))
|
|
27
31
|
return
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
const nodeToIgnores = new Set<Element>()
|
|
35
|
+
function ignoreParent(node: Element) {
|
|
36
|
+
if (!node || node === noteDisplay.value)
|
|
37
|
+
return
|
|
38
|
+
nodeToIgnores.add(node)
|
|
39
|
+
if (node.parentElement)
|
|
40
|
+
ignoreParent(node.parentElement)
|
|
41
|
+
}
|
|
31
42
|
|
|
32
|
-
const
|
|
43
|
+
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
44
|
+
const markersMap = new Map<number, HTMLElement>()
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
// Convert all sibling text nodes to spans, so we attach classes to them
|
|
47
|
+
for (const marker of markers) {
|
|
48
|
+
const parent = marker.parentElement!
|
|
49
|
+
const clicks = Number(marker.dataset!.clicks)
|
|
50
|
+
markersMap.set(clicks, marker)
|
|
51
|
+
// Ignore the parents of the marker, so the class only applies to the children
|
|
52
|
+
ignoreParent(parent)
|
|
53
|
+
Array.from(parent!.childNodes)
|
|
54
|
+
.forEach((node) => {
|
|
55
|
+
if (node.nodeType === 3) { // text node
|
|
56
|
+
const span = document.createElement('span')
|
|
57
|
+
span.textContent = node.textContent
|
|
58
|
+
parent.insertBefore(span, node)
|
|
59
|
+
node.remove()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const children = Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
37
64
|
|
|
38
|
-
|
|
39
|
-
|
|
65
|
+
let count = 0
|
|
66
|
+
|
|
67
|
+
// Segmenting notes by clicks
|
|
68
|
+
const segments = new Map<number, Element[]>()
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (!segments.has(count))
|
|
71
|
+
segments.set(count, [])
|
|
72
|
+
segments.get(count)!.push(child)
|
|
73
|
+
// Update count when reach marker
|
|
74
|
+
if (child.classList.contains(CLASS_MARKER))
|
|
40
75
|
count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
|
|
41
76
|
}
|
|
42
77
|
|
|
43
|
-
|
|
44
|
-
|
|
78
|
+
// Apply
|
|
79
|
+
for (const [count, els] of segments) {
|
|
80
|
+
els.forEach(el => el.classList.toggle(
|
|
81
|
+
CLASS_FADE,
|
|
82
|
+
nodeToIgnores.has(el)
|
|
83
|
+
? false
|
|
84
|
+
: count !== current,
|
|
85
|
+
))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const [clicks, marker] of markersMap) {
|
|
89
|
+
marker.classList.remove(CLASS_FADE)
|
|
90
|
+
marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
|
|
91
|
+
marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
|
|
92
|
+
marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
|
|
93
|
+
marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
|
|
94
|
+
marker.addEventListener('dblclick', (e) => {
|
|
95
|
+
props.clicksContext!.current = clicks
|
|
96
|
+
e.stopPropagation()
|
|
97
|
+
e.stopImmediatePropagation()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
45
100
|
}
|
|
46
101
|
|
|
47
102
|
watch(
|
|
48
|
-
() => [props.noteHtml, props.
|
|
103
|
+
() => [props.noteHtml, props.clicksContext?.current],
|
|
49
104
|
() => {
|
|
50
105
|
nextTick(() => {
|
|
51
106
|
highlightNote()
|
package/internals/NoteEditor.vue
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
2
3
|
import { nextTick, ref, watch, watchEffect } from 'vue'
|
|
3
4
|
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
|
|
5
|
+
import type { ClicksContext } from '@slidev/types'
|
|
4
6
|
import { useDynamicSlideInfo } from '../logic/note'
|
|
5
7
|
import NoteDisplay from './NoteDisplay.vue'
|
|
6
8
|
|
|
@@ -20,8 +22,8 @@ const props = defineProps({
|
|
|
20
22
|
placeholder: {
|
|
21
23
|
default: 'No notes for this slide',
|
|
22
24
|
},
|
|
23
|
-
|
|
24
|
-
type:
|
|
25
|
+
clicksContext: {
|
|
26
|
+
type: Object as PropType<ClicksContext>,
|
|
25
27
|
},
|
|
26
28
|
autoHeight: {
|
|
27
29
|
default: false,
|
|
@@ -103,7 +105,7 @@ watch(
|
|
|
103
105
|
:style="props.style"
|
|
104
106
|
:note="note || placeholder"
|
|
105
107
|
:note-html="info?.noteHTML"
|
|
106
|
-
:clicks="
|
|
108
|
+
:clicks-context="clicksContext"
|
|
107
109
|
/>
|
|
108
110
|
<textarea
|
|
109
111
|
v-else
|
package/internals/NoteStatic.vue
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from 'packages/types'
|
|
2
3
|
import { useSlideInfo } from '../logic/note'
|
|
3
4
|
import NoteDisplay from './NoteDisplay.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
no?: number
|
|
7
8
|
class?: string
|
|
8
|
-
|
|
9
|
+
clicksContext?: ClicksContext
|
|
9
10
|
}>()
|
|
10
11
|
|
|
11
12
|
const { info } = useSlideInfo(props.no)
|
|
@@ -16,6 +17,6 @@ const { info } = useSlideInfo(props.no)
|
|
|
16
17
|
:class="props.class"
|
|
17
18
|
:note="info?.note"
|
|
18
19
|
:note-html="info?.noteHTML"
|
|
19
|
-
:clicks="
|
|
20
|
+
:clicks-context="clicksContext"
|
|
20
21
|
/>
|
|
21
22
|
</template>
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ClicksContext } from '@slidev/types'
|
|
3
|
-
import type { Ref } from 'vue'
|
|
4
3
|
import { computed } from 'vue'
|
|
5
4
|
import { CLICKS_MAX } from '../constants'
|
|
6
5
|
|
|
7
6
|
const props = defineProps<{
|
|
8
|
-
|
|
7
|
+
clicksContext: ClicksContext
|
|
9
8
|
}>()
|
|
10
9
|
|
|
11
|
-
const total = computed(() => props.
|
|
10
|
+
const total = computed(() => props.clicksContext.total)
|
|
12
11
|
const current = computed({
|
|
13
12
|
get() {
|
|
14
|
-
return props.
|
|
13
|
+
return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
|
|
15
14
|
},
|
|
16
15
|
set(value: number) {
|
|
17
16
|
// eslint-disable-next-line vue/no-mutating-props
|
|
18
|
-
props.
|
|
17
|
+
props.clicksContext.current = value
|
|
19
18
|
},
|
|
20
19
|
})
|
|
21
20
|
|
package/internals/PrintSlide.vue
CHANGED
|
@@ -9,7 +9,7 @@ const props = defineProps<{ route: RouteRecordRaw }>()
|
|
|
9
9
|
|
|
10
10
|
const route = computed(() => props.route)
|
|
11
11
|
const nav = useNav(route)
|
|
12
|
-
const clicks0 = useFixedClicks(route.value, 0)
|
|
12
|
+
const clicks0 = useFixedClicks(route.value, 0)
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
@@ -19,6 +19,12 @@ const clicks0 = useFixedClicks(route.value, 0)[1]
|
|
|
19
19
|
:route="route"
|
|
20
20
|
/>
|
|
21
21
|
<template v-if="!clicks0.disabled">
|
|
22
|
-
<PrintSlideClick
|
|
22
|
+
<PrintSlideClick
|
|
23
|
+
v-for="i of clicks0.total"
|
|
24
|
+
:key="i"
|
|
25
|
+
:clicks-context="useFixedClicks(route, i)"
|
|
26
|
+
:nav="nav"
|
|
27
|
+
:route="route"
|
|
28
|
+
/>
|
|
23
29
|
</template>
|
|
24
30
|
</template>
|
|
@@ -140,7 +140,7 @@ watchEffect(() => {
|
|
|
140
140
|
<SlideWrapper
|
|
141
141
|
:is="route.component"
|
|
142
142
|
v-if="route?.component"
|
|
143
|
-
:clicks-context="useFixedClicks(route, CLICKS_MAX)
|
|
143
|
+
:clicks-context="useFixedClicks(route, CLICKS_MAX)"
|
|
144
144
|
:class="getSlideClass(route)"
|
|
145
145
|
:route="route"
|
|
146
146
|
render-context="overview"
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.48.0-beta.
|
|
4
|
+
"version": "0.48.0-beta.13",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"prettier": "^3.2.5",
|
|
52
52
|
"recordrtc": "^5.6.2",
|
|
53
53
|
"resolve": "^1.22.8",
|
|
54
|
+
"shiki-magic-move": "^0.1.0",
|
|
54
55
|
"unocss": "^0.58.5",
|
|
55
|
-
"vue": "^3.4.
|
|
56
|
+
"vue": "^3.4.20",
|
|
56
57
|
"vue-router": "^4.3.0",
|
|
57
|
-
"@slidev/
|
|
58
|
-
"@slidev/
|
|
58
|
+
"@slidev/types": "0.48.0-beta.13",
|
|
59
|
+
"@slidev/parser": "0.48.0-beta.13"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"vite": "^5.1.4"
|
package/pages/notes.vue
CHANGED
|
@@ -51,6 +51,7 @@ function decreaseFontSize() {
|
|
|
51
51
|
:note="currentRoute?.meta?.slide?.note"
|
|
52
52
|
:note-html="currentRoute?.meta?.slide?.noteHTML"
|
|
53
53
|
:placeholder="`No notes for Slide ${pageNo}.`"
|
|
54
|
+
:clicks-context="currentRoute?.meta?.__clicksContext"
|
|
54
55
|
/>
|
|
55
56
|
</div>
|
|
56
57
|
<div class="flex-none border-t border-main">
|
package/pages/overview.vue
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Ref } from 'vue'
|
|
3
2
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
4
3
|
import { useHead } from '@unhead/vue'
|
|
5
4
|
import type { RouteRecordRaw } from 'vue-router'
|
|
6
5
|
import type { ClicksContext } from 'packages/types'
|
|
7
6
|
import { themeVars } from '../env'
|
|
8
|
-
import { rawRoutes } from '../logic/nav'
|
|
7
|
+
import { openInEditor, rawRoutes } from '../logic/nav'
|
|
9
8
|
import { useFixedClicks } from '../composables/useClicks'
|
|
10
9
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
11
10
|
import { getSlideClass } from '../utils'
|
|
@@ -30,8 +29,8 @@ const wordCounts = computed(() => rawRoutes.map(route => wordCount(route.meta?.s
|
|
|
30
29
|
const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
|
|
31
30
|
const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
|
|
32
31
|
|
|
33
|
-
const clicksContextMap = new WeakMap<RouteRecordRaw,
|
|
34
|
-
function
|
|
32
|
+
const clicksContextMap = new WeakMap<RouteRecordRaw, ClicksContext>()
|
|
33
|
+
function getClicksContext(route: RouteRecordRaw) {
|
|
35
34
|
// We create a local clicks context to calculate the total clicks of the slide
|
|
36
35
|
if (!clicksContextMap.has(route))
|
|
37
36
|
clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
|
|
@@ -39,7 +38,7 @@ function getClickContext(route: RouteRecordRaw) {
|
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
function getSlideClicks(route: RouteRecordRaw) {
|
|
42
|
-
return route.meta?.clicks ||
|
|
41
|
+
return route.meta?.clicks || getClicksContext(route)?.total
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
function wordCount(str: string) {
|
|
@@ -132,10 +131,25 @@ onMounted(() => {
|
|
|
132
131
|
:ref="el => blocks.set(idx, el as any)"
|
|
133
132
|
class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
|
|
134
133
|
>
|
|
135
|
-
<div class="select-none w-13 text-right my4">
|
|
134
|
+
<div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
|
|
136
135
|
<div class="text-3xl op20 mb2">
|
|
137
136
|
{{ idx + 1 }}
|
|
138
137
|
</div>
|
|
138
|
+
<IconButton
|
|
139
|
+
class="mr--3 op0 group-hover:op80"
|
|
140
|
+
title="Play in new tab"
|
|
141
|
+
@click="openSlideInNewTab(route.path)"
|
|
142
|
+
>
|
|
143
|
+
<carbon:presentation-file />
|
|
144
|
+
</IconButton>
|
|
145
|
+
<IconButton
|
|
146
|
+
v-if="route.meta?.slide"
|
|
147
|
+
class="mr--3 op0 group-hover:op80"
|
|
148
|
+
title="Open in editor"
|
|
149
|
+
@click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
|
|
150
|
+
>
|
|
151
|
+
<carbon:cics-program />
|
|
152
|
+
</IconButton>
|
|
139
153
|
</div>
|
|
140
154
|
<div class="flex flex-col gap-2 my5">
|
|
141
155
|
<div
|
|
@@ -152,7 +166,7 @@ onMounted(() => {
|
|
|
152
166
|
<SlideWrapper
|
|
153
167
|
:is="route.component"
|
|
154
168
|
v-if="route?.component"
|
|
155
|
-
:clicks-context="
|
|
169
|
+
:clicks-context="getClicksContext(route)"
|
|
156
170
|
:class="getSlideClass(route)"
|
|
157
171
|
:route="route"
|
|
158
172
|
render-context="overview"
|
|
@@ -163,7 +177,7 @@ onMounted(() => {
|
|
|
163
177
|
<OverviewClicksSlider
|
|
164
178
|
v-if="getSlideClicks(route)"
|
|
165
179
|
mt-2
|
|
166
|
-
:
|
|
180
|
+
:clicks-context="getClicksContext(route)"
|
|
167
181
|
class="w-full"
|
|
168
182
|
/>
|
|
169
183
|
</div>
|
|
@@ -182,7 +196,7 @@ onMounted(() => {
|
|
|
182
196
|
class="max-w-250 w-250 text-lg rounded p3"
|
|
183
197
|
:auto-height="true"
|
|
184
198
|
:editing="edittingNote === idx"
|
|
185
|
-
:clicks="
|
|
199
|
+
:clicks-context="getClicksContext(route)"
|
|
186
200
|
@dblclick="edittingNote !== idx ? edittingNote = idx : null"
|
|
187
201
|
@update:editing="edittingNote = null"
|
|
188
202
|
/>
|
package/pages/play.vue
CHANGED
|
@@ -31,9 +31,9 @@ useSwipeControls(root)
|
|
|
31
31
|
|
|
32
32
|
const persistNav = computed(() => isScreenVertical.value || showEditor.value)
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const SideEditor = shallowRef<any>()
|
|
35
35
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
36
|
-
import('../internals/
|
|
36
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
37
37
|
|
|
38
38
|
const DrawingControls = shallowRef<any>()
|
|
39
39
|
if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
@@ -70,8 +70,8 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
70
70
|
</template>
|
|
71
71
|
</SlideContainer>
|
|
72
72
|
|
|
73
|
-
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
74
|
-
<
|
|
73
|
+
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor">
|
|
74
|
+
<SideEditor :resize="true" />
|
|
75
75
|
</template>
|
|
76
76
|
</div>
|
|
77
77
|
<Controls />
|
package/pages/presenter.vue
CHANGED
|
@@ -49,12 +49,12 @@ const nextFrameClicksCtx = computed(() => {
|
|
|
49
49
|
return nextFrame.value && clicksCtxMap[+nextFrame.value[0].path - 1]
|
|
50
50
|
})
|
|
51
51
|
watch([currentRoute, queryClicks], () => {
|
|
52
|
-
nextFrameClicksCtx.value && (nextFrameClicksCtx.value
|
|
52
|
+
nextFrameClicksCtx.value && (nextFrameClicksCtx.value.current = nextFrame.value![1])
|
|
53
53
|
}, { immediate: true })
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const SideEditor = shallowRef<any>()
|
|
56
56
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
57
|
-
import('../internals/
|
|
57
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
58
58
|
|
|
59
59
|
// sync presenter cursor
|
|
60
60
|
onMounted(() => {
|
|
@@ -121,9 +121,9 @@ onMounted(() => {
|
|
|
121
121
|
class="h-full w-full"
|
|
122
122
|
>
|
|
123
123
|
<SlideWrapper
|
|
124
|
-
:is="nextFrame[0].component as any"
|
|
124
|
+
:is="(nextFrame[0].component as any)"
|
|
125
125
|
:key="nextFrame[0].path"
|
|
126
|
-
:clicks-context="nextFrameClicksCtx
|
|
126
|
+
:clicks-context="nextFrameClicksCtx"
|
|
127
127
|
:class="getSlideClass(nextFrame[0])"
|
|
128
128
|
:route="nextFrame[0]"
|
|
129
129
|
render-context="previewNext"
|
|
@@ -134,8 +134,8 @@ onMounted(() => {
|
|
|
134
134
|
</div>
|
|
135
135
|
</div>
|
|
136
136
|
<!-- Notes -->
|
|
137
|
-
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
138
|
-
<
|
|
137
|
+
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor" class="grid-section note of-auto">
|
|
138
|
+
<SideEditor />
|
|
139
139
|
</div>
|
|
140
140
|
<div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
|
|
141
141
|
<NoteEditor
|
|
@@ -144,7 +144,7 @@ onMounted(() => {
|
|
|
144
144
|
:no="currentSlideId"
|
|
145
145
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
146
146
|
:editing="notesEditing"
|
|
147
|
-
:clicks="clicksContext
|
|
147
|
+
:clicks-context="clicksContext"
|
|
148
148
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
149
149
|
/>
|
|
150
150
|
<NoteStatic
|
|
@@ -153,7 +153,7 @@ onMounted(() => {
|
|
|
153
153
|
:no="currentSlideId"
|
|
154
154
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
155
155
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
156
|
-
:clicks="clicksContext
|
|
156
|
+
:clicks-context="clicksContext"
|
|
157
157
|
/>
|
|
158
158
|
<div class="border-t border-main py-1 px-2 text-sm">
|
|
159
159
|
<IconButton title="Increase font size" @click="increasePresenterFontSize">
|
package/state/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { slideAspect } from '../env'
|
|
|
5
5
|
export const showRecordingDialog = ref(false)
|
|
6
6
|
export const showInfoDialog = ref(false)
|
|
7
7
|
export const showGotoDialog = ref(false)
|
|
8
|
+
export const showOverview = ref(false)
|
|
8
9
|
|
|
9
10
|
export const shortcutsEnabled = ref(true)
|
|
10
11
|
export const breakpoints = useBreakpoints({
|
|
@@ -24,7 +25,6 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
|
|
|
24
25
|
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
25
26
|
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
26
27
|
|
|
27
|
-
export const showOverview = useLocalStorage('slidev-show-overview', false, { listenToStorageChanges: false })
|
|
28
28
|
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
29
29
|
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
30
30
|
export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false, { listenToStorageChanges: false })
|
package/styles/index.css
CHANGED
|
@@ -16,7 +16,11 @@ html {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.slidev-icon-btn {
|
|
19
|
-
|
|
19
|
+
aspect-ratio: 1;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
user-select: none;
|
|
22
|
+
outline: none;
|
|
23
|
+
cursor: pointer;
|
|
20
24
|
@apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
21
25
|
@apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
|
|
22
26
|
@apply md:p-2;
|
|
@@ -70,9 +74,21 @@ html {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
.slidev-note-click-mark {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
user-select: none;
|
|
78
|
+
font-size: 0.7em;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
--uno: text-violet bg-violet/10 px1 font-mono rounded items-center border
|
|
81
|
+
border-transparent;
|
|
82
|
+
}
|
|
83
|
+
.slidev-note-click-mark.slidev-note-click-mark-active {
|
|
84
|
+
--uno: border border-violet;
|
|
85
|
+
}
|
|
86
|
+
.slidev-note-click-mark.slidev-note-click-mark-past {
|
|
87
|
+
filter: saturate(0);
|
|
88
|
+
opacity: 0.5;
|
|
89
|
+
}
|
|
90
|
+
.slidev-note-click-mark.slidev-note-click-mark-future {
|
|
91
|
+
opacity: 0.5;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
.slidev-note-click-mark::before {
|
|
File without changes
|