Skip to content

Commit 56eec89

Browse files
committed
feat(presenter): use screen capture mirroring, resolve #1987
1 parent a2afb1c commit 56eec89

13 files changed

+245
-64
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@
5050
"slidev.include": [
5151
"**/slides.md",
5252
"packages/vscode/syntax/slidev.example.md"
53-
]
53+
],
54+
"vue.server.hybridMode": "typeScriptPluginOnly"
5455
}

packages/client/composables/useClicks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export function createFixedClicks(
165165
): ClicksContext {
166166
const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
167167
return createClicksContextBase(
168-
computed(() => Math.max(toValue(currentInit), clicksStart)),
168+
ref(Math.max(toValue(currentInit), clicksStart)),
169169
clicksStart,
170170
route?.meta?.clicks,
171171
)

packages/client/composables/useTimer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export function useTimer() {
1313

1414
return {
1515
timer,
16-
isTimerAvctive: isActive,
16+
isTimerActive: isActive,
1717
resetTimer: reset,
1818
toggleTimer: () => (isActive.value ? pause() : resume()),
1919
}

packages/client/internals/Badge.vue

+48
Original file line numberDiff line numberDiff line change
@@ -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>

packages/client/internals/FocusIndicator.vue

-9
This file was deleted.

packages/client/internals/RecordingDialog.vue

+2-12
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ async function start() {
7575
<DevicesSelectors />
7676
</div>
7777
<div class="flex my-1">
78-
<button class="cancel" @click="close">
78+
<button class="slidev-form-button" @click="close">
7979
Cancel
8080
</button>
8181
<div class="flex-auto" />
82-
<button @click="start">
82+
<button class="slidev-form-button primary" @click="start">
8383
Start
8484
</button>
8585
</div>
@@ -111,15 +111,5 @@ async function start() {
111111
input[type='text'] {
112112
@apply border border-main rounded px-2 py-1;
113113
}
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-
}
124114
}
125115
</style>
Original file line numberDiff line numberDiff line change
@@ -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>
Original file line numberDiff line numberDiff line change
@@ -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>

packages/client/internals/SyncControls.vue

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ const shouldSend = computed({
4747
</template>
4848
<template #menu>
4949
<div text-sm flex="~ col gap-2">
50-
<div px4 pt3 ws-nowrap>
50+
<div px4 pt3 pb1 ws-nowrap>
5151
<span op75>Slides navigation syncing for </span>
52-
<span font-bold text-primary>{{ isPresenter.value ? 'presenter' : 'viewer' }}</span>
52+
<span font-bold text-primary>{{ isPresenter ? 'presenter' : 'viewer' }}</span>
5353
</div>
5454
<SelectList
5555
v-model="shouldSend"

packages/client/logic/color.ts

+62
Original file line numberDiff line numberDiff line change
@@ -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+
}

packages/client/pages/export.vue

+8-12
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,8 @@ if (import.meta.hot) {
238238
<div class="min-w-fit" flex="~ col gap-3">
239239
<div border="~ main rounded-lg" p3 flex="~ col gap-2">
240240
<h2>Export as Vector File</h2>
241-
<div class="flex flex-col gap-2 items-start min-w-max">
242-
<button @click="pdf">
241+
<div class="flex flex-col gap-2 min-w-max">
242+
<button class="slidev-form-button" @click="pdf">
243243
PDF
244244
</button>
245245
</div>
@@ -253,22 +253,22 @@ if (import.meta.hot) {
253253
If you encounter issues, please use a modern Chromium-based browser,
254254
or export via the CLI.
255255
</div>
256-
<div class="flex flex-col gap-2 items-start min-w-max">
257-
<button @click="pptx">
256+
<div class="flex flex-col gap-2 min-w-max">
257+
<button class="slidev-form-button" @click="pptx">
258258
PPTX
259259
</button>
260-
<button @click="pngsGz">
260+
<button class="slidev-form-button" @click="pngsGz">
261261
PNGs.gz
262262
</button>
263263
</div>
264264
<div w-full h-1px border="t main" my2 />
265265
<div class="relative flex flex-col gap-2 flex-nowrap">
266-
<div class="flex flex-col gap-2 items-start min-w-max">
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">
268268
<span class="i-carbon:trash-can inline-block text-xl" />
269269
Clear Captured Images
270270
</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">
272272
<div class="i-carbon:camera-action inline-block text-xl" />
273273
Pre-capture Slides as Images
274274
</button>
@@ -325,10 +325,6 @@ if (import.meta.hot) {
325325
}
326326
}
327327
328-
button {
329-
--uno: 'w-full rounded bg-gray:10 px-4 py-2 hover:bg-gray/20';
330-
}
331-
332328
label {
333329
--uno: text-xl flex gap-2 items-center select-none;
334330

0 commit comments

Comments
 (0)