Skip to content

Commit 72ea8b5

Browse files
committed
feat: improve timer and progress bar
1 parent 05a0646 commit 72ea8b5

File tree

10 files changed

+183
-60
lines changed

10 files changed

+183
-60
lines changed
Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,68 @@
11
import { useInterval } from '@vueuse/core'
2-
import { computed } from 'vue'
2+
import { computed, toRef } from 'vue'
3+
import { sharedState } from '../state/shared'
34

45
export function useTimer() {
5-
const { counter, isActive, reset, pause, resume } = useInterval(1000, { controls: true })
6+
const interval = useInterval(100, { controls: true })
67

8+
const state = toRef(sharedState, 'timerStatus')
79
const timer = computed(() => {
8-
const passed = counter.value
9-
const sec = Math.floor(passed % 60).toString().padStart(2, '0')
10-
const min = Math.floor(passed / 60).toString().padStart(2, '0')
11-
return `${min}:${sec}`
10+
if (sharedState.timerStatus === 'stopped' && sharedState.timerStartedAt === 0)
11+
return { h: '', m: '-', s: '--', ms: '-' }
12+
// eslint-disable-next-line ts/no-unused-expressions
13+
interval.counter.value
14+
const passed = (Date.now() - sharedState.timerStartedAt)
15+
let h = Math.floor(passed / 1000 / 60 / 60).toString()
16+
if (h === '0')
17+
h = ''
18+
let min = Math.floor(passed / 1000 / 60).toString()
19+
if (h)
20+
min = min.padStart(2, '0')
21+
const sec = Math.floor(passed / 1000 % 60).toString().padStart(2, '0')
22+
const ms = Math.floor(passed % 1000 / 100).toString()
23+
return { h, m: min, s: sec, ms }
1224
})
1325

26+
function reset() {
27+
interval.pause()
28+
sharedState.timerStatus = 'stopped'
29+
sharedState.timerStartedAt = 0
30+
sharedState.timerPausedAt = 0
31+
}
32+
33+
function resume() {
34+
if (sharedState.timerStatus === 'stopped') {
35+
sharedState.timerStatus = 'running'
36+
sharedState.timerStartedAt = Date.now()
37+
}
38+
else if (sharedState.timerStatus === 'paused') {
39+
sharedState.timerStatus = 'running'
40+
sharedState.timerStartedAt = Date.now() - (sharedState.timerPausedAt - sharedState.timerStartedAt)
41+
}
42+
interval.resume()
43+
}
44+
45+
function pause() {
46+
sharedState.timerStatus = 'paused'
47+
sharedState.timerPausedAt = Date.now()
48+
interval.pause()
49+
}
50+
51+
function toggle() {
52+
if (sharedState.timerStatus === 'running') {
53+
pause()
54+
}
55+
else {
56+
resume()
57+
}
58+
}
59+
1460
return {
61+
state,
1562
timer,
16-
isTimerActive: isActive,
17-
resetTimer: reset,
18-
toggleTimer: () => (isActive.value ? pause() : resume()),
63+
reset,
64+
toggle,
65+
resume,
66+
pause,
1967
}
2068
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

packages/client/internals/NavControls.vue

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,9 @@ if (__SLIDEV_FEATURE_RECORD__)
178178

179179
<VerticalDivider v-if="!isEmbedded" />
180180

181-
<div class="h-40px flex" p="l-1 t-0.5 r-2" text="sm leading-2">
182-
<div class="my-auto">
183-
{{ currentSlideNo }}
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>
186184
</div>
187185

188186
<CustomNavControls />
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<script setup lang="ts">
2+
import { useTimer } from '../composables/useTimer'
3+
4+
const { state, timer, reset, toggle } = useTimer()
5+
</script>
6+
7+
<template>
8+
<div
9+
class="group flex items-center justify-center pl-4 select-none"
10+
:class="{ running: 'text-green6 dark:text-green3', paused: 'text-orange6 dark:text-orange3', stopped: 'op50' }[state]"
11+
>
12+
<div class="w-22px cursor-pointer">
13+
<div class="i-carbon:time group-hover:hidden text-xl" />
14+
<div class="group-not-hover:hidden flex flex-col items-center">
15+
<div class="relative op-80 hover:op-100" @click="toggle">
16+
<div v-if="state === 'running'" class="i-carbon:pause text-lg" />
17+
<div v-else class="i-carbon:play" />
18+
</div>
19+
<div class="op-80 hover:op-100" @click="reset">
20+
<div class="i-carbon:renew" />
21+
</div>
22+
</div>
23+
</div>
24+
<div class="text-3xl px-3 my-auto font-mono">
25+
<template v-if="timer.h">
26+
<span>{{ timer.h }}</span>
27+
<span op50>:</span>
28+
</template>
29+
<span>{{ timer.m }}</span>
30+
<span op50>:</span>
31+
<span>{{ timer.s }}</span>
32+
<span class="text-base op50">.{{ timer.ms }}</span>
33+
</div>
34+
</div>
35+
</template>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
import { parseTimesplits } from '@slidev/parser/utils'
3+
import { computed } from 'vue'
4+
import { useNav } from '../composables/useNav'
5+
// import { useTimer } from '../composables/useTimer'
6+
7+
const { slides } = useNav()
8+
9+
// const timer = useTimer()
10+
const slidesWithTimesplits = computed(() => slides.value.filter(i => i.meta.slide?.frontmatter.timesplit))
11+
12+
const timesplits = computed(() => {
13+
const parsed = parseTimesplits(
14+
slidesWithTimesplits.value
15+
.map(i => ({ no: i.no, timesplit: i.meta.slide?.frontmatter.timesplit as string })),
16+
)
17+
return parsed
18+
})
19+
</script>
20+
21+
<template>
22+
<div v-if="false" class="border-b border-main relative flex">
23+
{{ timesplits }}
24+
</div>
25+
</template>

packages/client/logic/slides.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SlideRoute } from '@slidev/types'
22
import { slides } from '#slidev/slides'
33
import { computed, watch, watchEffect } from 'vue'
4+
import { useNav } from '../composables/useNav'
45
import { useSlideContext } from '../context'
56

67
export { slides }
@@ -23,8 +24,9 @@ export function getSlidePath(
2324
}
2425

2526
export function useIsSlideActive() {
26-
const { $page, $nav } = useSlideContext()
27-
return computed(() => $page.value === $nav.value.currentSlideNo) // Use `$nav.value.currentSlideNo` rather than `useNav().currentSlideNo` to make it work in print/export mode. See https://github.com/slidevjs/slidev/issues/2310.
27+
const { $page } = useSlideContext()
28+
const { currentSlideNo } = useNav()
29+
return computed(() => $page.value === currentSlideNo.value)
2830
}
2931

3032
export function onSlideEnter(cb: () => void) {

packages/client/pages/notes.vue

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createClicksContextBase } from '../composables/useClicks'
66
import { useNav } from '../composables/useNav'
77
import { slidesTitle } from '../env'
88
import ClicksSlider from '../internals/ClicksSlider.vue'
9+
import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
910
import IconButton from '../internals/IconButton.vue'
1011
import Modal from '../internals/Modal.vue'
1112
import NoteDisplay from '../internals/NoteDisplay.vue'
@@ -58,14 +59,11 @@ const clicksContext = computed(() => {
5859
</button>
5960
</div>
6061
</Modal>
61-
<div
62-
class="fixed top-0 left-0 h-3px bg-primary transition-all duration-500"
63-
:style="{ width: `${(pageNo - 1) / (total - 1) * 100 + 1}%` }"
64-
/>
65-
<div class="h-full pt-2 flex flex-col">
62+
<div class="h-full flex flex-col">
63+
<CurrentProgressBar :clicks-context="clicksContext" :current="pageNo" />
6664
<div
6765
ref="scroller"
68-
class="px-5 flex-auto h-full overflow-auto"
66+
class="px-5 py-3 flex-auto h-full overflow-auto"
6967
:style="{ fontSize: `${fontSize}px` }"
7068
>
7169
<NoteDisplay
@@ -98,8 +96,9 @@ const clicksContext = computed(() => {
9896
<div class="i-carbon:help" />
9997
</IconButton>
10098
<div class="flex-auto" />
101-
<div class="p2 text-center">
102-
{{ pageNo }} / {{ total }}
99+
<div class="px2 my-auto">
100+
<span class="text-lg">{{ pageNo }}</span>
101+
<span class="opacity-50 text-sm"> / {{ total }}</span>
103102
</div>
104103
</div>
105104
</div>

packages/client/pages/presenter.vue

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { createClicksContextBase } from '../composables/useClicks'
66
import { useDrawings } from '../composables/useDrawings'
77
import { useNav } from '../composables/useNav'
88
import { useSwipeControls } from '../composables/useSwipeControls'
9-
import { useTimer } from '../composables/useTimer'
109
import { useWakeLock } from '../composables/useWakeLock'
1110
import { slidesTitle } from '../env'
1211
import ClicksSlider from '../internals/ClicksSlider.vue'
1312
import ContextMenu from '../internals/ContextMenu.vue'
13+
import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
1414
import DrawingControls from '../internals/DrawingControls.vue'
1515
import Goto from '../internals/Goto.vue'
1616
import IconButton from '../internals/IconButton.vue'
@@ -23,6 +23,7 @@ import SegmentControl from '../internals/SegmentControl.vue'
2323
import SlideContainer from '../internals/SlideContainer.vue'
2424
import SlidesShow from '../internals/SlidesShow.vue'
2525
import SlideWrapper from '../internals/SlideWrapper.vue'
26+
import TimerInlined from '../internals/TimerInlined.vue'
2627
import { onContextMenu } from '../logic/contextMenu'
2728
import { registerShortcuts } from '../logic/shortcuts'
2829
import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
@@ -44,16 +45,13 @@ const {
4445
nextRoute,
4546
slides,
4647
getPrimaryClicks,
47-
total,
4848
} = useNav()
4949
const { isDrawing } = useDrawings()
5050
5151
useHead({ title: `Presenter - ${slidesTitle}` })
5252
5353
const notesEditing = ref(false)
5454
55-
const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer()
56-
5755
const clicksCtxMap = computed(() => slides.value.map((route) => {
5856
const clicks = ref(0)
5957
return {
@@ -116,7 +114,10 @@ onMounted(() => {
116114
</script>
117115

118116
<template>
119-
<div class="bg-main h-full slidev-presenter" pt-2px>
117+
<div class="bg-main h-full slidev-presenter grid grid-rows-[max-content_1fr] of-hidden">
118+
<div>
119+
<CurrentProgressBar />
120+
</div>
120121
<div class="grid-container" :class="`layout${presenterLayout}`">
121122
<div ref="main" class="relative grid-section main flex flex-col">
122123
<div flex="~ gap-4 items-center" border="b main" p1>
@@ -210,32 +211,10 @@ onMounted(() => {
210211
<div class="grid-section bottom flex">
211212
<NavControls :persist="true" class="transition" :class="inFocus ? '' : 'op25'" />
212213
<div flex-auto />
213-
<div class="group flex items-center justify-center pl-4 select-none">
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>
214+
<TimerInlined />
230215
</div>
231216
<DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
232217
</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>
239218
</div>
240219
<Goto />
241220
<QuickOverview />
@@ -248,9 +227,7 @@ onMounted(() => {
248227
}
249228
250229
.grid-container {
251-
--uno: bg-gray/20;
252-
height: 100%;
253-
width: 100%;
230+
--uno: bg-gray/20 flex-1 of-hidden;
254231
display: grid;
255232
gap: 1px 1px;
256233
}
@@ -305,14 +282,9 @@ onMounted(() => {
305282
}
306283
}
307284
308-
.progress-bar {
309-
--uno: fixed left-0 right-0 top-0;
310-
}
311-
312285
.grid-section {
313286
--uno: bg-main;
314287
}
315-
316288
.grid-section.top {
317289
grid-area: top;
318290
}

packages/client/state/shared.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export interface SharedState {
66
page: number
77
clicks: number
88
clicksTotal: number
9+
timerStatus: 'stopped' | 'running' | 'paused'
10+
timerStartedAt: number
11+
timerPausedAt: number
912

1013
cursor?: {
1114
x: number
@@ -23,6 +26,9 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
2326
page: 1,
2427
clicks: 0,
2528
clicksTotal: 0,
29+
timerStatus: 'stopped',
30+
timerStartedAt: 0,
31+
timerPausedAt: 0,
2632
})
2733

2834
export {

packages/slidev/node/vite/serverRef.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ export async function createServerRefPlugin(
1414
nav: {
1515
page: 0,
1616
clicks: 0,
17+
timerStatus: 'stopped',
18+
timerStartedAt: 0,
19+
timerPausedAt: 0,
1720
},
1821
drawings: await loadDrawings(options),
1922
snapshots: await loadSnapshots(options),
2023
...pluginOptions.serverRef?.state,
2124
},
2225
onChanged(key, data, patch, timestamp) {
2326
pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp)
27+
2428
if (options.data.config.drawings.persist && key === 'drawings')
2529
writeDrawings(options, patch ?? data)
2630

0 commit comments

Comments
 (0)