Skip to content

Commit

Permalink
feat(presenter): use screen capture mirroring, resolve #1987
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Dec 26, 2024
1 parent a2afb1c commit 56eec89
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 64 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
"slidev.include": [
"**/slides.md",
"packages/vscode/syntax/slidev.example.md"
]
],
"vue.server.hybridMode": "typeScriptPluginOnly"
}
2 changes: 1 addition & 1 deletion packages/client/composables/useClicks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ export function createFixedClicks(
): ClicksContext {
const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
return createClicksContextBase(
computed(() => Math.max(toValue(currentInit), clicksStart)),
ref(Math.max(toValue(currentInit), clicksStart)),
clicksStart,
route?.meta?.clicks,
)
Expand Down
2 changes: 1 addition & 1 deletion packages/client/composables/useTimer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function useTimer() {

return {
timer,
isTimerAvctive: isActive,
isTimerActive: isActive,
resetTimer: reset,
toggleTimer: () => (isActive.value ? pause() : resume()),
}
Expand Down
48 changes: 48 additions & 0 deletions packages/client/internals/Badge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { computed } from 'vue'
import {
getHashColorFromString,
getHsla,
} from '../logic/color'
const props = withDefaults(
defineProps<{
text?: string
color?: boolean | number
as?: string
size?: string
}>(),
{
color: true,
},
)
const style = computed(() => {
if (!props.text || props.color === false)
return {}
return {
color: typeof props.color === 'number'
? getHsla(props.color)
: getHashColorFromString(props.text),
background: typeof props.color === 'number'
? getHsla(props.color, 0.1)
: getHashColorFromString(props.text, 0.1),
}
})
const sizeClasses = computed(() => {
switch (props.size || 'sm') {
case 'sm':
return 'px-1.5 text-11px leading-1.6em'
}
return ''
})
</script>

<template>
<component :is="as || 'span'" ws-nowrap rounded :class="sizeClasses" :style>
<slot>
<span v-text="props.text" />
</slot>
</component>
</template>
9 changes: 0 additions & 9 deletions packages/client/internals/FocusIndicator.vue

This file was deleted.

14 changes: 2 additions & 12 deletions packages/client/internals/RecordingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ async function start() {
<DevicesSelectors />
</div>
<div class="flex my-1">
<button class="cancel" @click="close">
<button class="slidev-form-button" @click="close">
Cancel
</button>
<div class="flex-auto" />
<button @click="start">
<button class="slidev-form-button primary" @click="start">
Start
</button>
</div>
Expand Down Expand Up @@ -111,15 +111,5 @@ async function start() {
input[type='text'] {
@apply border border-main rounded px-2 py-1;
}
button {
@apply bg-orange-400 text-white px-4 py-1 rounded border-b-2 border-orange-600;
@apply hover:(bg-orange-500 border-orange-700);
}
button.cancel {
@apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
@apply hover:(bg-opacity-75 border-opacity-75);
}
}
</style>
45 changes: 45 additions & 0 deletions packages/client/internals/ScreenCaptureMirror.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import { shallowRef, useTemplateRef } from 'vue'
const video = useTemplateRef('video')
const stream = shallowRef<MediaStream | null>(null)
const started = shallowRef(false)
async function startCapture() {
stream.value = await navigator.mediaDevices.getDisplayMedia({
video: {
// @ts-expect-error missing types
cursor: 'always',
},
audio: false,
selfBrowserSurface: 'include',
preferCurrentTab: false,
})
video.value!.srcObject = stream.value
video.value!.play()
started.value = true
stream.value.addEventListener('inactive', () => {
video.value!.srcObject = null
started.value = false
})
stream.value.addEventListener('ended', () => {
video.value!.srcObject = null
started.value = false
})
}
</script>

<template>
<div h-full w-full>
<video v-show="started" ref="video" class="w-full h-full object-contain" />
<div v-if="!started" w-full h-full flex="~ col gap-4 items-center justify-center">
<div op50>
Use screen capturing to mirror your main screen back to presenter view.<br>
Click the button below and <b>select your other monitor or window</b>.
</div>
<button class="slidev-form-button" @click="startCapture">
Start Screen Mirroring
</button>
</div>
</div>
</template>
29 changes: 29 additions & 0 deletions packages/client/internals/SegmentControl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import Badge from './Badge.vue'
defineProps<{
options: { label: string, value: any }[]
modelValue: any
}>()
defineEmits<{
(event: 'update:modelValue', newValue: any): void
}>()
</script>

<template>
<div flex="~ gap-1 items-center" rounded bg-gray:2 p1>
<Badge
v-for="option in options"
:key="option.value"
class="px-2 py-1 text-xs font-mono"
:class="option.value === modelValue ? '' : 'op50'"
:color="option.value === modelValue"
:aria-pressed="option.value === modelValue"
size="none"
:text="option.label"
as="button"
@click="$emit('update:modelValue', option.value)"
/>
</div>
</template>
4 changes: 2 additions & 2 deletions packages/client/internals/SyncControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ const shouldSend = computed({
</template>
<template #menu>
<div text-sm flex="~ col gap-2">
<div px4 pt3 ws-nowrap>
<div px4 pt3 pb1 ws-nowrap>
<span op75>Slides navigation syncing for </span>
<span font-bold text-primary>{{ isPresenter.value ? 'presenter' : 'viewer' }}</span>
<span font-bold text-primary>{{ isPresenter ? 'presenter' : 'viewer' }}</span>
</div>
<SelectList
v-model="shouldSend"
Expand Down
62 changes: 62 additions & 0 deletions packages/client/logic/color.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { isDark } from './dark'

/**
* Predefined color map for matching the branding
*
* Accpet a 6-digit hex color string or a hue number
* Hue numbers are preferred because they will adapt better contrast in light/dark mode
*
* Hue numbers reference:
* - 0: red
* - 30: orange
* - 60: yellow
* - 120: green
* - 180: cyan
* - 240: blue
* - 270: purple
*/
const predefinedColorMap = {
error: 0,
client: 60,
} as Record<string, number>

export function getHashColorFromString(
name: string,
opacity: number | string = 1,
) {
if (predefinedColorMap[name])
return getHsla(predefinedColorMap[name], opacity)

let hash = 0
for (let i = 0; i < name.length; i++)
hash = name.charCodeAt(i) + ((hash << 5) - hash)
const hue = hash % 360
return getHsla(hue, opacity)
}

export function getHsla(
hue: number,
opacity: number | string = 1,
) {
const saturation = hue === -1
? 0
: isDark.value ? 50 : 100
const lightness = isDark.value ? 60 : 20
return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`
}

export function getPluginColor(name: string, opacity = 1): string {
if (predefinedColorMap[name]) {
const color = predefinedColorMap[name]
if (typeof color === 'number') {
return getHsla(color, opacity)
}
else {
if (opacity === 1)
return color
const opacityHex = Math.floor(opacity * 255).toString(16).padStart(2, '0')
return color + opacityHex
}
}
return getHashColorFromString(name, opacity)
}
20 changes: 8 additions & 12 deletions packages/client/pages/export.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,8 @@ if (import.meta.hot) {
<div class="min-w-fit" flex="~ col gap-3">
<div border="~ main rounded-lg" p3 flex="~ col gap-2">
<h2>Export as Vector File</h2>
<div class="flex flex-col gap-2 items-start min-w-max">
<button @click="pdf">
<div class="flex flex-col gap-2 min-w-max">
<button class="slidev-form-button" @click="pdf">
PDF
</button>
</div>
Expand All @@ -253,22 +253,22 @@ if (import.meta.hot) {
If you encounter issues, please use a modern Chromium-based browser,
or export via the CLI.
</div>
<div class="flex flex-col gap-2 items-start min-w-max">
<button @click="pptx">
<div class="flex flex-col gap-2 min-w-max">
<button class="slidev-form-button" @click="pptx">
PPTX
</button>
<button @click="pngsGz">
<button class="slidev-form-button" @click="pngsGz">
PNGs.gz
</button>
</div>
<div w-full h-1px border="t main" my2 />
<div class="relative flex flex-col gap-2 flex-nowrap">
<div class="flex flex-col gap-2 items-start min-w-max">
<button v-if="capturedImages" class="flex justify-center items-center gap-2" @click="capturedImages = null">
<div class="flex flex-col gap-2 min-w-max">
<button v-if="capturedImages" class="slidev-form-button flex justify-center items-center gap-2" @click="capturedImages = null">
<span class="i-carbon:trash-can inline-block text-xl" />
Clear Captured Images
</button>
<button v-else class="flex justify-center items-center gap-2" @click="capturePngs">
<button v-else class="slidev-form-button flex justify-center items-center gap-2" @click="capturePngs">
<div class="i-carbon:camera-action inline-block text-xl" />
Pre-capture Slides as Images
</button>
Expand Down Expand Up @@ -325,10 +325,6 @@ if (import.meta.hot) {
}
}
button {
--uno: 'w-full rounded bg-gray:10 px-4 py-2 hover:bg-gray/20';
}
label {
--uno: text-xl flex gap-2 items-center select-none;
Expand Down
Loading

0 comments on commit 56eec89

Please sign in to comment.