Skip to content

Commit 35668bc

Browse files
committed
feat: editable notes in presenter mode
1 parent 05e57a0 commit 35668bc

File tree

7 files changed

+125
-36
lines changed

7 files changed

+125
-36
lines changed

packages/client/internals/Editor.vue

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { useEventListener, useFetch } from '@vueuse/core'
33
import { computed, watch, ref, onMounted, onUnmounted } from 'vue'
44
import { activeElement, showEditor } from '../state'
55
import { useCodeMirror } from '../setup/codemirror'
6-
import { currentRoute } from '../logic/nav'
6+
import { currentRoute, currentSlideId } from '../logic/nav'
7+
import { useDynamicSlideInfo } from '../logic/note'
78
89
const tab = ref<'content' | 'note'>('content')
910
const offsetRight = ref(0)
@@ -14,38 +15,27 @@ const frontmatter = ref<any>({})
1415
const contentInput = ref<HTMLTextAreaElement>()
1516
const noteInput = ref<HTMLTextAreaElement>()
1617
17-
const url = computed(() => `/@slidev/slide/${currentRoute.value?.meta?.slide?.id}.json`)
18-
const { data } = useFetch(url, { refetch: true }).get().json()
18+
const { info, update } = useDynamicSlideInfo(currentSlideId)
1919
2020
watch(
21-
data,
22-
() => {
23-
content.value = (data.value?.content || '').trim()
24-
note.value = (data.value?.note || '').trim()
25-
frontmatter.value = data.value?.frontmatter || {}
21+
info,
22+
(v) => {
23+
content.value = (v?.content || '').trim()
24+
note.value = (v?.note || '').trim()
25+
frontmatter.value = v?.frontmatter || {}
2626
dirty.value = false
2727
},
2828
{ immediate: true },
2929
)
3030
3131
async function save() {
32-
await fetch(
33-
url.value,
34-
{
35-
method: 'POST',
36-
headers: {
37-
'Accept': 'application/json',
38-
'Content-Type': 'application/json',
39-
},
40-
body: JSON.stringify({
41-
raw: null,
42-
note: note.value || undefined,
43-
content: content.value,
44-
frontmatter: frontmatter.value,
45-
}),
46-
},
47-
)
4832
dirty.value = false
33+
await update({
34+
raw: null!,
35+
note: note.value || undefined,
36+
content: content.value,
37+
frontmatter: frontmatter.value,
38+
})
4939
}
5040
5141
function close() {

packages/client/internals/NavControls.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ const dev = import.meta.env.DEV
103103
</a>
104104

105105
<button
106-
v-if="currentCamera !== 'none'"
106+
v-if="currentCamera !== 'none' && !isPresenter"
107107
class="icon-btn"
108108
:class="{'text-green-500': Boolean(showAvatar && streamCamera)}"
109109
title="Show camera view"
@@ -113,6 +113,7 @@ const dev = import.meta.env.DEV
113113
</button>
114114

115115
<div
116+
v-if="!isPresenter"
116117
ref="devicesList"
117118
class="flex relative"
118119
>

packages/client/internals/Presenter.vue

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script setup lang="ts">
22
import { useHead } from '@vueuse/head'
3-
import { useFetch } from '@vueuse/core'
4-
import { ref, computed, watchEffect } from 'vue'
5-
import { total, currentPage, currentRoute, nextRoute, tab, tabElements } from '../logic/nav'
3+
import { ignorableWatch } from '@vueuse/core'
4+
import { ref, computed, watch } from 'vue'
5+
import { total, currentPage, currentRoute, nextRoute, tab, tabElements, currentSlideId } from '../logic/nav'
66
import { showOverview } from '../state'
77
import SlideContainer from './SlideContainer.vue'
88
import NavControls from './NavControls.vue'
9+
import { useDynamicSlideInfo } from '../logic/note'
910
import SlidesOverview from './SlidesOverview.vue'
1011
// @ts-expect-error
1112
import configs from '/@slidev/configs'
@@ -14,10 +15,6 @@ useHead({
1415
title: configs.title ? `Presenter - ${configs.title} - Slidev` : 'Presenter - Slidev',
1516
})
1617
17-
watchEffect(() => {
18-
console.log(tabElements.value.length)
19-
})
20-
2118
const nextTabElements = ref([])
2219
const nextSlide = computed(() => {
2320
if (tab.value < tabElements.value.length) {
@@ -34,16 +31,42 @@ const nextSlide = computed(() => {
3431
}
3532
})
3633
37-
const url = computed(() => `/@slidev/slide/${currentRoute.value?.meta?.slide?.id}.json`)
38-
const { data } = useFetch(url, { refetch: true }).get().json()
34+
const { info, update } = useDynamicSlideInfo(currentSlideId)
35+
36+
const note = ref('')
37+
let timer: any
38+
39+
const { ignoreUpdates } = ignorableWatch(
40+
note,
41+
(v) => {
42+
const id = currentSlideId.value
43+
clearTimeout(timer)
44+
timer = setTimeout(() => {
45+
update({ raw: null!, note: v }, id)
46+
}, 500)
47+
},
48+
)
49+
50+
watch(
51+
info,
52+
(v) => {
53+
clearTimeout(timer)
54+
ignoreUpdates(() => {
55+
note.value = v?.note || ''
56+
})
57+
},
58+
{ immediate: true, flush: 'sync' },
59+
)
3960
</script>
4061

4162
<template>
4263
<div class="grid-container">
4364
<div class="grid-section top flex">
4465
<img src="../assets/logo-title-horizontal.png" class="h-14 ml-2 py-2 my-auto" />
4566
<div class="flex-auto" />
46-
<div class="px-4 my-auto">{{ currentPage + 1 }} / {{ total }}</div>
67+
<div class="px-4 my-auto">
68+
{{ currentPage + 1 }} / {{ total }}
69+
</div>
4770
</div>
4871
<div class="grid-section main flex flex-col p-4">
4972
<SlideContainer
@@ -67,8 +90,8 @@ const { data } = useFetch(url, { refetch: true }).get().json()
6790
</div>
6891
<div class="grid-section note">
6992
<textarea
93+
v-model="note"
7094
class="w-full h-full p-4 resize-none overflow-auto outline-none bg-transparent"
71-
:value="data?.note"
7295
placeholder="No notes for this slide"
7396
/>
7497
</div>

packages/client/logic/nav.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ export const isPresenter = computed(() => route.value.path.startsWith('/presente
1212

1313
export const total = computed(() => rawRoutes.length)
1414
export const path = computed(() => route.value.path)
15+
1516
export const currentPage = computed(() => parseInt(path.value.split(/\//g).slice(-1)[0]) || 0)
1617
export const currentPath = computed(() => getPath(currentPage.value))
1718
export const currentRoute = computed(() => rawRoutes.find(i => i.path === `${currentPage.value}`))
19+
export const currentSlideId = computed(() => currentRoute.value?.meta?.slide?.id)
20+
1821
export const hasNext = computed(() => currentPage.value < rawRoutes.length - 1)
1922
export const hasPrev = computed(() => currentPage.value > 0)
2023
export const nextRoute = computed(() => rawRoutes.find(i => i.path === `${Math.min(rawRoutes.length - 1, currentPage.value + 1)}`))

packages/client/logic/note.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { MaybeRef, useFetch } from '@vueuse/core'
2+
import { computed, ref, Ref, unref } from 'vue'
3+
import type { SlideInfo } from '../../slidev/node/parser'
4+
5+
export interface UseSlideInfo{
6+
info: Ref<SlideInfo | undefined>
7+
update: (data: Partial<SlideInfo>) => Promise<void>
8+
}
9+
10+
export function useSlideInfo(id: number | undefined): UseSlideInfo {
11+
if (id == null) {
12+
return {
13+
info: ref() as Ref<SlideInfo | undefined>,
14+
update: async() => {},
15+
}
16+
}
17+
const url = `/@slidev/slide/${id}.json`
18+
const { data: info, execute } = useFetch<SlideInfo>(url).get().json()
19+
20+
execute()
21+
22+
const update = async(data: Partial<SlideInfo>) => {
23+
await fetch(
24+
url,
25+
{
26+
method: 'POST',
27+
headers: {
28+
'Accept': 'application/json',
29+
'Content-Type': 'application/json',
30+
},
31+
body: JSON.stringify(data),
32+
},
33+
)
34+
}
35+
36+
import.meta.hot?.on('slidev-update', (playload) => {
37+
if (playload.id === id)
38+
info.value = playload.data
39+
})
40+
41+
return {
42+
info,
43+
update,
44+
}
45+
}
46+
47+
const map: Record<string, UseSlideInfo> = {}
48+
49+
export function useDynamicSlideInfo(id: MaybeRef<number | undefined>) {
50+
function get(id: number | undefined) {
51+
const i = `${id}`
52+
if (!map[i])
53+
map[i] = useSlideInfo(id)
54+
return map[i]
55+
}
56+
57+
return {
58+
info: computed(() => get(unref(id)).info.value),
59+
update: (data: Partial<SlideInfo>, newId?: number) => get(newId ?? unref(id)).update(data),
60+
}
61+
}

packages/slidev/node/plugins/loaders.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export function createSlidesLoader({ data, entry, clientRoot, themeRoots, userRo
7171
`${slidePrefix}${idx}.json`,
7272
)
7373

74+
server.ws.send({
75+
type: 'custom',
76+
event: 'slidev-update',
77+
data: {
78+
id: idx,
79+
data: data.slides[idx],
80+
},
81+
})
82+
7483
await parser.save(data, entry)
7584

7685
res.statusCode = 200

tsconfig.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
"include": [
2424
"docs/.vitepress/**/*.ts",
2525
"docs/.vitepress/**/*.vue",
26+
"packages/**/*.ts",
27+
"packages/**/*.vue"
2628
],
2729
"exclude": ["**/dist/**", "node_modules"]
2830
}

0 commit comments

Comments
 (0)