@slidev/client 52.5.0 → 52.7.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 +57 -22
- package/constants.ts +1 -0
- package/index.ts +1 -0
- package/internals/TimerBar.vue +49 -0
- package/internals/TimerInlined.vue +22 -4
- package/internals/TitleIcon.vue +1 -0
- package/package.json +14 -14
- package/pages/notes.vue +2 -0
- package/pages/presenter.vue +2 -0
- package/setup/code-runners.ts +2 -0
- package/state/shared.ts +16 -6
- package/state/storage.ts +0 -5
- package/internals/TimesplitBar.vue +0 -25
package/composables/useTimer.ts
CHANGED
|
@@ -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, '
|
|
9
|
-
const
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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(
|
|
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(
|
|
22
|
-
const ms = Math.floor(
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 (
|
|
35
|
-
|
|
36
|
-
|
|
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 (
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 (
|
|
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
|
}
|
package/constants.ts
CHANGED
package/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type { DrawingsState } from './state/drawings'
|
|
|
23
23
|
export { drawingState, onDrawingUpdate } from './state/drawings'
|
|
24
24
|
export type { SharedState } from './state/shared'
|
|
25
25
|
export { onSharedUpdate, sharedState } from './state/shared'
|
|
26
|
+
export { lockShortcuts } from './state/storage'
|
|
26
27
|
|
|
27
28
|
export {
|
|
28
29
|
addSyncMethod,
|
|
@@ -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 {
|
|
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="
|
|
25
|
+
:class="color"
|
|
11
26
|
>
|
|
12
27
|
<div class="w-22px cursor-pointer">
|
|
13
|
-
<div
|
|
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="
|
|
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/internals/TitleIcon.vue
CHANGED
|
@@ -61,6 +61,7 @@ const builtinIcons: Record<string, string> = {
|
|
|
61
61
|
'.yml': 'i-vscode-icons:file-type-light-yaml',
|
|
62
62
|
'.yaml': 'i-vscode-icons:file-type-light-yaml',
|
|
63
63
|
'.php': 'i-vscode-icons:file-type-php',
|
|
64
|
+
'.svg': 'i-vscode-icons:file-type-svg',
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function matchIcon(title: string) {
|
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.7.0",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"@iconify-json/carbon": "^1.2.14",
|
|
33
33
|
"@iconify-json/ph": "^1.2.2",
|
|
34
34
|
"@iconify-json/svg-spinners": "^1.2.4",
|
|
35
|
-
"@shikijs/engine-javascript": "^3.
|
|
36
|
-
"@shikijs/monaco": "^3.
|
|
37
|
-
"@shikijs/vitepress-twoslash": "^3.
|
|
35
|
+
"@shikijs/engine-javascript": "^3.15.0",
|
|
36
|
+
"@shikijs/monaco": "^3.15.0",
|
|
37
|
+
"@shikijs/vitepress-twoslash": "^3.15.0",
|
|
38
38
|
"@slidev/rough-notation": "^0.1.0",
|
|
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",
|
|
@@ -48,23 +48,23 @@
|
|
|
48
48
|
"fuse.js": "^7.1.0",
|
|
49
49
|
"katex": "^0.16.25",
|
|
50
50
|
"lz-string": "^1.5.0",
|
|
51
|
-
"mermaid": "^11.12.
|
|
52
|
-
"monaco-editor": "^0.
|
|
51
|
+
"mermaid": "^11.12.1",
|
|
52
|
+
"monaco-editor": "^0.54.0",
|
|
53
53
|
"nanotar": "^0.2.0",
|
|
54
54
|
"pptxgenjs": "^4.0.1",
|
|
55
55
|
"prettier": "^3.6.2",
|
|
56
56
|
"recordrtc": "^5.6.2",
|
|
57
|
-
"shiki": "^3.
|
|
58
|
-
"shiki-magic-move": "^1.2.
|
|
57
|
+
"shiki": "^3.15.0",
|
|
58
|
+
"shiki-magic-move": "^1.2.1",
|
|
59
59
|
"typescript": "^5.9.3",
|
|
60
60
|
"unocss": "^66.5.4",
|
|
61
|
-
"vue": "^3.5.
|
|
61
|
+
"vue": "^3.5.23",
|
|
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.7.0",
|
|
65
|
+
"@slidev/types": "52.7.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"vite": "^7.
|
|
68
|
+
"vite": "^7.2.2"
|
|
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"
|
package/pages/presenter.vue
CHANGED
|
@@ -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/setup/code-runners.ts
CHANGED
|
@@ -5,6 +5,7 @@ import deps from '#slidev/monaco-run-deps'
|
|
|
5
5
|
import setups from '#slidev/setups/code-runners'
|
|
6
6
|
import { createSingletonPromise } from '@antfu/utils'
|
|
7
7
|
import { ref } from 'vue'
|
|
8
|
+
import { configs } from '../env'
|
|
8
9
|
|
|
9
10
|
export default createSingletonPromise(async () => {
|
|
10
11
|
const runners: Record<string, CodeRunner> = {
|
|
@@ -63,6 +64,7 @@ function runJavaScript(code: string): CodeRunnerOutputs {
|
|
|
63
64
|
vmConsole.clear = () => result.value.length = 0
|
|
64
65
|
try {
|
|
65
66
|
const safeJS = `return async (console, __slidev_import, __slidev_on_error) => {
|
|
67
|
+
${configs.monacoRunUseStrict ? `"use strict";` : ''}
|
|
66
68
|
try {
|
|
67
69
|
${sanitizeJS(code)}
|
|
68
70
|
} catch (e) {
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
timer: {
|
|
37
|
+
status: 'stopped',
|
|
38
|
+
slides: {},
|
|
39
|
+
startedAt: 0,
|
|
40
|
+
pausedAt: 0,
|
|
41
|
+
},
|
|
32
42
|
})
|
|
33
43
|
|
|
34
44
|
export {
|
package/state/storage.ts
CHANGED
|
@@ -17,11 +17,6 @@ export const disableTransition = ref(false)
|
|
|
17
17
|
|
|
18
18
|
export const shortcutsEnabled = ref(true)
|
|
19
19
|
|
|
20
|
-
/**
|
|
21
|
-
* Whether the keyboard shortcuts are enabled. Readonly,
|
|
22
|
-
* use `lockShortcuts` and `releaseShortcuts` to modify.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
20
|
// Use a locking mechanism to support multiple simultaneous locks
|
|
26
21
|
// and avoid race conditions. Race conditions may occur, for example,
|
|
27
22
|
// when locking shortcuts on editor focus and moving from one editor
|
|
@@ -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>
|