@slidev/client 52.5.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.
@@ -1,55 +1,85 @@
1
+ import { parseTimeString } from '@slidev/parser/utils'
1
2
  import { useInterval } from '@vueuse/core'
2
3
  import { computed, toRef } from 'vue'
4
+ import { configs } from '../env'
3
5
  import { sharedState } from '../state/shared'
4
6
 
5
7
  export function useTimer() {
8
+ const mode = computed(() => configs.timer || 'stopwatch')
9
+ const duration = computed(() => parseTimeString(configs.duration).seconds)
6
10
  const interval = useInterval(100, { controls: true })
7
11
 
8
- const state = toRef(sharedState, 'timerStatus')
9
- const timer = computed(() => {
10
- if (sharedState.timerStatus === 'stopped' && sharedState.timerStartedAt === 0)
11
- return { h: '', m: '-', s: '--', ms: '-' }
12
+ const state = toRef(sharedState, 'timer')
13
+ const status = computed(() => state.value?.status)
14
+ const passedMs = computed(() => {
12
15
  // eslint-disable-next-line ts/no-unused-expressions
13
16
  interval.counter.value
14
- const passed = (Date.now() - sharedState.timerStartedAt)
15
- let h = Math.floor(passed / 1000 / 60 / 60).toString()
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)
23
+
24
+ const timer = computed(() => {
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()
16
35
  if (h === '0')
17
36
  h = ''
18
- let min = Math.floor(passed / 1000 / 60).toString()
37
+ let min = Math.floor(total / 1000 / 60 % 60).toString()
19
38
  if (h)
20
39
  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 }
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
+ }
24
49
  })
25
50
 
26
51
  function reset() {
27
52
  interval.pause()
28
- sharedState.timerStatus = 'stopped'
29
- sharedState.timerStartedAt = 0
30
- sharedState.timerPausedAt = 0
53
+ state.value = {
54
+ status: 'stopped',
55
+ slides: {},
56
+ startedAt: 0,
57
+ pausedAt: 0,
58
+ }
31
59
  }
32
60
 
33
61
  function resume() {
34
- if (sharedState.timerStatus === 'stopped') {
35
- sharedState.timerStatus = 'running'
36
- sharedState.timerStartedAt = Date.now()
62
+ if (!state.value)
63
+ return
64
+ if (state.value?.status === 'stopped') {
65
+ state.value.status = 'running'
66
+ state.value.startedAt = Date.now()
37
67
  }
38
- else if (sharedState.timerStatus === 'paused') {
39
- sharedState.timerStatus = 'running'
40
- sharedState.timerStartedAt = Date.now() - (sharedState.timerPausedAt - sharedState.timerStartedAt)
68
+ else if (state.value.status === 'paused') {
69
+ state.value.status = 'running'
70
+ state.value.startedAt = Date.now() - (state.value.pausedAt - state.value.startedAt)
41
71
  }
42
72
  interval.resume()
43
73
  }
44
74
 
45
75
  function pause() {
46
- sharedState.timerStatus = 'paused'
47
- sharedState.timerPausedAt = Date.now()
76
+ state.value.status = 'paused'
77
+ state.value.pausedAt = Date.now()
48
78
  interval.pause()
49
79
  }
50
80
 
51
81
  function toggle() {
52
- if (sharedState.timerStatus === 'running') {
82
+ if (state.value.status === 'running') {
53
83
  pause()
54
84
  }
55
85
  else {
@@ -59,10 +89,15 @@ export function useTimer() {
59
89
 
60
90
  return {
61
91
  state,
92
+ status,
62
93
  timer,
63
94
  reset,
64
95
  toggle,
65
96
  resume,
66
97
  pause,
98
+ passed,
99
+ percentage,
100
+ duration,
101
+ mode,
67
102
  }
68
103
  }
@@ -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>
@@ -1,19 +1,37 @@
1
1
  <script setup lang="ts">
2
+ import { computed } from 'vue'
2
3
  import { useTimer } from '../composables/useTimer'
3
4
 
4
- const { state, timer, reset, toggle } = useTimer()
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
+ })
5
20
  </script>
6
21
 
7
22
  <template>
8
23
  <div
9
24
  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]"
25
+ :class="color"
11
26
  >
12
27
  <div class="w-22px cursor-pointer">
13
- <div class="i-carbon:time group-hover:hidden text-xl" />
28
+ <div
29
+ class="group-hover:hidden text-2xl"
30
+ :class="mode === 'countdown' ? 'i-carbon:timer' : 'i-carbon:time'"
31
+ />
14
32
  <div class="group-not-hover:hidden flex flex-col items-center">
15
33
  <div class="relative op-80 hover:op-100" @click="toggle">
16
- <div v-if="state === 'running'" class="i-carbon:pause text-lg" />
34
+ <div v-if="status === 'running'" class="i-carbon:pause text-lg" />
17
35
  <div v-else class="i-carbon:play" />
18
36
  </div>
19
37
  <div class="op-80 hover:op-100" @click="reset">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.5.0",
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": "^13.9.0",
43
- "@vueuse/math": "^13.9.0",
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.5.0",
65
- "@slidev/types": "52.5.0"
64
+ "@slidev/parser": "52.6.0",
65
+ "@slidev/types": "52.6.0"
66
66
  },
67
67
  "devDependencies": {
68
- "vite": "^7.1.10"
68
+ "vite": "^7.1.12"
69
69
  }
70
70
  }
package/pages/notes.vue CHANGED
@@ -10,6 +10,7 @@ import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
10
10
  import IconButton from '../internals/IconButton.vue'
11
11
  import Modal from '../internals/Modal.vue'
12
12
  import NoteDisplay from '../internals/NoteDisplay.vue'
13
+ import TimerBar from '../internals/TimerBar.vue'
13
14
  import { fullscreen } from '../state'
14
15
  import { sharedState } from '../state/shared'
15
16
 
@@ -61,6 +62,7 @@ const clicksContext = computed(() => {
61
62
  </Modal>
62
63
  <div class="h-full flex flex-col">
63
64
  <CurrentProgressBar :clicks-context="clicksContext" :current="pageNo" />
65
+ <TimerBar />
64
66
  <div
65
67
  ref="scroller"
66
68
  class="px-5 py-3 flex-auto h-full overflow-auto"
@@ -23,6 +23,7 @@ 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'
26
27
  import TimerInlined from '../internals/TimerInlined.vue'
27
28
  import { onContextMenu } from '../logic/contextMenu'
28
29
  import { registerShortcuts } from '../logic/shortcuts'
@@ -117,6 +118,7 @@ onMounted(() => {
117
118
  <div class="bg-main h-full slidev-presenter grid grid-rows-[max-content_1fr] of-hidden">
118
119
  <div>
119
120
  <CurrentProgressBar />
121
+ <TimerBar />
120
122
  </div>
121
123
  <div class="grid-container" :class="`layout${presenterLayout}`">
122
124
  <div ref="main" class="relative grid-section main flex flex-col">
package/state/shared.ts CHANGED
@@ -6,9 +6,16 @@ export interface SharedState {
6
6
  page: number
7
7
  clicks: number
8
8
  clicksTotal: number
9
- timerStatus: 'stopped' | 'running' | 'paused'
10
- timerStartedAt: number
11
- timerPausedAt: number
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
+ }
12
19
 
13
20
  cursor?: {
14
21
  x: number
@@ -26,9 +33,12 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState<SharedState>(s
26
33
  page: 1,
27
34
  clicks: 0,
28
35
  clicksTotal: 0,
29
- timerStatus: 'stopped',
30
- timerStartedAt: 0,
31
- timerPausedAt: 0,
36
+ timer: {
37
+ status: 'stopped',
38
+ slides: {},
39
+ startedAt: 0,
40
+ pausedAt: 0,
41
+ },
32
42
  })
33
43
 
34
44
  export {
@@ -1,25 +0,0 @@
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>