@slidev/client 52.4.0 → 52.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composables/useTimer.ts +92 -9
- package/internals/CurrentProgressBar.vue +34 -0
- package/internals/NavControls.vue +3 -5
- package/internals/TimerBar.vue +49 -0
- package/internals/TimerInlined.vue +53 -0
- package/package.json +6 -6
- package/pages/notes.vue +9 -8
- package/pages/presenter.vue +10 -36
- package/state/shared.ts +16 -0
package/composables/useTimer.ts
CHANGED
|
@@ -1,20 +1,103 @@
|
|
|
1
|
+
import { parseTimeString } from '@slidev/parser/utils'
|
|
1
2
|
import { useInterval } from '@vueuse/core'
|
|
2
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, toRef } from 'vue'
|
|
4
|
+
import { configs } from '../env'
|
|
5
|
+
import { sharedState } from '../state/shared'
|
|
3
6
|
|
|
4
7
|
export function useTimer() {
|
|
5
|
-
const
|
|
8
|
+
const mode = computed(() => configs.timer || 'stopwatch')
|
|
9
|
+
const duration = computed(() => parseTimeString(configs.duration).seconds)
|
|
10
|
+
const interval = useInterval(100, { controls: true })
|
|
11
|
+
|
|
12
|
+
const state = toRef(sharedState, 'timer')
|
|
13
|
+
const status = computed(() => state.value?.status)
|
|
14
|
+
const passedMs = computed(() => {
|
|
15
|
+
// eslint-disable-next-line ts/no-unused-expressions
|
|
16
|
+
interval.counter.value
|
|
17
|
+
if (state.value.status === 'stopped' || !state.value.startedAt)
|
|
18
|
+
return 0
|
|
19
|
+
return Date.now() - state.value.startedAt
|
|
20
|
+
})
|
|
21
|
+
const passed = computed(() => passedMs.value / 1000)
|
|
22
|
+
const percentage = computed(() => passed.value / duration.value * 100)
|
|
6
23
|
|
|
7
24
|
const timer = computed(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
if (mode.value === 'stopwatch') {
|
|
26
|
+
if (state.value.status === 'stopped' || !state.value.startedAt)
|
|
27
|
+
return { h: '', m: '-', s: '--', ms: '-' }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const total = mode.value === 'countdown'
|
|
31
|
+
? duration.value * 1000 - passedMs.value
|
|
32
|
+
: passedMs.value
|
|
33
|
+
|
|
34
|
+
let h = Math.floor(total / 1000 / 60 / 60).toString()
|
|
35
|
+
if (h === '0')
|
|
36
|
+
h = ''
|
|
37
|
+
let min = Math.floor(total / 1000 / 60 % 60).toString()
|
|
38
|
+
if (h)
|
|
39
|
+
min = min.padStart(2, '0')
|
|
40
|
+
const sec = Math.floor(total / 1000 % 60).toString().padStart(2, '0')
|
|
41
|
+
const ms = Math.floor(total % 1000 / 100).toString()
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
h,
|
|
45
|
+
m: min,
|
|
46
|
+
s: sec,
|
|
47
|
+
ms,
|
|
48
|
+
}
|
|
12
49
|
})
|
|
13
50
|
|
|
51
|
+
function reset() {
|
|
52
|
+
interval.pause()
|
|
53
|
+
state.value = {
|
|
54
|
+
status: 'stopped',
|
|
55
|
+
slides: {},
|
|
56
|
+
startedAt: 0,
|
|
57
|
+
pausedAt: 0,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resume() {
|
|
62
|
+
if (!state.value)
|
|
63
|
+
return
|
|
64
|
+
if (state.value?.status === 'stopped') {
|
|
65
|
+
state.value.status = 'running'
|
|
66
|
+
state.value.startedAt = Date.now()
|
|
67
|
+
}
|
|
68
|
+
else if (state.value.status === 'paused') {
|
|
69
|
+
state.value.status = 'running'
|
|
70
|
+
state.value.startedAt = Date.now() - (state.value.pausedAt - state.value.startedAt)
|
|
71
|
+
}
|
|
72
|
+
interval.resume()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pause() {
|
|
76
|
+
state.value.status = 'paused'
|
|
77
|
+
state.value.pausedAt = Date.now()
|
|
78
|
+
interval.pause()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toggle() {
|
|
82
|
+
if (state.value.status === 'running') {
|
|
83
|
+
pause()
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
resume()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
14
90
|
return {
|
|
91
|
+
state,
|
|
92
|
+
status,
|
|
15
93
|
timer,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
94
|
+
reset,
|
|
95
|
+
toggle,
|
|
96
|
+
resume,
|
|
97
|
+
pause,
|
|
98
|
+
passed,
|
|
99
|
+
percentage,
|
|
100
|
+
duration,
|
|
101
|
+
mode,
|
|
19
102
|
}
|
|
20
103
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { useNav } from '../composables/useNav'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
clicksContext?: ClicksContext
|
|
8
|
+
current?: number
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const nav = useNav()
|
|
12
|
+
const clicksContext = computed(() => props.clicksContext ?? nav.clicksContext.value)
|
|
13
|
+
const current = computed(() => props.current ?? nav.currentSlideNo.value)
|
|
14
|
+
const { total } = nav
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<div class="relative flex gap-px">
|
|
19
|
+
<div
|
|
20
|
+
v-for="i of total - 1"
|
|
21
|
+
:key="i" class="border-x border-b border-main h-4px transition-all"
|
|
22
|
+
:style="{ width: `${(1 / (total - 1) * 100)}%` }"
|
|
23
|
+
:class="i < current ? 'bg-primary border-primary' : ''"
|
|
24
|
+
>
|
|
25
|
+
<Transition name="fade">
|
|
26
|
+
<div
|
|
27
|
+
v-if="i === current"
|
|
28
|
+
class="h-full bg-primary op75 transition-all"
|
|
29
|
+
:style="{ width: `${clicksContext.total === 0 ? 0 : clicksContext.current / (clicksContext.total + 1) * 100}%` }"
|
|
30
|
+
/>
|
|
31
|
+
</Transition>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
@@ -178,11 +178,9 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
178
178
|
|
|
179
179
|
<VerticalDivider v-if="!isEmbedded" />
|
|
180
180
|
|
|
181
|
-
<div class="
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
<span class="opacity-50">/ {{ total }}</span>
|
|
185
|
-
</div>
|
|
181
|
+
<div class="px2 my-auto">
|
|
182
|
+
<span class="text-lg">{{ currentSlideNo }}</span>
|
|
183
|
+
<span class="opacity-50 text-sm"> / {{ total }}</span>
|
|
186
184
|
</div>
|
|
187
185
|
|
|
188
186
|
<CustomNavControls />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// import { parseTimesplits } from '@slidev/parser/utils'
|
|
3
|
+
import { computed, reactive } from 'vue'
|
|
4
|
+
// import { useNav } from '../composables/useNav'
|
|
5
|
+
import { useTimer } from '../composables/useTimer'
|
|
6
|
+
|
|
7
|
+
// const { slides } = useNav()
|
|
8
|
+
|
|
9
|
+
const timer = reactive(useTimer())
|
|
10
|
+
// TODO: timesplit
|
|
11
|
+
// const slidesWithTimesplits = computed(() => slides.value.filter(i => i.meta.slide?.frontmatter.timesplit))
|
|
12
|
+
|
|
13
|
+
// const _timesplits = computed(() => {
|
|
14
|
+
// const parsed = parseTimesplits(
|
|
15
|
+
// slidesWithTimesplits.value
|
|
16
|
+
// .map(i => ({ no: i.no, timesplit: i.meta.slide?.frontmatter.timesplit as string })),
|
|
17
|
+
// )
|
|
18
|
+
// return parsed
|
|
19
|
+
// })
|
|
20
|
+
|
|
21
|
+
// TODO: maybe make it configurable, or somehow more smart
|
|
22
|
+
const color = computed(() => {
|
|
23
|
+
if (timer.status === 'stopped')
|
|
24
|
+
return 'op50'
|
|
25
|
+
if (timer.status === 'paused')
|
|
26
|
+
return 'bg-blue'
|
|
27
|
+
|
|
28
|
+
if (timer.percentage > 80)
|
|
29
|
+
return 'bg-yellow'
|
|
30
|
+
else if (timer.percentage > 100)
|
|
31
|
+
return 'bg-red'
|
|
32
|
+
else
|
|
33
|
+
return 'bg-green'
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<div
|
|
39
|
+
class="border-b mt-px border-main relative flex h-4px"
|
|
40
|
+
>
|
|
41
|
+
<div
|
|
42
|
+
v-if="timer.status !== 'stopped'"
|
|
43
|
+
class="h-4px"
|
|
44
|
+
:class="color"
|
|
45
|
+
:style="{ width: `${timer.percentage}%` }"
|
|
46
|
+
/>
|
|
47
|
+
<!-- {{ timesplits }} -->
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useTimer } from '../composables/useTimer'
|
|
4
|
+
|
|
5
|
+
const { status, percentage, mode, timer, reset, toggle } = useTimer()
|
|
6
|
+
|
|
7
|
+
const color = computed(() => {
|
|
8
|
+
if (status.value === 'stopped')
|
|
9
|
+
return 'op50'
|
|
10
|
+
if (status.value === 'paused')
|
|
11
|
+
return 'text-blue6 dark:text-blue3'
|
|
12
|
+
|
|
13
|
+
if (percentage.value > 80)
|
|
14
|
+
return 'text-yellow6 dark:text-yellow3'
|
|
15
|
+
else if (percentage.value > 100)
|
|
16
|
+
return 'text-red6 dark:text-red3'
|
|
17
|
+
else
|
|
18
|
+
return 'text-green6 dark:text-green3'
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div
|
|
24
|
+
class="group flex items-center justify-center pl-4 select-none"
|
|
25
|
+
:class="color"
|
|
26
|
+
>
|
|
27
|
+
<div class="w-22px cursor-pointer">
|
|
28
|
+
<div
|
|
29
|
+
class="group-hover:hidden text-2xl"
|
|
30
|
+
:class="mode === 'countdown' ? 'i-carbon:timer' : 'i-carbon:time'"
|
|
31
|
+
/>
|
|
32
|
+
<div class="group-not-hover:hidden flex flex-col items-center">
|
|
33
|
+
<div class="relative op-80 hover:op-100" @click="toggle">
|
|
34
|
+
<div v-if="status === 'running'" class="i-carbon:pause text-lg" />
|
|
35
|
+
<div v-else class="i-carbon:play" />
|
|
36
|
+
</div>
|
|
37
|
+
<div class="op-80 hover:op-100" @click="reset">
|
|
38
|
+
<div class="i-carbon:renew" />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="text-3xl px-3 my-auto font-mono">
|
|
43
|
+
<template v-if="timer.h">
|
|
44
|
+
<span>{{ timer.h }}</span>
|
|
45
|
+
<span op50>:</span>
|
|
46
|
+
</template>
|
|
47
|
+
<span>{{ timer.m }}</span>
|
|
48
|
+
<span op50>:</span>
|
|
49
|
+
<span>{{ timer.s }}</span>
|
|
50
|
+
<span class="text-base op50">.{{ timer.ms }}</span>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "52.
|
|
4
|
+
"version": "52.6.0",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@typescript/ata": "^0.9.8",
|
|
40
40
|
"@unhead/vue": "^2.0.19",
|
|
41
41
|
"@unocss/reset": "^66.5.4",
|
|
42
|
-
"@vueuse/core": "^
|
|
43
|
-
"@vueuse/math": "^
|
|
42
|
+
"@vueuse/core": "^14.0.0",
|
|
43
|
+
"@vueuse/math": "^14.0.0",
|
|
44
44
|
"@vueuse/motion": "^3.0.3",
|
|
45
45
|
"drauu": "^0.4.3",
|
|
46
46
|
"file-saver": "^2.0.5",
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
"vue": "^3.5.22",
|
|
62
62
|
"vue-router": "^4.6.3",
|
|
63
63
|
"yaml": "^2.8.1",
|
|
64
|
-
"@slidev/parser": "52.
|
|
65
|
-
"@slidev/types": "52.
|
|
64
|
+
"@slidev/parser": "52.6.0",
|
|
65
|
+
"@slidev/types": "52.6.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"vite": "^7.1.
|
|
68
|
+
"vite": "^7.1.12"
|
|
69
69
|
}
|
|
70
70
|
}
|
package/pages/notes.vue
CHANGED
|
@@ -6,9 +6,11 @@ import { createClicksContextBase } from '../composables/useClicks'
|
|
|
6
6
|
import { useNav } from '../composables/useNav'
|
|
7
7
|
import { slidesTitle } from '../env'
|
|
8
8
|
import ClicksSlider from '../internals/ClicksSlider.vue'
|
|
9
|
+
import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
|
|
9
10
|
import IconButton from '../internals/IconButton.vue'
|
|
10
11
|
import Modal from '../internals/Modal.vue'
|
|
11
12
|
import NoteDisplay from '../internals/NoteDisplay.vue'
|
|
13
|
+
import TimerBar from '../internals/TimerBar.vue'
|
|
12
14
|
import { fullscreen } from '../state'
|
|
13
15
|
import { sharedState } from '../state/shared'
|
|
14
16
|
|
|
@@ -58,14 +60,12 @@ const clicksContext = computed(() => {
|
|
|
58
60
|
</button>
|
|
59
61
|
</div>
|
|
60
62
|
</Modal>
|
|
61
|
-
<div
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/>
|
|
65
|
-
<div class="h-full pt-2 flex flex-col">
|
|
63
|
+
<div class="h-full flex flex-col">
|
|
64
|
+
<CurrentProgressBar :clicks-context="clicksContext" :current="pageNo" />
|
|
65
|
+
<TimerBar />
|
|
66
66
|
<div
|
|
67
67
|
ref="scroller"
|
|
68
|
-
class="px-5 flex-auto h-full overflow-auto"
|
|
68
|
+
class="px-5 py-3 flex-auto h-full overflow-auto"
|
|
69
69
|
:style="{ fontSize: `${fontSize}px` }"
|
|
70
70
|
>
|
|
71
71
|
<NoteDisplay
|
|
@@ -98,8 +98,9 @@ const clicksContext = computed(() => {
|
|
|
98
98
|
<div class="i-carbon:help" />
|
|
99
99
|
</IconButton>
|
|
100
100
|
<div class="flex-auto" />
|
|
101
|
-
<div class="
|
|
102
|
-
{{ pageNo }}
|
|
101
|
+
<div class="px2 my-auto">
|
|
102
|
+
<span class="text-lg">{{ pageNo }}</span>
|
|
103
|
+
<span class="opacity-50 text-sm"> / {{ total }}</span>
|
|
103
104
|
</div>
|
|
104
105
|
</div>
|
|
105
106
|
</div>
|
package/pages/presenter.vue
CHANGED
|
@@ -6,11 +6,11 @@ import { createClicksContextBase } from '../composables/useClicks'
|
|
|
6
6
|
import { useDrawings } from '../composables/useDrawings'
|
|
7
7
|
import { useNav } from '../composables/useNav'
|
|
8
8
|
import { useSwipeControls } from '../composables/useSwipeControls'
|
|
9
|
-
import { useTimer } from '../composables/useTimer'
|
|
10
9
|
import { useWakeLock } from '../composables/useWakeLock'
|
|
11
10
|
import { slidesTitle } from '../env'
|
|
12
11
|
import ClicksSlider from '../internals/ClicksSlider.vue'
|
|
13
12
|
import ContextMenu from '../internals/ContextMenu.vue'
|
|
13
|
+
import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
|
|
14
14
|
import DrawingControls from '../internals/DrawingControls.vue'
|
|
15
15
|
import Goto from '../internals/Goto.vue'
|
|
16
16
|
import IconButton from '../internals/IconButton.vue'
|
|
@@ -23,6 +23,8 @@ import SegmentControl from '../internals/SegmentControl.vue'
|
|
|
23
23
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
24
24
|
import SlidesShow from '../internals/SlidesShow.vue'
|
|
25
25
|
import SlideWrapper from '../internals/SlideWrapper.vue'
|
|
26
|
+
import TimerBar from '../internals/TimerBar.vue'
|
|
27
|
+
import TimerInlined from '../internals/TimerInlined.vue'
|
|
26
28
|
import { onContextMenu } from '../logic/contextMenu'
|
|
27
29
|
import { registerShortcuts } from '../logic/shortcuts'
|
|
28
30
|
import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
|
|
@@ -44,7 +46,6 @@ const {
|
|
|
44
46
|
nextRoute,
|
|
45
47
|
slides,
|
|
46
48
|
getPrimaryClicks,
|
|
47
|
-
total,
|
|
48
49
|
} = useNav()
|
|
49
50
|
const { isDrawing } = useDrawings()
|
|
50
51
|
|
|
@@ -52,8 +53,6 @@ useHead({ title: `Presenter - ${slidesTitle}` })
|
|
|
52
53
|
|
|
53
54
|
const notesEditing = ref(false)
|
|
54
55
|
|
|
55
|
-
const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer()
|
|
56
|
-
|
|
57
56
|
const clicksCtxMap = computed(() => slides.value.map((route) => {
|
|
58
57
|
const clicks = ref(0)
|
|
59
58
|
return {
|
|
@@ -116,7 +115,11 @@ onMounted(() => {
|
|
|
116
115
|
</script>
|
|
117
116
|
|
|
118
117
|
<template>
|
|
119
|
-
<div class="bg-main h-full slidev-presenter
|
|
118
|
+
<div class="bg-main h-full slidev-presenter grid grid-rows-[max-content_1fr] of-hidden">
|
|
119
|
+
<div>
|
|
120
|
+
<CurrentProgressBar />
|
|
121
|
+
<TimerBar />
|
|
122
|
+
</div>
|
|
120
123
|
<div class="grid-container" :class="`layout${presenterLayout}`">
|
|
121
124
|
<div ref="main" class="relative grid-section main flex flex-col">
|
|
122
125
|
<div flex="~ gap-4 items-center" border="b main" p1>
|
|
@@ -210,32 +213,10 @@ onMounted(() => {
|
|
|
210
213
|
<div class="grid-section bottom flex">
|
|
211
214
|
<NavControls :persist="true" class="transition" :class="inFocus ? '' : 'op25'" />
|
|
212
215
|
<div flex-auto />
|
|
213
|
-
<
|
|
214
|
-
<div class="w-22px cursor-pointer">
|
|
215
|
-
<div class="i-carbon:time group-hover:hidden text-xl" />
|
|
216
|
-
<div class="group-not-hover:hidden flex flex-col items-center">
|
|
217
|
-
<div class="relative op-80 hover:op-100" @click="toggleTimer">
|
|
218
|
-
<div v-if="isTimerActive" class="i-carbon:pause text-lg" />
|
|
219
|
-
<div v-else class="i-carbon:play" />
|
|
220
|
-
</div>
|
|
221
|
-
<div class="op-80 hover:op-100" @click="resetTimer">
|
|
222
|
-
<div class="i-carbon:renew" />
|
|
223
|
-
</div>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
<div class="text-2xl px-3 my-auto font-mono">
|
|
227
|
-
{{ timer }}
|
|
228
|
-
</div>
|
|
229
|
-
</div>
|
|
216
|
+
<TimerInlined />
|
|
230
217
|
</div>
|
|
231
218
|
<DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
|
|
232
219
|
</div>
|
|
233
|
-
<div class="progress-bar">
|
|
234
|
-
<div
|
|
235
|
-
class="progress h-3px bg-primary transition-all"
|
|
236
|
-
:style="{ width: `${(currentSlideNo - 1) / (total - 1) * 100 + 1}%` }"
|
|
237
|
-
/>
|
|
238
|
-
</div>
|
|
239
220
|
</div>
|
|
240
221
|
<Goto />
|
|
241
222
|
<QuickOverview />
|
|
@@ -248,9 +229,7 @@ onMounted(() => {
|
|
|
248
229
|
}
|
|
249
230
|
|
|
250
231
|
.grid-container {
|
|
251
|
-
--uno: bg-gray/20;
|
|
252
|
-
height: 100%;
|
|
253
|
-
width: 100%;
|
|
232
|
+
--uno: bg-gray/20 flex-1 of-hidden;
|
|
254
233
|
display: grid;
|
|
255
234
|
gap: 1px 1px;
|
|
256
235
|
}
|
|
@@ -305,14 +284,9 @@ onMounted(() => {
|
|
|
305
284
|
}
|
|
306
285
|
}
|
|
307
286
|
|
|
308
|
-
.progress-bar {
|
|
309
|
-
--uno: fixed left-0 right-0 top-0;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
287
|
.grid-section {
|
|
313
288
|
--uno: bg-main;
|
|
314
289
|
}
|
|
315
|
-
|
|
316
290
|
.grid-section.top {
|
|
317
291
|
grid-area: top;
|
|
318
292
|
}
|
package/state/shared.ts
CHANGED
|
@@ -7,6 +7,16 @@ export interface SharedState {
|
|
|
7
7
|
clicks: number
|
|
8
8
|
clicksTotal: number
|
|
9
9
|
|
|
10
|
+
timer: {
|
|
11
|
+
status: 'stopped' | 'running' | 'paused'
|
|
12
|
+
slides: Record<number, {
|
|
13
|
+
start?: number
|
|
14
|
+
end?: number
|
|
15
|
+
}>
|
|
16
|
+
startedAt: number
|
|
17
|
+
pausedAt: number
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
cursor?: {
|
|
11
21
|
x: number
|
|
12
22
|
y: number
|
|
@@ -23,6 +33,12 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
|
|
|
23
33
|
page: 1,
|
|
24
34
|
clicks: 0,
|
|
25
35
|
clicksTotal: 0,
|
|
36
|
+
timer: {
|
|
37
|
+
status: 'stopped',
|
|
38
|
+
slides: {},
|
|
39
|
+
startedAt: 0,
|
|
40
|
+
pausedAt: 0,
|
|
41
|
+
},
|
|
26
42
|
})
|
|
27
43
|
|
|
28
44
|
export {
|