Skip to content

Commit 3ca226c

Browse files
committed
feat: introduce notes editor
1 parent 6979541 commit 3ca226c

File tree

3 files changed

+111
-1
lines changed

3 files changed

+111
-1
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script setup lang="ts">
2+
import type { SlideRoute } from '@slidev/types'
3+
import { useHead } from '@unhead/vue'
4+
import { debouncedWatch } from '@vueuse/core'
5+
import { ref } from 'vue'
6+
import { useNav } from '../composables/useNav'
7+
import { useDynamicSlideInfo } from '../composables/useSlideInfo'
8+
import { slidesTitle } from '../env'
9+
import IconButton from '../internals/IconButton.vue'
10+
import Modal from '../internals/Modal.vue'
11+
12+
useHead({ title: `Notes Edit - ${slidesTitle}` })
13+
14+
const { slides } = useNav()
15+
16+
const showHelp = ref(false)
17+
const note = ref(serializeNotes(slides.value))
18+
19+
function serializeNotes(slides: SlideRoute[]) {
20+
const lines: string[] = []
21+
22+
for (const slide of slides) {
23+
if (!slide.meta.slide.note?.trim())
24+
continue
25+
lines.push(`--- #${slide.no}`)
26+
lines.push('')
27+
lines.push(slide.meta.slide.note)
28+
lines.push('')
29+
}
30+
31+
return lines.join('\n')
32+
}
33+
34+
function deserializeNotes(notes: string, slides: SlideRoute[]) {
35+
const lines = notes.split(/^(---\s*#\d+\s*)$/gm)
36+
37+
lines.forEach((line, index) => {
38+
const match = line.match(/^---\s*#(\d+)\s*$/)
39+
if (match) {
40+
const no = Number.parseInt(match[1])
41+
const note = lines[index + 1].trim()
42+
const slide = slides.find(s => s.no === no)
43+
if (slide) {
44+
slide.meta.slide.note = note
45+
useDynamicSlideInfo(no).update({ note })
46+
}
47+
}
48+
})
49+
}
50+
51+
debouncedWatch(note, (value) => {
52+
deserializeNotes(value, slides.value)
53+
}, { debounce: 300 })
54+
</script>
55+
56+
<template>
57+
<Modal v-model="showHelp" class="px-6 py-4 flex flex-col gap-2">
58+
<div class="flex gap-2 text-xl">
59+
<div class="i-carbon:information my-auto" /> Help
60+
</div>
61+
<div class="prose dark:prose-invert">
62+
<p>This is the batch notes editor. You can edit the notes for all the slides at once here.</p>
63+
64+
<p>The note for each slide are separated by <code>--- #[no]</code> lines, you might want to keep them while editing.</p>
65+
</div>
66+
<div class="flex my-1">
67+
<button class="slidev-form-button" @click="showHelp = false">
68+
Close
69+
</button>
70+
</div>
71+
</Modal>
72+
<div class="h-full">
73+
<div class="slidev-glass-effect fixed bottom-5 right-5 rounded-full border border-main">
74+
<IconButton title="Help" class="rounded-full" @click="showHelp = true">
75+
<div class="i-carbon:help text-2xl" />
76+
</IconButton>
77+
</div>
78+
<textarea
79+
v-model="note"
80+
class="prose dark:prose-invert resize-none p5 outline-none bg-transparent block h-full w-full! max-w-full! max-h-full! min-h-full! min-w-full!"
81+
/>
82+
</div>
83+
</template>

packages/client/pages/notes.vue

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { computed, ref, watch } from 'vue'
55
import { createClicksContextBase } from '../composables/useClicks'
66
import { useNav } from '../composables/useNav'
77
import { slidesTitle } from '../env'
8-
98
import ClicksSlider from '../internals/ClicksSlider.vue'
109
import IconButton from '../internals/IconButton.vue'
10+
import Modal from '../internals/Modal.vue'
1111
import NoteDisplay from '../internals/NoteDisplay.vue'
1212
import { fullscreen } from '../state'
1313
import { sharedState } from '../state/shared'
@@ -20,6 +20,7 @@ const { isFullscreen, toggle: toggleFullscreen } = fullscreen
2020
const scroller = ref<HTMLDivElement>()
2121
const fontSize = useLocalStorage('slidev-notes-font-size', 18)
2222
const pageNo = computed(() => sharedState.page)
23+
const showHelp = ref(false)
2324
const currentRoute = computed(() => slides.value.find(i => i.no === pageNo.value))
2425
2526
watch(pageNo, () => {
@@ -43,6 +44,20 @@ const clicksContext = computed(() => {
4344
</script>
4445

4546
<template>
47+
<Modal v-model="showHelp" class="px-6 py-4 flex flex-col gap-2">
48+
<div class="flex gap-2 text-xl">
49+
<div class="i-carbon:information my-auto" /> Help
50+
</div>
51+
<div class="prose dark:prose-invert">
52+
<p>This is the hands-free live notes viewer.</p>
53+
<p>It's designed to be used in a separate view or device. The progress is controlled by and auto synced with the main presenter or slide.</p>
54+
</div>
55+
<div class="flex my-1">
56+
<button class="slidev-form-button" @click="showHelp = false">
57+
Close
58+
</button>
59+
</div>
60+
</Modal>
4661
<div
4762
class="fixed top-0 left-0 h-3px bg-primary transition-all duration-500"
4863
:style="{ width: `${(pageNo - 1) / (total - 1) * 100 + 1}%` }"
@@ -76,6 +91,12 @@ const clicksContext = computed(() => {
7691
<IconButton title="Decrease font size" @click="decreaseFontSize">
7792
<div class="i-carbon:zoom-out" />
7893
</IconButton>
94+
<IconButton title="Edit notes" to="/notes-edit" target="_blank">
95+
<div class="i-carbon:edit" />
96+
</IconButton>
97+
<IconButton title="Help" class="rounded-full" @click="showHelp = true">
98+
<div class="i-carbon:help" />
99+
</IconButton>
79100
<div class="flex-auto" />
80101
<div class="p2 text-center">
81102
{{ pageNo }} / {{ total }}

packages/client/setup/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export default function setupRoutes() {
3939
component: () => import('../pages/notes.vue'),
4040
beforeEnter: passwordGuard,
4141
},
42+
{
43+
name: 'notes-edit',
44+
path: '/notes-edit',
45+
component: () => import('../pages/notes-edit.vue'),
46+
beforeEnter: passwordGuard,
47+
},
4248
{
4349
name: 'presenter',
4450
path: '/presenter/:no',

0 commit comments

Comments
 (0)