@slidev/client 0.50.0 → 0.51.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composables/useClicks.ts +1 -1
- package/composables/useTimer.ts +1 -1
- package/internals/Badge.vue +48 -0
- package/internals/{DevicesList.vue → DevicesSelectors.vue} +12 -4
- package/internals/MenuButton.vue +2 -2
- package/internals/NavControls.vue +14 -8
- package/internals/RecordingControls.vue +2 -2
- package/internals/RecordingDialog.vue +4 -14
- package/internals/ScreenCaptureMirror.vue +45 -0
- package/internals/SegmentControl.vue +29 -0
- package/internals/SelectList.vue +1 -5
- package/internals/Settings.vue +13 -3
- package/internals/SyncControls.vue +73 -0
- package/logic/color.ts +62 -0
- package/package.json +12 -12
- package/pages/export.vue +8 -12
- package/pages/notes.vue +3 -3
- package/pages/presenter.vue +37 -18
- package/setup/root.ts +31 -23
- package/state/drawings.ts +5 -1
- package/state/index.ts +1 -56
- package/state/shared.ts +0 -7
- package/state/storage.ts +70 -0
- package/styles/index.css +10 -0
- package/uno.config.ts +1 -0
package/composables/useClicks.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function createFixedClicks(
|
|
|
165
165
|
): ClicksContext {
|
|
166
166
|
const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
|
|
167
167
|
return createClicksContextBase(
|
|
168
|
-
|
|
168
|
+
ref(Math.max(toValue(currentInit), clicksStart)),
|
|
169
169
|
clicksStart,
|
|
170
170
|
route?.meta?.clicks,
|
|
171
171
|
)
|
package/composables/useTimer.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
getHashColorFromString,
|
|
5
|
+
getHsla,
|
|
6
|
+
} from '../logic/color'
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
text?: string
|
|
11
|
+
color?: boolean | number
|
|
12
|
+
as?: string
|
|
13
|
+
size?: string
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
color: true,
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const style = computed(() => {
|
|
21
|
+
if (!props.text || props.color === false)
|
|
22
|
+
return {}
|
|
23
|
+
return {
|
|
24
|
+
color: typeof props.color === 'number'
|
|
25
|
+
? getHsla(props.color)
|
|
26
|
+
: getHashColorFromString(props.text),
|
|
27
|
+
background: typeof props.color === 'number'
|
|
28
|
+
? getHsla(props.color, 0.1)
|
|
29
|
+
: getHashColorFromString(props.text, 0.1),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const sizeClasses = computed(() => {
|
|
34
|
+
switch (props.size || 'sm') {
|
|
35
|
+
case 'sm':
|
|
36
|
+
return 'px-1.5 text-11px leading-1.6em'
|
|
37
|
+
}
|
|
38
|
+
return ''
|
|
39
|
+
})
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<component :is="as || 'span'" ws-nowrap rounded :class="sizeClasses" :style>
|
|
44
|
+
<slot>
|
|
45
|
+
<span v-text="props.text" />
|
|
46
|
+
</slot>
|
|
47
|
+
</component>
|
|
48
|
+
</template>
|
|
@@ -43,13 +43,21 @@ ensureDevicesListPermissions()
|
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<template>
|
|
46
|
-
<div
|
|
47
|
-
<SelectList
|
|
48
|
-
|
|
46
|
+
<div text-sm flex="~ col gap-2">
|
|
47
|
+
<SelectList
|
|
48
|
+
v-model="currentCamera"
|
|
49
|
+
title="Camera"
|
|
50
|
+
:items="camerasItems"
|
|
51
|
+
/>
|
|
52
|
+
<SelectList
|
|
53
|
+
v-model="currentMic"
|
|
54
|
+
title="Microphone"
|
|
55
|
+
:items="microphonesItems"
|
|
56
|
+
/>
|
|
49
57
|
<SelectList
|
|
50
58
|
v-if="mimeTypeItems.length"
|
|
51
59
|
v-model="mimeType"
|
|
52
|
-
title="
|
|
60
|
+
title="Video Format"
|
|
53
61
|
:items="mimeTypeItems"
|
|
54
62
|
/>
|
|
55
63
|
</div>
|
package/internals/MenuButton.vue
CHANGED
|
@@ -30,8 +30,8 @@ onClickOutside(el, () => {
|
|
|
30
30
|
<KeepAlive>
|
|
31
31
|
<div
|
|
32
32
|
v-if="value"
|
|
33
|
-
class="
|
|
34
|
-
|
|
33
|
+
class="bg-main text-main shadow-xl absolute bottom-10 left-0 z-menu"
|
|
34
|
+
border="~ main rounded-md"
|
|
35
35
|
>
|
|
36
36
|
<slot name="menu" />
|
|
37
37
|
</div>
|
|
@@ -10,6 +10,7 @@ import { downloadPDF } from '../utils'
|
|
|
10
10
|
import IconButton from './IconButton.vue'
|
|
11
11
|
import MenuButton from './MenuButton.vue'
|
|
12
12
|
import Settings from './Settings.vue'
|
|
13
|
+
import SyncControls from './SyncControls.vue'
|
|
13
14
|
|
|
14
15
|
import VerticalDivider from './VerticalDivider.vue'
|
|
15
16
|
|
|
@@ -48,7 +49,7 @@ function onMouseLeave() {
|
|
|
48
49
|
|
|
49
50
|
const barStyle = computed(() => props.persist
|
|
50
51
|
? 'text-$slidev-controls-foreground bg-transparent'
|
|
51
|
-
: 'rounded-md bg-main shadow-xl
|
|
52
|
+
: 'rounded-md bg-main shadow-xl border border-main')
|
|
52
53
|
|
|
53
54
|
const RecordingControls = shallowRef<any>()
|
|
54
55
|
if (__SLIDEV_FEATURE_RECORD__)
|
|
@@ -130,19 +131,15 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
130
131
|
>
|
|
131
132
|
<div class="i-carbon:text-annotation-toggle" />
|
|
132
133
|
</IconButton>
|
|
133
|
-
|
|
134
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout">
|
|
135
|
-
<div class="i-carbon:template" />
|
|
136
|
-
{{ presenterLayout }}
|
|
137
|
-
</IconButton>
|
|
138
134
|
</template>
|
|
135
|
+
|
|
139
136
|
<template v-if="!__DEV__">
|
|
140
137
|
<IconButton v-if="configs.download" title="Download as PDF" @click="downloadPDF">
|
|
141
138
|
<div class="i-carbon:download" />
|
|
142
139
|
</IconButton>
|
|
143
140
|
</template>
|
|
144
141
|
|
|
145
|
-
<template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__">
|
|
142
|
+
<template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__ && !isEmbedded && !isPresenter">
|
|
146
143
|
<IconButton title="Browser Exporter" to="/export">
|
|
147
144
|
<div class="i-carbon:document-pdf" />
|
|
148
145
|
</IconButton>
|
|
@@ -156,7 +153,16 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
156
153
|
<div class="i-carbon:information" />
|
|
157
154
|
</IconButton>
|
|
158
155
|
|
|
159
|
-
<template v-if="!
|
|
156
|
+
<template v-if="!isEmbedded">
|
|
157
|
+
<VerticalDivider />
|
|
158
|
+
|
|
159
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout">
|
|
160
|
+
<div class="i-carbon:template" />
|
|
161
|
+
{{ presenterLayout }}
|
|
162
|
+
</IconButton>
|
|
163
|
+
|
|
164
|
+
<SyncControls v-if="__SLIDEV_FEATURE_PRESENTER__" />
|
|
165
|
+
|
|
160
166
|
<MenuButton>
|
|
161
167
|
<template #button>
|
|
162
168
|
<IconButton title="More Options">
|
|
@@ -3,7 +3,7 @@ import { useLocalStorage } from '@vueuse/core'
|
|
|
3
3
|
import { onMounted, watch } from 'vue'
|
|
4
4
|
import { recorder } from '../logic/recording'
|
|
5
5
|
import { currentCamera, showRecordingDialog } from '../state'
|
|
6
|
-
import
|
|
6
|
+
import DevicesSelectors from './DevicesSelectors.vue'
|
|
7
7
|
import IconButton from './IconButton.vue'
|
|
8
8
|
import MenuButton from './MenuButton.vue'
|
|
9
9
|
|
|
@@ -59,7 +59,7 @@ onMounted(() => {
|
|
|
59
59
|
</IconButton>
|
|
60
60
|
</template>
|
|
61
61
|
<template #menu>
|
|
62
|
-
<
|
|
62
|
+
<DevicesSelectors />
|
|
63
63
|
</template>
|
|
64
64
|
</MenuButton>
|
|
65
65
|
</template>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
3
|
import { nextTick } from 'vue'
|
|
4
4
|
import { getFilename, mimeType, recordCamera, recorder, recordingName } from '../logic/recording'
|
|
5
|
-
import
|
|
5
|
+
import DevicesSelectors from './DevicesSelectors.vue'
|
|
6
6
|
import Modal from './Modal.vue'
|
|
7
7
|
|
|
8
8
|
const props = defineProps({
|
|
@@ -72,14 +72,14 @@ async function start() {
|
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
|
-
<
|
|
75
|
+
<DevicesSelectors />
|
|
76
76
|
</div>
|
|
77
77
|
<div class="flex my-1">
|
|
78
|
-
<button class="
|
|
78
|
+
<button class="slidev-form-button" @click="close">
|
|
79
79
|
Cancel
|
|
80
80
|
</button>
|
|
81
81
|
<div class="flex-auto" />
|
|
82
|
-
<button @click="start">
|
|
82
|
+
<button class="slidev-form-button primary" @click="start">
|
|
83
83
|
Start
|
|
84
84
|
</button>
|
|
85
85
|
</div>
|
|
@@ -111,15 +111,5 @@ async function start() {
|
|
|
111
111
|
input[type='text'] {
|
|
112
112
|
@apply border border-main rounded px-2 py-1;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
button {
|
|
116
|
-
@apply bg-orange-400 text-white px-4 py-1 rounded border-b-2 border-orange-600;
|
|
117
|
-
@apply hover:(bg-orange-500 border-orange-700);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
button.cancel {
|
|
121
|
-
@apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
|
|
122
|
-
@apply hover:(bg-opacity-75 border-opacity-75);
|
|
123
|
-
}
|
|
124
114
|
}
|
|
125
115
|
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { shallowRef, useTemplateRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
const video = useTemplateRef('video')
|
|
5
|
+
const stream = shallowRef<MediaStream | null>(null)
|
|
6
|
+
const started = shallowRef(false)
|
|
7
|
+
|
|
8
|
+
async function startCapture() {
|
|
9
|
+
stream.value = await navigator.mediaDevices.getDisplayMedia({
|
|
10
|
+
video: {
|
|
11
|
+
// @ts-expect-error missing types
|
|
12
|
+
cursor: 'always',
|
|
13
|
+
},
|
|
14
|
+
audio: false,
|
|
15
|
+
selfBrowserSurface: 'include',
|
|
16
|
+
preferCurrentTab: false,
|
|
17
|
+
})
|
|
18
|
+
video.value!.srcObject = stream.value
|
|
19
|
+
video.value!.play()
|
|
20
|
+
started.value = true
|
|
21
|
+
stream.value.addEventListener('inactive', () => {
|
|
22
|
+
video.value!.srcObject = null
|
|
23
|
+
started.value = false
|
|
24
|
+
})
|
|
25
|
+
stream.value.addEventListener('ended', () => {
|
|
26
|
+
video.value!.srcObject = null
|
|
27
|
+
started.value = false
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div h-full w-full>
|
|
34
|
+
<video v-show="started" ref="video" class="w-full h-full object-contain" />
|
|
35
|
+
<div v-if="!started" w-full h-full flex="~ col gap-4 items-center justify-center">
|
|
36
|
+
<div op50>
|
|
37
|
+
Use screen capturing to mirror your main screen back to presenter view.<br>
|
|
38
|
+
Click the button below and <b>select your other monitor or window</b>.
|
|
39
|
+
</div>
|
|
40
|
+
<button class="slidev-form-button" @click="startCapture">
|
|
41
|
+
Start Screen Mirroring
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Badge from './Badge.vue'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
options: { label: string, value: any }[]
|
|
6
|
+
modelValue: any
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
defineEmits<{
|
|
10
|
+
(event: 'update:modelValue', newValue: any): void
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div flex="~ gap-1 items-center" rounded bg-gray:2 p1>
|
|
16
|
+
<Badge
|
|
17
|
+
v-for="option in options"
|
|
18
|
+
:key="option.value"
|
|
19
|
+
class="px-2 py-1 text-xs font-mono"
|
|
20
|
+
:class="option.value === modelValue ? '' : 'op50'"
|
|
21
|
+
:color="option.value === modelValue"
|
|
22
|
+
:aria-pressed="option.value === modelValue"
|
|
23
|
+
size="none"
|
|
24
|
+
:text="option.label"
|
|
25
|
+
as="button"
|
|
26
|
+
@click="$emit('update:modelValue', option.value)"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
package/internals/SelectList.vue
CHANGED
|
@@ -44,10 +44,6 @@ const value = useVModel(props, 'modelValue', emit, { passive: true })
|
|
|
44
44
|
</template>
|
|
45
45
|
|
|
46
46
|
<style lang="postcss" scoped>
|
|
47
|
-
.select-list {
|
|
48
|
-
@apply my-2;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
.item {
|
|
52
48
|
@apply flex rounded whitespace-nowrap py-1 gap-1 px-2 cursor-default hover:bg-gray-400 hover:bg-opacity-10;
|
|
53
49
|
|
|
@@ -57,6 +53,6 @@ const value = useVModel(props, 'modelValue', emit, { passive: true })
|
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
.title {
|
|
60
|
-
@apply text-
|
|
56
|
+
@apply text-sm op75 px4 pt2 pb1 select-none text-nowrap font-bold border-t border-main;
|
|
61
57
|
}
|
|
62
58
|
</style>
|
package/internals/Settings.vue
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { SelectionItem } from './types'
|
|
3
3
|
import { useWakeLock } from '@vueuse/core'
|
|
4
|
+
import { useNav } from '../composables/useNav'
|
|
4
5
|
import { slideScale, wakeLockEnabled } from '../state'
|
|
5
6
|
import SelectList from './SelectList.vue'
|
|
6
7
|
|
|
8
|
+
const { isPresenter } = useNav()
|
|
9
|
+
|
|
7
10
|
const scaleItems: SelectionItem<number>[] = [
|
|
8
11
|
{
|
|
9
12
|
display: 'Fit',
|
|
@@ -30,11 +33,18 @@ const wakeLockItems: SelectionItem<boolean>[] = [
|
|
|
30
33
|
</script>
|
|
31
34
|
|
|
32
35
|
<template>
|
|
33
|
-
<div
|
|
34
|
-
<SelectList
|
|
36
|
+
<div text-sm select-none flex="~ col gap-2" min-w-30>
|
|
37
|
+
<SelectList
|
|
38
|
+
v-if="!isPresenter"
|
|
39
|
+
v-model="slideScale"
|
|
40
|
+
title="Scale"
|
|
41
|
+
:items="scaleItems"
|
|
42
|
+
/>
|
|
35
43
|
<SelectList
|
|
36
44
|
v-if="__SLIDEV_FEATURE_WAKE_LOCK__ && isSupported"
|
|
37
|
-
v-model="wakeLockEnabled"
|
|
45
|
+
v-model="wakeLockEnabled"
|
|
46
|
+
title="Wake lock"
|
|
47
|
+
:items="wakeLockItems"
|
|
38
48
|
/>
|
|
39
49
|
</div>
|
|
40
50
|
</template>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useNav } from '../composables/useNav'
|
|
4
|
+
import { syncDirections } from '../state'
|
|
5
|
+
import IconButton from './IconButton.vue'
|
|
6
|
+
import MenuButton from './MenuButton.vue'
|
|
7
|
+
import SelectList from './SelectList.vue'
|
|
8
|
+
|
|
9
|
+
const { isPresenter } = useNav()
|
|
10
|
+
|
|
11
|
+
const shouldReceive = computed({
|
|
12
|
+
get: () => isPresenter.value
|
|
13
|
+
? syncDirections.value.presenterReceive
|
|
14
|
+
: syncDirections.value.viewerReceive,
|
|
15
|
+
set(v) {
|
|
16
|
+
if (isPresenter.value) {
|
|
17
|
+
syncDirections.value.presenterReceive = v
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
syncDirections.value.viewerReceive = v
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const shouldSend = computed({
|
|
26
|
+
get: () => isPresenter.value
|
|
27
|
+
? syncDirections.value.presenterSend
|
|
28
|
+
: syncDirections.value.viewerSend,
|
|
29
|
+
set(v) {
|
|
30
|
+
if (isPresenter.value) {
|
|
31
|
+
syncDirections.value.presenterSend = v
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
syncDirections.value.viewerSend = v
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<MenuButton>
|
|
42
|
+
<template #button>
|
|
43
|
+
<IconButton title="Change sync settings">
|
|
44
|
+
<div class="i-ph:arrow-up-bold mx--1.2 scale-x-80" :class="shouldSend ? 'text-green6 dark:text-green' : 'op30'" />
|
|
45
|
+
<div class="i-ph:arrow-down-bold mx--1.2 scale-x-80" :class="shouldReceive ? 'text-green6 dark:text-green' : 'op30'" />
|
|
46
|
+
</IconButton>
|
|
47
|
+
</template>
|
|
48
|
+
<template #menu>
|
|
49
|
+
<div text-sm flex="~ col gap-2">
|
|
50
|
+
<div px4 pt3 pb1 ws-nowrap>
|
|
51
|
+
<span op75>Slides navigation syncing for </span>
|
|
52
|
+
<span font-bold text-primary>{{ isPresenter ? 'presenter' : 'viewer' }}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<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"
|
|
65
|
+
:items="[
|
|
66
|
+
{ value: true, display: 'On' },
|
|
67
|
+
{ value: false, display: 'Off' },
|
|
68
|
+
]"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
</MenuButton>
|
|
73
|
+
</template>
|
package/logic/color.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isDark } from './dark'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Predefined color map for matching the branding
|
|
5
|
+
*
|
|
6
|
+
* Accpet a 6-digit hex color string or a hue number
|
|
7
|
+
* Hue numbers are preferred because they will adapt better contrast in light/dark mode
|
|
8
|
+
*
|
|
9
|
+
* Hue numbers reference:
|
|
10
|
+
* - 0: red
|
|
11
|
+
* - 30: orange
|
|
12
|
+
* - 60: yellow
|
|
13
|
+
* - 120: green
|
|
14
|
+
* - 180: cyan
|
|
15
|
+
* - 240: blue
|
|
16
|
+
* - 270: purple
|
|
17
|
+
*/
|
|
18
|
+
const predefinedColorMap = {
|
|
19
|
+
error: 0,
|
|
20
|
+
client: 60,
|
|
21
|
+
} as Record<string, number>
|
|
22
|
+
|
|
23
|
+
export function getHashColorFromString(
|
|
24
|
+
name: string,
|
|
25
|
+
opacity: number | string = 1,
|
|
26
|
+
) {
|
|
27
|
+
if (predefinedColorMap[name])
|
|
28
|
+
return getHsla(predefinedColorMap[name], opacity)
|
|
29
|
+
|
|
30
|
+
let hash = 0
|
|
31
|
+
for (let i = 0; i < name.length; i++)
|
|
32
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
|
33
|
+
const hue = hash % 360
|
|
34
|
+
return getHsla(hue, opacity)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getHsla(
|
|
38
|
+
hue: number,
|
|
39
|
+
opacity: number | string = 1,
|
|
40
|
+
) {
|
|
41
|
+
const saturation = hue === -1
|
|
42
|
+
? 0
|
|
43
|
+
: isDark.value ? 50 : 100
|
|
44
|
+
const lightness = isDark.value ? 60 : 20
|
|
45
|
+
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getPluginColor(name: string, opacity = 1): string {
|
|
49
|
+
if (predefinedColorMap[name]) {
|
|
50
|
+
const color = predefinedColorMap[name]
|
|
51
|
+
if (typeof color === 'number') {
|
|
52
|
+
return getHsla(color, opacity)
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (opacity === 1)
|
|
56
|
+
return color
|
|
57
|
+
const opacityHex = Math.floor(opacity * 255).toString(16).padStart(2, '0')
|
|
58
|
+
return color + opacityHex
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return getHashColorFromString(name, opacity)
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.51.0-beta.1",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,21 +32,21 @@
|
|
|
32
32
|
"@iconify-json/carbon": "^1.2.5",
|
|
33
33
|
"@iconify-json/ph": "^1.2.2",
|
|
34
34
|
"@iconify-json/svg-spinners": "^1.2.2",
|
|
35
|
-
"@shikijs/monaco": "^1.24.
|
|
36
|
-
"@shikijs/vitepress-twoslash": "^1.24.
|
|
35
|
+
"@shikijs/monaco": "^1.24.4",
|
|
36
|
+
"@shikijs/vitepress-twoslash": "^1.24.4",
|
|
37
37
|
"@slidev/rough-notation": "^0.1.0",
|
|
38
38
|
"@typescript/ata": "^0.9.7",
|
|
39
39
|
"@unhead/vue": "^1.11.14",
|
|
40
|
-
"@unocss/reset": "^0.65.
|
|
41
|
-
"@vueuse/core": "^12.
|
|
42
|
-
"@vueuse/math": "^12.
|
|
40
|
+
"@unocss/reset": "^0.65.3",
|
|
41
|
+
"@vueuse/core": "^12.2.0",
|
|
42
|
+
"@vueuse/math": "^12.2.0",
|
|
43
43
|
"@vueuse/motion": "^2.2.6",
|
|
44
44
|
"drauu": "^0.4.2",
|
|
45
45
|
"file-saver": "^2.0.5",
|
|
46
46
|
"floating-vue": "^5.2.2",
|
|
47
47
|
"fuse.js": "^7.0.0",
|
|
48
48
|
"html-to-image": "^1.11.11",
|
|
49
|
-
"katex": "^0.16.
|
|
49
|
+
"katex": "^0.16.18",
|
|
50
50
|
"lz-string": "^1.5.0",
|
|
51
51
|
"mermaid": "^11.4.1",
|
|
52
52
|
"monaco-editor": "0.51.0",
|
|
@@ -54,17 +54,17 @@
|
|
|
54
54
|
"pptxgenjs": "^3.12.0",
|
|
55
55
|
"prettier": "^3.4.2",
|
|
56
56
|
"recordrtc": "^5.6.2",
|
|
57
|
-
"shiki": "^1.24.
|
|
57
|
+
"shiki": "^1.24.4",
|
|
58
58
|
"shiki-magic-move": "^0.5.2",
|
|
59
59
|
"typescript": "5.6.3",
|
|
60
|
-
"unocss": "^0.65.
|
|
60
|
+
"unocss": "^0.65.3",
|
|
61
61
|
"vue": "^3.5.13",
|
|
62
62
|
"vue-router": "^4.5.0",
|
|
63
63
|
"yaml": "^2.6.1",
|
|
64
|
-
"@slidev/parser": "0.
|
|
65
|
-
"@slidev/types": "0.
|
|
64
|
+
"@slidev/parser": "0.51.0-beta.1",
|
|
65
|
+
"@slidev/types": "0.51.0-beta.1"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"vite": "^6.0.
|
|
68
|
+
"vite": "^6.0.6"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/pages/export.vue
CHANGED
|
@@ -238,8 +238,8 @@ if (import.meta.hot) {
|
|
|
238
238
|
<div class="min-w-fit" flex="~ col gap-3">
|
|
239
239
|
<div border="~ main rounded-lg" p3 flex="~ col gap-2">
|
|
240
240
|
<h2>Export as Vector File</h2>
|
|
241
|
-
<div class="flex flex-col gap-2
|
|
242
|
-
<button @click="pdf">
|
|
241
|
+
<div class="flex flex-col gap-2 min-w-max">
|
|
242
|
+
<button class="slidev-form-button" @click="pdf">
|
|
243
243
|
PDF
|
|
244
244
|
</button>
|
|
245
245
|
</div>
|
|
@@ -253,22 +253,22 @@ if (import.meta.hot) {
|
|
|
253
253
|
If you encounter issues, please use a modern Chromium-based browser,
|
|
254
254
|
or export via the CLI.
|
|
255
255
|
</div>
|
|
256
|
-
<div class="flex flex-col gap-2
|
|
257
|
-
<button @click="pptx">
|
|
256
|
+
<div class="flex flex-col gap-2 min-w-max">
|
|
257
|
+
<button class="slidev-form-button" @click="pptx">
|
|
258
258
|
PPTX
|
|
259
259
|
</button>
|
|
260
|
-
<button @click="pngsGz">
|
|
260
|
+
<button class="slidev-form-button" @click="pngsGz">
|
|
261
261
|
PNGs.gz
|
|
262
262
|
</button>
|
|
263
263
|
</div>
|
|
264
264
|
<div w-full h-1px border="t main" my2 />
|
|
265
265
|
<div class="relative flex flex-col gap-2 flex-nowrap">
|
|
266
|
-
<div class="flex flex-col gap-2
|
|
267
|
-
<button v-if="capturedImages" class="flex justify-center items-center gap-2" @click="capturedImages = null">
|
|
266
|
+
<div class="flex flex-col gap-2 min-w-max">
|
|
267
|
+
<button v-if="capturedImages" class="slidev-form-button flex justify-center items-center gap-2" @click="capturedImages = null">
|
|
268
268
|
<span class="i-carbon:trash-can inline-block text-xl" />
|
|
269
269
|
Clear Captured Images
|
|
270
270
|
</button>
|
|
271
|
-
<button v-else class="flex justify-center items-center gap-2" @click="capturePngs">
|
|
271
|
+
<button v-else class="slidev-form-button flex justify-center items-center gap-2" @click="capturePngs">
|
|
272
272
|
<div class="i-carbon:camera-action inline-block text-xl" />
|
|
273
273
|
Pre-capture Slides as Images
|
|
274
274
|
</button>
|
|
@@ -325,10 +325,6 @@ if (import.meta.hot) {
|
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
button {
|
|
329
|
-
--uno: 'w-full rounded bg-gray:10 px-4 py-2 hover:bg-gray/20';
|
|
330
|
-
}
|
|
331
|
-
|
|
332
328
|
label {
|
|
333
329
|
--uno: text-xl flex gap-2 items-center select-none;
|
|
334
330
|
|
package/pages/notes.vue
CHANGED
|
@@ -19,7 +19,7 @@ const { isFullscreen, toggle: toggleFullscreen } = fullscreen
|
|
|
19
19
|
|
|
20
20
|
const scroller = ref<HTMLDivElement>()
|
|
21
21
|
const fontSize = useLocalStorage('slidev-notes-font-size', 18)
|
|
22
|
-
const pageNo = computed(() => sharedState.
|
|
22
|
+
const pageNo = computed(() => sharedState.page)
|
|
23
23
|
const currentRoute = computed(() => slides.value.find(i => i.no === pageNo.value))
|
|
24
24
|
|
|
25
25
|
watch(pageNo, () => {
|
|
@@ -36,8 +36,8 @@ function decreaseFontSize() {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const clicksContext = computed(() => {
|
|
39
|
-
const clicks = sharedState.
|
|
40
|
-
const total = sharedState.
|
|
39
|
+
const clicks = sharedState.clicks
|
|
40
|
+
const total = sharedState.clicksTotal
|
|
41
41
|
return createClicksContextBase(ref(clicks), undefined, total)
|
|
42
42
|
})
|
|
43
43
|
</script>
|
package/pages/presenter.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { useHead } from '@unhead/vue'
|
|
3
|
-
import { useMouse, useWindowFocus } from '@vueuse/core'
|
|
3
|
+
import { useLocalStorage, useMouse, useWindowFocus } from '@vueuse/core'
|
|
4
4
|
import { computed, onMounted, reactive, ref, shallowRef, watch } from 'vue'
|
|
5
5
|
import { createFixedClicks } from '../composables/useClicks'
|
|
6
6
|
import { useDrawings } from '../composables/useDrawings'
|
|
@@ -18,6 +18,8 @@ import NavControls from '../internals/NavControls.vue'
|
|
|
18
18
|
import NoteEditable from '../internals/NoteEditable.vue'
|
|
19
19
|
import NoteStatic from '../internals/NoteStatic.vue'
|
|
20
20
|
import QuickOverview from '../internals/QuickOverview.vue'
|
|
21
|
+
import ScreenCaptureMirror from '../internals/ScreenCaptureMirror.vue'
|
|
22
|
+
import SegmentControl from '../internals/SegmentControl.vue'
|
|
21
23
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
22
24
|
import SlidesShow from '../internals/SlidesShow.vue'
|
|
23
25
|
import SlideWrapper from '../internals/SlideWrapper.vue'
|
|
@@ -26,6 +28,7 @@ import { registerShortcuts } from '../logic/shortcuts'
|
|
|
26
28
|
import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
|
|
27
29
|
import { sharedState } from '../state/shared'
|
|
28
30
|
|
|
31
|
+
const inFocus = useWindowFocus()
|
|
29
32
|
const main = ref<HTMLDivElement>()
|
|
30
33
|
|
|
31
34
|
registerShortcuts()
|
|
@@ -49,7 +52,7 @@ useHead({ title: `Presenter - ${slidesTitle}` })
|
|
|
49
52
|
|
|
50
53
|
const notesEditing = ref(false)
|
|
51
54
|
|
|
52
|
-
const { timer,
|
|
55
|
+
const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer()
|
|
53
56
|
|
|
54
57
|
const clicksCtxMap = computed(() => slides.value.map(route => createFixedClicks(route)))
|
|
55
58
|
const nextFrame = computed(() => {
|
|
@@ -74,6 +77,7 @@ watch(
|
|
|
74
77
|
{ immediate: true },
|
|
75
78
|
)
|
|
76
79
|
|
|
80
|
+
const mainSlideMode = useLocalStorage<'slides' | 'mirror'>('slidev-presenter-main-slide-mode', 'slides')
|
|
77
81
|
const SideEditor = shallowRef<any>()
|
|
78
82
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
79
83
|
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
@@ -86,7 +90,7 @@ onMounted(() => {
|
|
|
86
90
|
|
|
87
91
|
watch(
|
|
88
92
|
() => {
|
|
89
|
-
if (!focus.value || isDrawing.value || !showPresenterCursor.value)
|
|
93
|
+
if (!focus.value || isDrawing.value || !showPresenterCursor.value || !slidesContainer)
|
|
90
94
|
return undefined
|
|
91
95
|
|
|
92
96
|
const rect = slidesContainer.getBoundingClientRect()
|
|
@@ -106,25 +110,39 @@ onMounted(() => {
|
|
|
106
110
|
</script>
|
|
107
111
|
|
|
108
112
|
<template>
|
|
109
|
-
<div class="bg-main h-full slidev-presenter">
|
|
113
|
+
<div class="bg-main h-full slidev-presenter" pt-2px>
|
|
110
114
|
<div class="grid-container" :class="`layout${presenterLayout}`">
|
|
111
115
|
<div ref="main" class="relative grid-section main flex flex-col">
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
<div flex="~ gap-4 items-center" border="b main" p1>
|
|
117
|
+
<span op50 px2>Current</span>
|
|
118
|
+
<div flex-auto />
|
|
119
|
+
<SegmentControl
|
|
120
|
+
v-model="mainSlideMode"
|
|
121
|
+
:options="[
|
|
122
|
+
{ label: 'Slides', value: 'slides' },
|
|
123
|
+
{ label: 'Screen Mirror', value: 'mirror' },
|
|
124
|
+
]"
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
<template v-if="mainSlideMode === 'mirror'">
|
|
128
|
+
<ScreenCaptureMirror />
|
|
129
|
+
</template>
|
|
130
|
+
<template v-else>
|
|
131
|
+
<SlideContainer
|
|
132
|
+
key="main"
|
|
133
|
+
class="p-2 lg:p-4 flex-auto"
|
|
134
|
+
is-main
|
|
135
|
+
@contextmenu="onContextMenu"
|
|
136
|
+
>
|
|
137
|
+
<SlidesShow render-context="presenter" />
|
|
138
|
+
</SlideContainer>
|
|
139
|
+
</template>
|
|
140
|
+
|
|
120
141
|
<ClicksSlider
|
|
121
142
|
:key="currentSlideRoute?.no"
|
|
122
143
|
:clicks-context="getPrimaryClicks(currentSlideRoute)"
|
|
123
144
|
class="w-full pb2 px4 flex-none"
|
|
124
145
|
/>
|
|
125
|
-
<div class="absolute left-0 top-0 bg-main border-b border-r border-main px2 py1 op50 text-sm">
|
|
126
|
-
Current
|
|
127
|
-
</div>
|
|
128
146
|
</div>
|
|
129
147
|
<div class="relative grid-section next flex flex-col p-2 lg:p-4">
|
|
130
148
|
<SlideContainer v-if="nextFrame && nextFrameClicksCtx" key="next">
|
|
@@ -165,7 +183,8 @@ onMounted(() => {
|
|
|
165
183
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
166
184
|
:clicks-context="clicksContext"
|
|
167
185
|
/>
|
|
168
|
-
<div
|
|
186
|
+
<div border-t border-main />
|
|
187
|
+
<div class="py-1 px-2 text-sm transition" :class="inFocus ? '' : 'op25'">
|
|
169
188
|
<IconButton title="Increase font size" @click="increasePresenterFontSize">
|
|
170
189
|
<div class="i-carbon:zoom-in" />
|
|
171
190
|
</IconButton>
|
|
@@ -182,14 +201,14 @@ onMounted(() => {
|
|
|
182
201
|
</div>
|
|
183
202
|
</div>
|
|
184
203
|
<div class="grid-section bottom flex">
|
|
185
|
-
<NavControls :persist="true" />
|
|
204
|
+
<NavControls :persist="true" class="transition" :class="inFocus ? '' : 'op25'" />
|
|
186
205
|
<div flex-auto />
|
|
187
206
|
<div class="group flex items-center justify-center pl-4 select-none">
|
|
188
207
|
<div class="w-22px cursor-pointer">
|
|
189
208
|
<div class="i-carbon:time group-hover:hidden text-xl" />
|
|
190
209
|
<div class="group-not-hover:hidden flex flex-col items-center">
|
|
191
210
|
<div class="relative op-80 hover:op-100" @click="toggleTimer">
|
|
192
|
-
<div v-if="
|
|
211
|
+
<div v-if="isTimerActive" class="i-carbon:pause text-lg" />
|
|
193
212
|
<div v-else class="i-carbon:play" />
|
|
194
213
|
</div>
|
|
195
214
|
<div class="op-80 hover:op-100" @click="resetTimer">
|
package/setup/root.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { configs, slidesTitle } from '../env'
|
|
|
11
11
|
import { skipTransition } from '../logic/hmr'
|
|
12
12
|
import { getSlidePath } from '../logic/slides'
|
|
13
13
|
import { makeId } from '../logic/utils'
|
|
14
|
+
import { syncDirections } from '../state'
|
|
14
15
|
import { initDrawingState } from '../state/drawings'
|
|
15
16
|
import { initSharedState, onPatch, patch } from '../state/shared'
|
|
16
17
|
|
|
@@ -58,30 +59,28 @@ export default function setupRoot() {
|
|
|
58
59
|
initDrawingState(`${slidesTitle} - drawings`)
|
|
59
60
|
|
|
60
61
|
const id = `${location.origin}_${makeId()}`
|
|
62
|
+
const syncType = computed(() => isPresenter.value ? 'presenter' : 'viewer')
|
|
61
63
|
|
|
62
64
|
// update shared state
|
|
63
65
|
function updateSharedState() {
|
|
66
|
+
const shouldSend = isPresenter.value
|
|
67
|
+
? syncDirections.value.presenterSend
|
|
68
|
+
: syncDirections.value.viewerSend
|
|
69
|
+
|
|
70
|
+
if (!shouldSend)
|
|
71
|
+
return
|
|
64
72
|
if (isNotesViewer.value || isPrintMode.value)
|
|
65
73
|
return
|
|
66
|
-
|
|
67
74
|
// we allow Presenter mode, or Viewer mode from trusted origins to update the shared state
|
|
68
75
|
if (!isPresenter.value && !TRUST_ORIGINS.includes(location.host.split(':')[0]))
|
|
69
76
|
return
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
patch('clicksTotal', clicksContext.value.total)
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
patch('viewerPage', +currentSlideNo.value)
|
|
78
|
-
patch('viewerClicks', clicksContext.value.current)
|
|
79
|
-
patch('viewerClicksTotal', clicksContext.value.total)
|
|
80
|
-
}
|
|
81
|
-
|
|
78
|
+
patch('page', +currentSlideNo.value)
|
|
79
|
+
patch('clicks', clicksContext.value.current)
|
|
80
|
+
patch('clicksTotal', clicksContext.value.total)
|
|
82
81
|
patch('lastUpdate', {
|
|
83
82
|
id,
|
|
84
|
-
type:
|
|
83
|
+
type: syncType.value,
|
|
85
84
|
time: new Date().getTime(),
|
|
86
85
|
})
|
|
87
86
|
}
|
|
@@ -90,17 +89,26 @@ export default function setupRoot() {
|
|
|
90
89
|
watch(clicksContext, updateSharedState)
|
|
91
90
|
|
|
92
91
|
onPatch((state) => {
|
|
92
|
+
const shouldReceive = isPresenter.value
|
|
93
|
+
? syncDirections.value.presenterReceive
|
|
94
|
+
: syncDirections.value.viewerReceive
|
|
95
|
+
if (!shouldReceive)
|
|
96
|
+
return
|
|
93
97
|
if (!hasPrimarySlide.value || isPrintMode.value)
|
|
94
98
|
return
|
|
95
|
-
if (state.lastUpdate?.type ===
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
if (state.lastUpdate?.type === syncType.value)
|
|
100
|
+
return
|
|
101
|
+
if ((+state.page === +currentSlideNo.value && +clicksContext.value.current === +state.clicks))
|
|
102
|
+
return
|
|
103
|
+
// if (state.lastUpdate?.type === 'presenter') {
|
|
104
|
+
skipTransition.value = false
|
|
105
|
+
router.replace({
|
|
106
|
+
path: getSlidePath(state.page, isPresenter.value),
|
|
107
|
+
query: {
|
|
108
|
+
...router.currentRoute.value.query,
|
|
109
|
+
clicks: state.clicks || 0,
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
// }
|
|
105
113
|
})
|
|
106
114
|
}
|
package/state/drawings.ts
CHANGED
|
@@ -9,4 +9,8 @@ export const {
|
|
|
9
9
|
onUpdate: onDrawingUpdate,
|
|
10
10
|
patch: patchDrawingState,
|
|
11
11
|
state: drawingState,
|
|
12
|
-
} = createSyncState<DrawingsState>(
|
|
12
|
+
} = createSyncState<DrawingsState>(
|
|
13
|
+
serverDrawingState,
|
|
14
|
+
serverDrawingState,
|
|
15
|
+
__SLIDEV_FEATURE_DRAWINGS_PERSIST__,
|
|
16
|
+
)
|
package/state/index.ts
CHANGED
|
@@ -1,56 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
|
|
3
|
-
import { computed, ref, shallowRef } from 'vue'
|
|
4
|
-
import { slideAspect } from '../env'
|
|
5
|
-
|
|
6
|
-
export const showRecordingDialog = ref(false)
|
|
7
|
-
export const showInfoDialog = ref(false)
|
|
8
|
-
export const showGotoDialog = ref(false)
|
|
9
|
-
export const showOverview = ref(false)
|
|
10
|
-
|
|
11
|
-
export const shortcutsEnabled = ref(true)
|
|
12
|
-
export const breakpoints = useBreakpoints({
|
|
13
|
-
xs: 460,
|
|
14
|
-
...breakpointsTailwind,
|
|
15
|
-
})
|
|
16
|
-
export const windowSize = useWindowSize()
|
|
17
|
-
export const magicKeys = useMagicKeys()
|
|
18
|
-
export const isScreenVertical = computed(() => windowSize.height.value - windowSize.width.value / slideAspect.value > 120)
|
|
19
|
-
export const fullscreen = useFullscreen(isClient ? document.body : null)
|
|
20
|
-
|
|
21
|
-
export const activeElement = useActiveElement()
|
|
22
|
-
export const isInputting = computed(() => ['INPUT', 'TEXTAREA'].includes(activeElement.value?.tagName || ''))
|
|
23
|
-
export const isOnFocus = computed(() => ['BUTTON', 'A'].includes(activeElement.value?.tagName || ''))
|
|
24
|
-
|
|
25
|
-
export const currentCamera = useLocalStorage<string>('slidev-camera', 'default', { listenToStorageChanges: false })
|
|
26
|
-
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
27
|
-
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
28
|
-
export const wakeLockEnabled = useLocalStorage('slidev-wake-lock', true)
|
|
29
|
-
export const skipExportPdfTip = useLocalStorage('slidev-skip-export-pdf-tip', false)
|
|
30
|
-
|
|
31
|
-
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
32
|
-
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
33
|
-
export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false, { listenToStorageChanges: false })
|
|
34
|
-
export const editorWidth = useLocalStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 318, { listenToStorageChanges: false })
|
|
35
|
-
export const editorHeight = useLocalStorage('slidev-editor-height', isClient ? window.innerHeight * 0.4 : 300, { listenToStorageChanges: false })
|
|
36
|
-
|
|
37
|
-
export const activeDragElement = shallowRef<DragElementState | null>(null)
|
|
38
|
-
|
|
39
|
-
export const presenterNotesFontSize = useLocalStorage('slidev-presenter-font-size', 1, { listenToStorageChanges: false })
|
|
40
|
-
export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { listenToStorageChanges: false })
|
|
41
|
-
|
|
42
|
-
export function togglePresenterLayout() {
|
|
43
|
-
presenterLayout.value = presenterLayout.value + 1
|
|
44
|
-
if (presenterLayout.value > 3)
|
|
45
|
-
presenterLayout.value = 1
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function increasePresenterFontSize() {
|
|
49
|
-
presenterNotesFontSize.value = Math.min(2, presenterNotesFontSize.value + 0.1)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function decreasePresenterFontSize() {
|
|
53
|
-
presenterNotesFontSize.value = Math.max(0.5, presenterNotesFontSize.value - 0.1)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const toggleOverview = useToggle(showOverview)
|
|
1
|
+
export * from './storage'
|
package/state/shared.ts
CHANGED
|
@@ -11,10 +11,6 @@ export interface SharedState {
|
|
|
11
11
|
y: number
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
viewerPage: number
|
|
15
|
-
viewerClicks: number
|
|
16
|
-
viewerClicksTotal: number
|
|
17
|
-
|
|
18
14
|
lastUpdate?: {
|
|
19
15
|
id: string
|
|
20
16
|
type: 'presenter' | 'viewer'
|
|
@@ -26,9 +22,6 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
|
|
|
26
22
|
page: 1,
|
|
27
23
|
clicks: 0,
|
|
28
24
|
clicksTotal: 0,
|
|
29
|
-
viewerPage: 1,
|
|
30
|
-
viewerClicks: 0,
|
|
31
|
-
viewerClicksTotal: 0,
|
|
32
25
|
})
|
|
33
26
|
|
|
34
27
|
export {
|
package/state/storage.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DragElementState } from '../composables/useDragElements'
|
|
2
|
+
import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
|
|
3
|
+
import { computed, ref, shallowRef } from 'vue'
|
|
4
|
+
import { slideAspect } from '../env'
|
|
5
|
+
|
|
6
|
+
export const showRecordingDialog = ref(false)
|
|
7
|
+
export const showInfoDialog = ref(false)
|
|
8
|
+
export const showGotoDialog = ref(false)
|
|
9
|
+
export const showOverview = ref(false)
|
|
10
|
+
|
|
11
|
+
export const shortcutsEnabled = ref(true)
|
|
12
|
+
export const breakpoints = useBreakpoints({
|
|
13
|
+
xs: 460,
|
|
14
|
+
...breakpointsTailwind,
|
|
15
|
+
})
|
|
16
|
+
export const windowSize = useWindowSize()
|
|
17
|
+
export const magicKeys = useMagicKeys()
|
|
18
|
+
export const isScreenVertical = computed(() => windowSize.height.value - windowSize.width.value / slideAspect.value > 120)
|
|
19
|
+
export const fullscreen = useFullscreen(isClient ? document.body : null)
|
|
20
|
+
|
|
21
|
+
export const activeElement = useActiveElement()
|
|
22
|
+
export const isInputting = computed(() => ['INPUT', 'TEXTAREA'].includes(activeElement.value?.tagName || ''))
|
|
23
|
+
export const isOnFocus = computed(() => ['BUTTON', 'A'].includes(activeElement.value?.tagName || ''))
|
|
24
|
+
|
|
25
|
+
export const currentCamera = useLocalStorage<string>('slidev-camera', 'default', { listenToStorageChanges: false })
|
|
26
|
+
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
27
|
+
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
28
|
+
export const wakeLockEnabled = useLocalStorage('slidev-wake-lock', true)
|
|
29
|
+
export const skipExportPdfTip = useLocalStorage('slidev-skip-export-pdf-tip', false)
|
|
30
|
+
|
|
31
|
+
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
32
|
+
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
33
|
+
export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false, { listenToStorageChanges: false })
|
|
34
|
+
export const editorWidth = useLocalStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 318, { listenToStorageChanges: false })
|
|
35
|
+
export const editorHeight = useLocalStorage('slidev-editor-height', isClient ? window.innerHeight * 0.4 : 300, { listenToStorageChanges: false })
|
|
36
|
+
|
|
37
|
+
export const activeDragElement = shallowRef<DragElementState | null>(null)
|
|
38
|
+
|
|
39
|
+
export const presenterNotesFontSize = useLocalStorage('slidev-presenter-font-size', 1, { listenToStorageChanges: false })
|
|
40
|
+
export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { listenToStorageChanges: false })
|
|
41
|
+
|
|
42
|
+
export function togglePresenterLayout() {
|
|
43
|
+
presenterLayout.value = presenterLayout.value + 1
|
|
44
|
+
if (presenterLayout.value > 3)
|
|
45
|
+
presenterLayout.value = 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function increasePresenterFontSize() {
|
|
49
|
+
presenterNotesFontSize.value = Math.min(2, presenterNotesFontSize.value + 0.1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function decreasePresenterFontSize() {
|
|
53
|
+
presenterNotesFontSize.value = Math.max(0.5, presenterNotesFontSize.value - 0.1)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const toggleOverview = useToggle(showOverview)
|
|
57
|
+
|
|
58
|
+
export const syncDirections = useLocalStorage(
|
|
59
|
+
'slidev-sync-directions',
|
|
60
|
+
{
|
|
61
|
+
viewerSend: true,
|
|
62
|
+
viewerReceive: true,
|
|
63
|
+
presenterSend: true,
|
|
64
|
+
presenterReceive: true,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
listenToStorageChanges: false,
|
|
68
|
+
mergeDefaults: true,
|
|
69
|
+
},
|
|
70
|
+
)
|
package/styles/index.css
CHANGED
|
@@ -121,6 +121,16 @@ html {
|
|
|
121
121
|
transform: translateY(0.1em);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
.slidev-form-button {
|
|
125
|
+
--uno: text-white px-4 py-1 rounded border-b-2;
|
|
126
|
+
--uno: 'bg-gray-400:50 border-gray-800:50';
|
|
127
|
+
--uno: 'hover:(bg-gray-400:75 border-gray8:75)';
|
|
128
|
+
}
|
|
129
|
+
.slidev-form-button.primary {
|
|
130
|
+
--uno: bg-teal-600 border-teal-800;
|
|
131
|
+
--uno: 'hover:(bg-teal-500 border-teal-700)';
|
|
132
|
+
}
|
|
133
|
+
|
|
124
134
|
/* Transform the position back for Rough Notation (v-mark) */
|
|
125
135
|
.rough-annotation {
|
|
126
136
|
transform: scale(calc(1 / var(--slidev-slide-scale)));
|
package/uno.config.ts
CHANGED