diff --git a/demo/starter/slides.md b/demo/starter/slides.md index 9e65e48145..19bd435f05 100644 --- a/demo/starter/slides.md +++ b/demo/starter/slides.md @@ -20,6 +20,8 @@ drawings: transition: slide-left # enable MDC Syntax: https://sli.dev/features/mdc mdc: true +# duration of the presentation +duration: 35min --- # Welcome to Slidev diff --git a/docs/features/timer.md b/docs/features/timer.md new file mode 100644 index 0000000000..f9e94ae74a --- /dev/null +++ b/docs/features/timer.md @@ -0,0 +1,23 @@ +--- +tags: [presenter] +description: Timer for the presenter mode. +--- + +# Presenter Timer + +Slidev provides a timer for the presenter mode. You can start, pause, and reset the timer. + +It will show a timer (in stopwatch or countdown mode), and a progress bar in the presenter mode. + +## Configuration + +You can set the duration of the presentation in the headmatter. Default is `30min`. + +```yaml +--- +# duration of the presentation, default is '30min' +duration: 30min +# timer mode, can be 'countdown' or 'stopwatch', default is 'stopwatch' +timer: stopwatch +--- +``` diff --git a/packages/client/composables/useTimer.ts b/packages/client/composables/useTimer.ts index 95d76a689e..66e90e68ed 100644 --- a/packages/client/composables/useTimer.ts +++ b/packages/client/composables/useTimer.ts @@ -1,55 +1,85 @@ +import { parseTimeString } from '@slidev/parser/utils' import { useInterval } from '@vueuse/core' import { computed, toRef } from 'vue' +import { configs } from '../env' import { sharedState } from '../state/shared' export function useTimer() { + const mode = computed(() => configs.timer || 'stopwatch') + const duration = computed(() => parseTimeString(configs.duration).seconds) const interval = useInterval(100, { controls: true }) - const state = toRef(sharedState, 'timerStatus') - const timer = computed(() => { - if (sharedState.timerStatus === 'stopped' && sharedState.timerStartedAt === 0) - return { h: '', m: '-', s: '--', ms: '-' } + const state = toRef(sharedState, 'timer') + const status = computed(() => state.value?.status) + const passedMs = computed(() => { // eslint-disable-next-line ts/no-unused-expressions interval.counter.value - const passed = (Date.now() - sharedState.timerStartedAt) - let h = Math.floor(passed / 1000 / 60 / 60).toString() + if (state.value.status === 'stopped' || !state.value.startedAt) + return 0 + return Date.now() - state.value.startedAt + }) + const passed = computed(() => passedMs.value / 1000) + const percentage = computed(() => passed.value / duration.value * 100) + + const timer = computed(() => { + if (mode.value === 'stopwatch') { + if (state.value.status === 'stopped' || !state.value.startedAt) + return { h: '', m: '-', s: '--', ms: '-' } + } + + const total = mode.value === 'countdown' + ? duration.value * 1000 - passedMs.value + : passedMs.value + + let h = Math.floor(total / 1000 / 60 / 60).toString() if (h === '0') h = '' - let min = Math.floor(passed / 1000 / 60 % 60).toString() + let min = Math.floor(total / 1000 / 60 % 60).toString() if (h) min = min.padStart(2, '0') - const sec = Math.floor(passed / 1000 % 60).toString().padStart(2, '0') - const ms = Math.floor(passed % 1000 / 100).toString() - return { h, m: min, s: sec, ms } + const sec = Math.floor(total / 1000 % 60).toString().padStart(2, '0') + const ms = Math.floor(total % 1000 / 100).toString() + + return { + h, + m: min, + s: sec, + ms, + } }) function reset() { interval.pause() - sharedState.timerStatus = 'stopped' - sharedState.timerStartedAt = 0 - sharedState.timerPausedAt = 0 + state.value = { + status: 'stopped', + slides: {}, + startedAt: 0, + pausedAt: 0, + } } function resume() { - if (sharedState.timerStatus === 'stopped') { - sharedState.timerStatus = 'running' - sharedState.timerStartedAt = Date.now() + if (!state.value) + return + if (state.value?.status === 'stopped') { + state.value.status = 'running' + state.value.startedAt = Date.now() } - else if (sharedState.timerStatus === 'paused') { - sharedState.timerStatus = 'running' - sharedState.timerStartedAt = Date.now() - (sharedState.timerPausedAt - sharedState.timerStartedAt) + else if (state.value.status === 'paused') { + state.value.status = 'running' + state.value.startedAt = Date.now() - (state.value.pausedAt - state.value.startedAt) } interval.resume() } function pause() { - sharedState.timerStatus = 'paused' - sharedState.timerPausedAt = Date.now() + state.value.status = 'paused' + state.value.pausedAt = Date.now() interval.pause() } function toggle() { - if (sharedState.timerStatus === 'running') { + if (state.value.status === 'running') { pause() } else { @@ -59,10 +89,15 @@ export function useTimer() { return { state, + status, timer, reset, toggle, resume, pause, + passed, + percentage, + duration, + mode, } } diff --git a/packages/client/internals/TimerBar.vue b/packages/client/internals/TimerBar.vue new file mode 100644 index 0000000000..00e7c81b2d --- /dev/null +++ b/packages/client/internals/TimerBar.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/client/internals/TimerInlined.vue b/packages/client/internals/TimerInlined.vue index 1575e183b7..0b5f5818ca 100644 --- a/packages/client/internals/TimerInlined.vue +++ b/packages/client/internals/TimerInlined.vue @@ -1,19 +1,37 @@