Skip to content

Commit 53b7a72

Browse files
committed
feat: recording dialog
1 parent b335308 commit 53b7a72

File tree

8 files changed

+190
-12
lines changed

8 files changed

+190
-12
lines changed
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script setup lang="ts">
2-
import { showOverview } from '../state'
2+
import { showOverview, showRecordingDialog } from '../state'
33
import SlidesOverview from './SlidesOverview.vue'
44
import WebCamera from './WebCamera.vue'
5+
import RecordingDialog from './RecordingDialog.vue'
56
</script>
67

78
<template>
89
<SlidesOverview v-model="showOverview" />
910
<WebCamera />
11+
<RecordingDialog v-model="showRecordingDialog"/>
1012
</template>

packages/client/internals/Modal.vue

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<script setup lang="ts">
2+
import { useVModel } from '@vueuse/core'
3+
import { defineEmit, defineProps, shallowRef } from 'vue'
4+
5+
const emit = defineEmit()
6+
const props = defineProps({
7+
modelValue: {
8+
default: false,
9+
},
10+
class: {
11+
default: '',
12+
},
13+
})
14+
15+
const value = useVModel(props, 'modelValue', emit)
16+
const container = shallowRef<HTMLDivElement>()
17+
18+
function onClick(e: MouseEvent) {
19+
if (e?.target === container.value)
20+
value.value = false
21+
}
22+
</script>
23+
24+
<template>
25+
<div
26+
v-show="value"
27+
ref="container"
28+
class="fixed top-0 bottom-0 left-0 right-0 bg-black bg-opacity-80 grid"
29+
@click="onClick"
30+
>
31+
<div
32+
class="m-auto bg-main rounded shadow"
33+
:class="props.class"
34+
>
35+
<slot />
36+
</div>
37+
</div>
38+
</template>

packages/client/internals/NavControls.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { computed, defineProps, ref } from 'vue'
44
import { isDark, toggleDark } from '../logic/dark'
55
import { recorder } from '../logic/recording'
66
import { hasNext, hasPrev, prev, next, isPresenter, currentPage } from '../logic/nav'
7-
import { showOverview, showEditor, currentCamera } from '../state'
7+
import { showOverview, showEditor, currentCamera, showRecordingDialog } from '../state'
88
import DevicesList from './DevicesList.vue'
99
1010
defineProps({
@@ -19,7 +19,7 @@ const {
1919
recording,
2020
showAvatar,
2121
streamCamera,
22-
toggleRecording,
22+
stopRecording,
2323
toggleAvatar,
2424
} = recorder
2525
@@ -34,6 +34,13 @@ const presenterLink = computed(() => `${location.origin}/presenter/${currentPage
3434
const nonPresenterLink = computed(() => `${location.origin}/${currentPage.value}`)
3535
3636
const dev = import.meta.env.DEV
37+
38+
function toggleRecording() {
39+
if (recording.value)
40+
stopRecording()
41+
else
42+
showRecordingDialog.value = true
43+
}
3744
</script>
3845

3946
<template>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script setup lang="ts">
2+
import { useVModel } from '@vueuse/core'
3+
import { defineEmit, defineProps, nextTick, ref } from 'vue'
4+
import { recorder, recordingName, recordCamera, getFilename } from '../logic/recording'
5+
import Modal from './Modal.vue'
6+
import DevicesList from './DevicesList.vue'
7+
8+
const emit = defineEmit()
9+
const props = defineProps({
10+
modelValue: {
11+
default: false,
12+
},
13+
})
14+
15+
const value = useVModel(props, 'modelValue', emit)
16+
17+
const { startRecording } = recorder
18+
19+
function close() {
20+
value.value = false
21+
}
22+
23+
async function start() {
24+
close()
25+
await nextTick()
26+
startRecording()
27+
}
28+
</script>
29+
30+
<template>
31+
<Modal v-model="value" class="p-4 recording-dialog flex flex-col gap-2">
32+
<div class="flex leading-5 gap-2">
33+
<carbon:video />Recording
34+
</div>
35+
<div class="grid grid-cols-2 gap-4">
36+
<div class="flex flex-col gap-2 py-2">
37+
<div class="form-text">
38+
<label for="title">Recording Name</label>
39+
<input
40+
v-model="recordingName"
41+
name="title"
42+
type="text"
43+
placeholder="Enter the title..."
44+
/>
45+
<div class="text-xs w-full opacity-50 py-2">
46+
<div>This will be used in the output filename that might <br>help you better orangize your recording chips.</div>
47+
</div>
48+
</div>
49+
<div class="form-check">
50+
<input
51+
v-model="recordCamera"
52+
name="record-camera"
53+
type="checkbox"
54+
/>
55+
<label for="record-camera" @click="recordCamera = !recordCamera">Record camera sperately</label>
56+
</div>
57+
<div class="text-xs w-full opacity-50">
58+
<div class="mt-2 opacity-50">Enumated filenames</div>
59+
<div class="font-mono">
60+
{{ getFilename('screen') }}
61+
</div>
62+
<div class="font-mono" v-if="recordCamera">
63+
{{ getFilename('camera') }}
64+
</div>
65+
</div>
66+
</div>
67+
<DevicesList />
68+
</div>
69+
<div class="flex">
70+
<button class="cancel" @click="close">
71+
Cancel
72+
</button>
73+
<div class="flex-auto"></div>
74+
<button @click="start">
75+
Start
76+
</button>
77+
</div>
78+
</Modal>
79+
</template>
80+
81+
<style lang="postcss">
82+
.recording-dialog {
83+
84+
.form-text {
85+
@apply flex flex-col;
86+
87+
label {
88+
@apply text-xs uppercase opacity-50 tracking-widest py-1;
89+
}
90+
}
91+
92+
.form-check {
93+
@apply leading-5;
94+
95+
* {
96+
@apply my-auto align-middle;
97+
}
98+
99+
label {
100+
@apply ml-1 text-sm select-none;
101+
}
102+
}
103+
104+
input[type="text"] {
105+
@apply border broder-gray-400 rounded px-2 py-1;
106+
}
107+
108+
button {
109+
@apply bg-orange-400 text-white px-4 py-1 rounded border-b-2 border-orange-600;
110+
@apply hover:(bg-orange-500 border-orange-700)
111+
}
112+
113+
button.cancel {
114+
@apply bg-gray-400 text-white px-4 py-1 rounded border-b-2 border-gray-500;
115+
@apply bg-opacity-50 border-opacity-50;
116+
@apply hover:(bg-opacity-75 border-opacity-75)
117+
}
118+
}
119+
</style>

packages/client/logic/recording.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,22 @@ import { Ref, ref, shallowRef, watch } from 'vue'
22
import Recorder from 'recordrtc'
33
import type { Options as RecorderOptions } from 'recordrtc'
44
import { useEventListener, useDevicesList } from '@vueuse/core'
5+
import { isTruthy } from '@antfu/utils'
56
import { currentCamera, currentMic } from '../state'
67

8+
export const recordingName = ref('')
9+
export const recordCamera = ref(true)
10+
11+
export function getFilename(media?: string) {
12+
const d = new Date()
13+
14+
const pad = (v: number) => `${v}`.padStart(2, '0')
15+
16+
const date = `${pad(d.getMonth() + 1)}${pad(d.getDate())}-${pad(d.getHours())}${pad(d.getMinutes())}`
17+
18+
return `${[media, recordingName.value, date].filter(isTruthy).join('-')}.webm`
19+
}
20+
721
export const {
822
devices,
923
videoInputs: cameras,
@@ -66,7 +80,7 @@ export function useRecording() {
6680
return
6781

6882
streamCamera.value = await navigator.mediaDevices.getUserMedia({
69-
video: currentCamera.value === 'none'
83+
video: currentCamera.value === 'none' || recordCamera.value === false
7084
? false
7185
: {
7286
deviceId: currentCamera.value,
@@ -147,15 +161,15 @@ export function useRecording() {
147161
recorderCamera.value?.stopRecording(() => {
148162
const blob = recorderCamera.value!.getBlob()
149163
const url = URL.createObjectURL(blob)
150-
download(`camera-${new Date().toLocaleTimeString().replace(/[:\s_]/g, '-')}.webm`, url)
164+
download(getFilename('camera'), url)
151165
window.URL.revokeObjectURL(url)
152166
closeStream(streamCamera)
153167
recorderCamera.value = undefined
154168
})
155169
recorderSlides.value?.stopRecording(() => {
156170
const blob = recorderSlides.value!.getBlob()
157171
const url = URL.createObjectURL(blob)
158-
download(`slides-${new Date().toLocaleTimeString().replace(/[:\s_]/g, '-')}.webm`, url)
172+
download(getFilename('screen'), url)
159173
window.URL.revokeObjectURL(url)
160174
closeCameraStream()
161175
recorderSlides.value = undefined

packages/client/state/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed, ref } from 'vue'
33

44
export const showOverview = ref(false)
55
export const showEditor = ref(false)
6+
export const showRecordingDialog = ref(false)
67

78
export const query = useUrlSearchParams()
89
export const breakpoints = useBreakpoints(breakpointsTailwind)

packages/slidev/node/common.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { promises as fs, existsSync } from 'fs'
22
import { join, posix } from 'path'
3+
import { uniq } from '@antfu/utils'
34
import { ResolvedSlidevOptions } from './plugins/options'
45

56
export async function getIndexHtml({ clientRoot, themeRoots, userRoot }: ResolvedSlidevOptions): Promise<string> {
67
let main = await fs.readFile(join(clientRoot, 'index.html'), 'utf-8')
78
let head = ''
89
let body = ''
910

10-
const roots = [
11+
const roots = uniq([
1112
...themeRoots,
1213
userRoot,
13-
]
14+
])
1415

1516
for (const root of roots) {
1617
const path = join(root, 'index.html')

packages/theme-default/index.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,3 @@
22
<link rel="preconnect" href="https://fonts.gstatic.com">
33
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Nunito+Sans:wght@200;400;600&display=swap" rel="stylesheet">
44
</head>
5-
6-
<body>
7-
<script src="./your-scripts"></script>
8-
</body>

0 commit comments

Comments
 (0)