From df57065a624a59069cf971c674e6866298139d13 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 20 Oct 2025 20:41:46 +0900 Subject: [PATCH 01/10] feat: parser --- demo/starter/slides.md | 2 + .../parser/src/timesplit/timestamp.test.ts | 24 +++++ packages/parser/src/timesplit/timestamp.ts | 100 ++++++++++++++++++ packages/types/src/frontmatter.ts | 4 + 4 files changed, 130 insertions(+) create mode 100644 packages/parser/src/timesplit/timestamp.test.ts create mode 100644 packages/parser/src/timesplit/timestamp.ts diff --git a/demo/starter/slides.md b/demo/starter/slides.md index 9e65e48145..d78fcffe64 100644 --- a/demo/starter/slides.md +++ b/demo/starter/slides.md @@ -551,6 +551,7 @@ Learn more: [Mermaid Diagrams](https://sli.dev/features/mermaid) and [PlantUML D foo: bar dragPos: square: 691,32,167,_,-16 +timesplit: +30s --- # Draggable Elements @@ -626,6 +627,7 @@ console.log(emptyArray(10).reduce(fib => [...fib, fib.at(-1)! + fib.at(- --- layout: center class: text-center +timesplit: 10m --- # Learn More diff --git a/packages/parser/src/timesplit/timestamp.test.ts b/packages/parser/src/timesplit/timestamp.test.ts new file mode 100644 index 0000000000..c322093c8c --- /dev/null +++ b/packages/parser/src/timesplit/timestamp.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest' +import { parseTimestamp } from './timestamp' + +describe('parseTimestamp', () => { + it('should parse timestamp into seconds', () => { + expect(parseTimestamp('10:50.1')).toEqual({ seconds: 650.1, relative: false }) + expect(parseTimestamp('10s')).toEqual({ seconds: 10, relative: false }) + expect(parseTimestamp('5m')).toEqual({ seconds: 300, relative: false }) + expect(parseTimestamp('3min')).toEqual({ seconds: 180, relative: false }) + expect(parseTimestamp('3mins 5secs')).toEqual({ seconds: 185, relative: false }) + expect(parseTimestamp('10.5m3s')).toEqual({ seconds: 633, relative: false }) + expect(parseTimestamp('+10s')).toEqual({ seconds: 10, relative: true }) + expect(parseTimestamp('1h10m30s')).toEqual({ seconds: 4230, relative: false }) + expect(parseTimestamp('1h4s')).toEqual({ seconds: 3604, relative: false }) + expect(parseTimestamp('1:1:1')).toEqual({ seconds: 3661, relative: false }) + expect(parseTimestamp('0.5years')).toEqual({ seconds: 15778476, relative: false }) + }) + + it('should throw an error for invalid timestamp', () => { + expect(() => parseTimestamp('10x')).toThrow('Invalid timestamp unit: x') + expect(() => parseTimestamp('10h:10m:10s')).toThrow('Invalid timestamp format') + expect(() => parseTimestamp('hello 1s world')).toThrow('Unknown timestamp remaining: hello world') + }) +}) diff --git a/packages/parser/src/timesplit/timestamp.ts b/packages/parser/src/timesplit/timestamp.ts new file mode 100644 index 0000000000..0642081690 --- /dev/null +++ b/packages/parser/src/timesplit/timestamp.ts @@ -0,0 +1,100 @@ +/** + * Parse timestamp into seconds + * + * Accepts: + * - 10:50.1 + * - 10s + * - 5m + * - 3min + * - 3mins 5secs + * - 10.5m3s + * - +10s + * - 1h10m30s + * - 1h4s + * - 1:1:1 + */ +export function parseTimestamp(timestamp: string): { + seconds: number + relative: boolean +} { + const relative = timestamp.startsWith('+') + if (relative) { + timestamp = timestamp.slice(1) + } + let seconds = 0 + if (timestamp.includes(':')) { + const parts = timestamp.split(':').map(Number) + let h = 0 + let m = 0 + let s = 0 + if (parts.length === 3) { + h = parts[0] + m = parts[1] + s = parts[2] + } + else if (parts.length === 2) { + m = parts[0] + s = parts[1] + } + else if (parts.length === 1) { + s = parts[0] + } + else { + throw new TypeError('Invalid timestamp format') + } + if (Number.isNaN(h) || Number.isNaN(m) || Number.isNaN(s)) { + throw new TypeError('Invalid timestamp format') + } + seconds = (h || 0) * 3600 + (m || 0) * 60 + (s || 0) + } + else if (!timestamp.match(/[a-z]/i)) { + seconds = Number(timestamp) + } + else { + const unitMap: Record = { + s: 1, + sec: 1, + secs: 1, + m: 60, + min: 60, + mins: 60, + h: 3600, + hr: 3600, + hrs: 3600, + hour: 3600, + hours: 3600, + day: 86400, + days: 86400, + week: 604800, + weeks: 604800, + month: 2629746, + months: 2629746, + year: 31556952, + years: 31556952, + } + const regex = /([\d.]+)([a-z]+)/gi + const matches = timestamp.matchAll(regex) + if (matches) { + for (const match of matches) { + const value = Number(match[1]) + if (Number.isNaN(value)) { + throw new TypeError(`Invalid timestamp value: ${match[1]}`) + } + const unit = match[2].toLowerCase() + if (!(unit in unitMap)) { + throw new TypeError(`Invalid timestamp unit: ${unit}`) + } + seconds += value * unitMap[unit] + } + } + const remaining = timestamp.replace(regex, '').trim() + if (remaining) { + throw new TypeError(`Unknown timestamp remaining: ${remaining}`) + } + } + + return { + seconds, + relative, + } +} diff --git a/packages/types/src/frontmatter.ts b/packages/types/src/frontmatter.ts index 15620abf8d..7fd1288d87 100644 --- a/packages/types/src/frontmatter.ts +++ b/packages/types/src/frontmatter.ts @@ -360,6 +360,10 @@ export interface Frontmatter extends TransitionOptions { * See https://sli.dev/guide/syntax.html#importing-slides */ src?: string + /** + * Set time split for the end of the slide + */ + timesplit?: string } export interface DrawingsOptions { From e6ef9b48fe93ccc0e1ba959624c73558a07b6d65 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 20 Oct 2025 20:51:57 +0900 Subject: [PATCH 02/10] feat: parser --- .../parser/src/timesplit/timesplit.test.ts | 47 +++++++++++++++++ packages/parser/src/timesplit/timesplit.ts | 51 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 packages/parser/src/timesplit/timesplit.test.ts create mode 100644 packages/parser/src/timesplit/timesplit.ts diff --git a/packages/parser/src/timesplit/timesplit.test.ts b/packages/parser/src/timesplit/timesplit.test.ts new file mode 100644 index 0000000000..27e6804d42 --- /dev/null +++ b/packages/parser/src/timesplit/timesplit.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest' +import { parseTimesplits } from './timesplit' + +describe('parseTimesplits', () => { + it('should parse timestamp into seconds', () => { + expect(parseTimesplits([ + { no: 1, timesplit: '+10s' }, + { no: 5, timesplit: '10:10' }, + { no: 3, timesplit: '15m' }, + { no: 7, timesplit: '1h10m30s' }, + ])).toMatchInlineSnapshot(` + [ + { + "name": "[start]", + "noEnd": 1, + "noStart": 0, + "timestampEnd": 10, + "timestampStart": 0, + }, + { + "noEnd": 5, + "noStart": 1, + "timestampEnd": 610, + "timestampStart": 10, + }, + { + "noEnd": 3, + "noStart": 5, + "timestampEnd": 900, + "timestampStart": 610, + }, + { + "noEnd": 7, + "noStart": 3, + "timestampEnd": 4230, + "timestampStart": 900, + }, + { + "noEnd": 7, + "noStart": 7, + "timestampEnd": 4230, + "timestampStart": 4230, + }, + ] + `) + }) +}) diff --git a/packages/parser/src/timesplit/timesplit.ts b/packages/parser/src/timesplit/timesplit.ts new file mode 100644 index 0000000000..453b2f7f9a --- /dev/null +++ b/packages/parser/src/timesplit/timesplit.ts @@ -0,0 +1,51 @@ +import { parseTimestamp } from './timestamp' + +export interface TimesplitInput { + no: number + timesplit: string + name?: string +} + +export interface TimesplitOutput { + timestampStart: number + timestampEnd: number + noStart: number + noEnd: number + name?: string +} + +export function parseTimesplits(inputs: TimesplitInput[]): TimesplitOutput[] { + let ts = 0 + const outputs: TimesplitOutput[] = [] + let current: TimesplitOutput = { + timestampStart: ts, + timestampEnd: ts, + noStart: 0, + noEnd: 0, + name: '[start]', + } + outputs.push(current) + for (const input of inputs) { + const time = parseTimestamp(input.timesplit) + const end = time.relative + ? ts + time.seconds + : time.seconds + if (end < ts) { + throw new Error(`Timesplit end ${end} is before start ${ts}`) + } + current.timestampEnd = end + current.noEnd = input.no + if (input.name) { + current.name = input.name + } + ts = end + current = { + timestampStart: end, + timestampEnd: end, + noStart: input.no, + noEnd: input.no, + } + outputs.push(current) + } + return outputs +} From 94455c9bdb052b10217fe35f7d23a102436affaa Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 20 Oct 2025 21:16:28 +0900 Subject: [PATCH 03/10] wip: --- packages/parser/package.json | 29 ++++++----------- packages/parser/src/timesplit/index.ts | 2 ++ packages/parser/src/timesplit/timesplit.ts | 4 +-- .../parser/src/timesplit/timestamp.test.ts | 32 +++++++++---------- packages/parser/src/timesplit/timestamp.ts | 2 +- packages/parser/src/utils.ts | 2 ++ packages/parser/tsdown.config.ts | 13 +++++++- packages/types/src/frontmatter.ts | 11 +++++++ packages/vscode/schema/frontmatter.json | 10 ++++++ packages/vscode/schema/headmatter.json | 20 ++++++++++++ 10 files changed, 85 insertions(+), 40 deletions(-) create mode 100644 packages/parser/src/timesplit/index.ts diff --git a/packages/parser/package.json b/packages/parser/package.json index 169266f4df..6033ed4f6c 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -12,26 +12,15 @@ }, "bugs": "https://github.com/slidevjs/slidev/issues", "exports": { - ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - }, - "./core": { - "types": "./dist/core.d.mts", - "import": "./dist/core.mjs" - }, - "./fs": { - "types": "./dist/fs.d.mts", - "import": "./dist/fs.mjs" - }, - "./utils": { - "types": "./dist/utils.d.mts", - "import": "./dist/utils.mjs" - } + ".": "./dist/index.mjs", + "./core": "./dist/core.mjs", + "./fs": "./dist/fs.mjs", + "./utils": "./dist/utils.mjs", + "./package.json": "./package.json" }, - "main": "dist/index.mjs", - "module": "dist/index.mjs", - "types": "dist/index.d.mts", + "main": "./dist/index.mjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", "files": [ "*.d.ts", "dist" @@ -40,7 +29,7 @@ "node": ">=18.0.0" }, "scripts": { - "build": "tsdown src/index.ts src/core.ts src/fs.ts src/utils.ts", + "build": "tsdown", "dev": "nr build --watch", "prepublishOnly": "npm run build" }, diff --git a/packages/parser/src/timesplit/index.ts b/packages/parser/src/timesplit/index.ts new file mode 100644 index 0000000000..d9bcebd20b --- /dev/null +++ b/packages/parser/src/timesplit/index.ts @@ -0,0 +1,2 @@ +export * from './timesplit' +export * from './timestamp' diff --git a/packages/parser/src/timesplit/timesplit.ts b/packages/parser/src/timesplit/timesplit.ts index 453b2f7f9a..075da1d969 100644 --- a/packages/parser/src/timesplit/timesplit.ts +++ b/packages/parser/src/timesplit/timesplit.ts @@ -1,4 +1,4 @@ -import { parseTimestamp } from './timestamp' +import { parseTimestampString } from './timestamp' export interface TimesplitInput { no: number @@ -26,7 +26,7 @@ export function parseTimesplits(inputs: TimesplitInput[]): TimesplitOutput[] { } outputs.push(current) for (const input of inputs) { - const time = parseTimestamp(input.timesplit) + const time = parseTimestampString(input.timesplit) const end = time.relative ? ts + time.seconds : time.seconds diff --git a/packages/parser/src/timesplit/timestamp.test.ts b/packages/parser/src/timesplit/timestamp.test.ts index c322093c8c..d1705375f1 100644 --- a/packages/parser/src/timesplit/timestamp.test.ts +++ b/packages/parser/src/timesplit/timestamp.test.ts @@ -1,24 +1,24 @@ import { describe, expect, it } from 'vitest' -import { parseTimestamp } from './timestamp' +import { parseTimestampString } from './timestamp' -describe('parseTimestamp', () => { +describe('parseTimestampString', () => { it('should parse timestamp into seconds', () => { - expect(parseTimestamp('10:50.1')).toEqual({ seconds: 650.1, relative: false }) - expect(parseTimestamp('10s')).toEqual({ seconds: 10, relative: false }) - expect(parseTimestamp('5m')).toEqual({ seconds: 300, relative: false }) - expect(parseTimestamp('3min')).toEqual({ seconds: 180, relative: false }) - expect(parseTimestamp('3mins 5secs')).toEqual({ seconds: 185, relative: false }) - expect(parseTimestamp('10.5m3s')).toEqual({ seconds: 633, relative: false }) - expect(parseTimestamp('+10s')).toEqual({ seconds: 10, relative: true }) - expect(parseTimestamp('1h10m30s')).toEqual({ seconds: 4230, relative: false }) - expect(parseTimestamp('1h4s')).toEqual({ seconds: 3604, relative: false }) - expect(parseTimestamp('1:1:1')).toEqual({ seconds: 3661, relative: false }) - expect(parseTimestamp('0.5years')).toEqual({ seconds: 15778476, relative: false }) + expect(parseTimestampString('10:50.1')).toEqual({ seconds: 650.1, relative: false }) + expect(parseTimestampString('10s')).toEqual({ seconds: 10, relative: false }) + expect(parseTimestampString('5m')).toEqual({ seconds: 300, relative: false }) + expect(parseTimestampString('3min')).toEqual({ seconds: 180, relative: false }) + expect(parseTimestampString('3mins 5secs')).toEqual({ seconds: 185, relative: false }) + expect(parseTimestampString('10.5m3s')).toEqual({ seconds: 633, relative: false }) + expect(parseTimestampString('+10s')).toEqual({ seconds: 10, relative: true }) + expect(parseTimestampString('1h10m30s')).toEqual({ seconds: 4230, relative: false }) + expect(parseTimestampString('1h4s')).toEqual({ seconds: 3604, relative: false }) + expect(parseTimestampString('1:1:1')).toEqual({ seconds: 3661, relative: false }) + expect(parseTimestampString('0.5years')).toEqual({ seconds: 15778476, relative: false }) }) it('should throw an error for invalid timestamp', () => { - expect(() => parseTimestamp('10x')).toThrow('Invalid timestamp unit: x') - expect(() => parseTimestamp('10h:10m:10s')).toThrow('Invalid timestamp format') - expect(() => parseTimestamp('hello 1s world')).toThrow('Unknown timestamp remaining: hello world') + expect(() => parseTimestampString('10x')).toThrow('Invalid timestamp unit: x') + expect(() => parseTimestampString('10h:10m:10s')).toThrow('Invalid timestamp format') + expect(() => parseTimestampString('hello 1s world')).toThrow('Unknown timestamp remaining: hello world') }) }) diff --git a/packages/parser/src/timesplit/timestamp.ts b/packages/parser/src/timesplit/timestamp.ts index 0642081690..7e87d7197e 100644 --- a/packages/parser/src/timesplit/timestamp.ts +++ b/packages/parser/src/timesplit/timestamp.ts @@ -13,7 +13,7 @@ * - 1h4s * - 1:1:1 */ -export function parseTimestamp(timestamp: string): { +export function parseTimestampString(timestamp: string): { seconds: number relative: boolean } { diff --git a/packages/parser/src/utils.ts b/packages/parser/src/utils.ts index 0586f3b28d..cbdcc4f697 100644 --- a/packages/parser/src/utils.ts +++ b/packages/parser/src/utils.ts @@ -1,5 +1,7 @@ import { isNumber, range, uniq } from '@antfu/utils' +export * from './timesplit' + /** * 1,3-5,8 => [1, 3, 4, 5, 8] */ diff --git a/packages/parser/tsdown.config.ts b/packages/parser/tsdown.config.ts index b67bd3cfca..12ad1bcf10 100644 --- a/packages/parser/tsdown.config.ts +++ b/packages/parser/tsdown.config.ts @@ -1 +1,12 @@ -export { default } from '../../tsdown.config.ts' +import { defineConfig } from 'tsdown' +import baseConfig from '../../tsdown.config.ts' + +export default defineConfig({ + ...baseConfig, + entry: { + index: 'src/index.ts', + core: 'src/core.ts', + fs: 'src/fs.ts', + utils: 'src/utils.ts', + }, +}) diff --git a/packages/types/src/frontmatter.ts b/packages/types/src/frontmatter.ts index 7fd1288d87..29415c98d1 100644 --- a/packages/types/src/frontmatter.ts +++ b/packages/types/src/frontmatter.ts @@ -362,8 +362,19 @@ export interface Frontmatter extends TransitionOptions { src?: string /** * Set time split for the end of the slide + * + * Accepts: + * - 10:05 + * - 10m5s + * - +10s (relative to the previous point) */ timesplit?: string + /** + * Set title for the time split + * + * Default to slide title + */ + timesplitTitle?: string } export interface DrawingsOptions { diff --git a/packages/vscode/schema/frontmatter.json b/packages/vscode/schema/frontmatter.json index 9bf5e8b7da..48a5f2c580 100644 --- a/packages/vscode/schema/frontmatter.json +++ b/packages/vscode/schema/frontmatter.json @@ -119,6 +119,16 @@ "type": "string", "description": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides", "markdownDescription": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides" + }, + "timesplit": { + "type": "string", + "description": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)", + "markdownDescription": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)" + }, + "timesplitTitle": { + "type": "string", + "description": "Set title for the time split\n\nDefault to slide title", + "markdownDescription": "Set title for the time split\n\nDefault to slide title" } } }, diff --git a/packages/vscode/schema/headmatter.json b/packages/vscode/schema/headmatter.json index d6fffb87ce..d250582cf5 100644 --- a/packages/vscode/schema/headmatter.json +++ b/packages/vscode/schema/headmatter.json @@ -97,6 +97,16 @@ "description": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides", "markdownDescription": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides" }, + "timesplit": { + "type": "string", + "description": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)", + "markdownDescription": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)" + }, + "timesplitTitle": { + "type": "string", + "description": "Set title for the time split\n\nDefault to slide title", + "markdownDescription": "Set title for the time split\n\nDefault to slide title" + }, "transition": { "anyOf": [ { @@ -973,6 +983,16 @@ "type": "string", "description": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides", "markdownDescription": "Includes a markdown file\n\nSee https://sli.dev/guide/syntax.html#importing-slides" + }, + "timesplit": { + "type": "string", + "description": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)", + "markdownDescription": "Set time split for the end of the slide\n\nAccepts:\n- 10:05\n- 10m5s\n- +10s (relative to the previous point)" + }, + "timesplitTitle": { + "type": "string", + "description": "Set title for the time split\n\nDefault to slide title", + "markdownDescription": "Set title for the time split\n\nDefault to slide title" } } } From bb2c808c993e54b51065728d962b0ed8f8ce2db0 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Mon, 20 Oct 2025 21:40:08 +0900 Subject: [PATCH 04/10] feat: improve timer --- packages/client/composables/useTimer.ts | 66 ++++++++++++++++--- packages/client/internals/TimerInlined.vue | 35 ++++++++++ packages/client/pages/presenter.vue | 22 +------ packages/client/state/shared.ts | 6 ++ .../parser/src/timesplit/timesplit.test.ts | 2 +- packages/parser/src/timesplit/timesplit.ts | 10 +-- packages/slidev/node/vite/serverRef.ts | 4 ++ 7 files changed, 110 insertions(+), 35 deletions(-) create mode 100644 packages/client/internals/TimerInlined.vue diff --git a/packages/client/composables/useTimer.ts b/packages/client/composables/useTimer.ts index 18f6645964..5364f1b1c9 100644 --- a/packages/client/composables/useTimer.ts +++ b/packages/client/composables/useTimer.ts @@ -1,20 +1,68 @@ import { useInterval } from '@vueuse/core' -import { computed } from 'vue' +import { computed, toRef } from 'vue' +import { sharedState } from '../state/shared' export function useTimer() { - const { counter, isActive, reset, pause, resume } = useInterval(1000, { controls: true }) + const interval = useInterval(100, { controls: true }) + const state = toRef(sharedState, 'timerStatus') const timer = computed(() => { - const passed = counter.value - const sec = Math.floor(passed % 60).toString().padStart(2, '0') - const min = Math.floor(passed / 60).toString().padStart(2, '0') - return `${min}:${sec}` + if (sharedState.timerStatus === 'stopped' && sharedState.timerStartedAt === 0) + return { h: '', m: '-', s: '--', ms: '-' } + // 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 (h === '0') + h = '' + let min = Math.floor(passed / 1000 / 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 } }) + function reset() { + interval.pause() + sharedState.timerStatus = 'stopped' + sharedState.timerStartedAt = 0 + sharedState.timerPausedAt = 0 + } + + function resume() { + if (sharedState.timerStatus === 'stopped') { + sharedState.timerStatus = 'running' + sharedState.timerStartedAt = Date.now() + } + else if (sharedState.timerStatus === 'paused') { + sharedState.timerStatus = 'running' + sharedState.timerStartedAt = Date.now() - (sharedState.timerPausedAt - sharedState.timerStartedAt) + } + interval.resume() + } + + function pause() { + sharedState.timerStatus = 'paused' + sharedState.timerPausedAt = Date.now() + interval.pause() + } + + function toggle() { + if (sharedState.timerStatus === 'running') { + pause() + } + else { + resume() + } + } + return { + state, timer, - isTimerActive: isActive, - resetTimer: reset, - toggleTimer: () => (isActive.value ? pause() : resume()), + reset, + toggle, + resume, + pause, } } diff --git a/packages/client/internals/TimerInlined.vue b/packages/client/internals/TimerInlined.vue new file mode 100644 index 0000000000..1575e183b7 --- /dev/null +++ b/packages/client/internals/TimerInlined.vue @@ -0,0 +1,35 @@ + + + diff --git a/packages/client/pages/presenter.vue b/packages/client/pages/presenter.vue index 12b4fefd6d..2687e7b99d 100644 --- a/packages/client/pages/presenter.vue +++ b/packages/client/pages/presenter.vue @@ -6,7 +6,6 @@ import { createClicksContextBase } from '../composables/useClicks' import { useDrawings } from '../composables/useDrawings' import { useNav } from '../composables/useNav' import { useSwipeControls } from '../composables/useSwipeControls' -import { useTimer } from '../composables/useTimer' import { useWakeLock } from '../composables/useWakeLock' import { slidesTitle } from '../env' import ClicksSlider from '../internals/ClicksSlider.vue' @@ -23,6 +22,7 @@ import SegmentControl from '../internals/SegmentControl.vue' import SlideContainer from '../internals/SlideContainer.vue' import SlidesShow from '../internals/SlidesShow.vue' import SlideWrapper from '../internals/SlideWrapper.vue' +import TimerInlined from '../internals/TimerInlined.vue' import { onContextMenu } from '../logic/contextMenu' import { registerShortcuts } from '../logic/shortcuts' import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state' @@ -52,8 +52,6 @@ useHead({ title: `Presenter - ${slidesTitle}` }) const notesEditing = ref(false) -const { timer, isTimerActive, resetTimer, toggleTimer } = useTimer() - const clicksCtxMap = computed(() => slides.value.map((route) => { const clicks = ref(0) return { @@ -210,23 +208,7 @@ onMounted(() => {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ timer }} -
-
+
diff --git a/packages/client/state/shared.ts b/packages/client/state/shared.ts index bb48b2960f..f0de911449 100644 --- a/packages/client/state/shared.ts +++ b/packages/client/state/shared.ts @@ -6,6 +6,9 @@ export interface SharedState { page: number clicks: number clicksTotal: number + timerStatus: 'stopped' | 'running' | 'paused' + timerStartedAt: number + timerPausedAt: number cursor?: { x: number @@ -23,6 +26,9 @@ const { init, onPatch, onUpdate, patch, state } = createSyncState(s page: 1, clicks: 0, clicksTotal: 0, + timerStatus: 'stopped', + timerStartedAt: 0, + timerPausedAt: 0, }) export { diff --git a/packages/parser/src/timesplit/timesplit.test.ts b/packages/parser/src/timesplit/timesplit.test.ts index 27e6804d42..d88af47c58 100644 --- a/packages/parser/src/timesplit/timesplit.test.ts +++ b/packages/parser/src/timesplit/timesplit.test.ts @@ -11,11 +11,11 @@ describe('parseTimesplits', () => { ])).toMatchInlineSnapshot(` [ { - "name": "[start]", "noEnd": 1, "noStart": 0, "timestampEnd": 10, "timestampStart": 0, + "title": "[start]", }, { "noEnd": 5, diff --git a/packages/parser/src/timesplit/timesplit.ts b/packages/parser/src/timesplit/timesplit.ts index 075da1d969..5d7afea639 100644 --- a/packages/parser/src/timesplit/timesplit.ts +++ b/packages/parser/src/timesplit/timesplit.ts @@ -3,7 +3,7 @@ import { parseTimestampString } from './timestamp' export interface TimesplitInput { no: number timesplit: string - name?: string + title?: string } export interface TimesplitOutput { @@ -11,7 +11,7 @@ export interface TimesplitOutput { timestampEnd: number noStart: number noEnd: number - name?: string + title?: string } export function parseTimesplits(inputs: TimesplitInput[]): TimesplitOutput[] { @@ -22,7 +22,7 @@ export function parseTimesplits(inputs: TimesplitInput[]): TimesplitOutput[] { timestampEnd: ts, noStart: 0, noEnd: 0, - name: '[start]', + title: '[start]', } outputs.push(current) for (const input of inputs) { @@ -35,8 +35,8 @@ export function parseTimesplits(inputs: TimesplitInput[]): TimesplitOutput[] { } current.timestampEnd = end current.noEnd = input.no - if (input.name) { - current.name = input.name + if (input.title) { + current.title = input.title } ts = end current = { diff --git a/packages/slidev/node/vite/serverRef.ts b/packages/slidev/node/vite/serverRef.ts index 84c5cd3701..bd4268b801 100644 --- a/packages/slidev/node/vite/serverRef.ts +++ b/packages/slidev/node/vite/serverRef.ts @@ -14,6 +14,9 @@ export async function createServerRefPlugin( nav: { page: 0, clicks: 0, + timerStatus: 'stopped', + timerStartedAt: 0, + timerPausedAt: 0, }, drawings: await loadDrawings(options), snapshots: await loadSnapshots(options), @@ -21,6 +24,7 @@ export async function createServerRefPlugin( }, onChanged(key, data, patch, timestamp) { pluginOptions.serverRef?.onChanged?.(key, data, patch, timestamp) + if (options.data.config.drawings.persist && key === 'drawings') writeDrawings(options, patch ?? data) From f39d50efe740acbd9eb6e77ef8a1caf42ce1b94f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:42:09 +0000 Subject: [PATCH 05/10] [autofix.ci] apply automated fixes --- docs/features/notes-auto-ruby.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/features/notes-auto-ruby.md b/docs/features/notes-auto-ruby.md index 6321e2c643..6b7d685cde 100644 --- a/docs/features/notes-auto-ruby.md +++ b/docs/features/notes-auto-ruby.md @@ -28,4 +28,3 @@ notesAutoRuby: And the notes will be rendered as:

私は日本語ni hon go勉強べんきょうしています。

- From b18cb6f92167e9dd3a5d5de2fb110da60632b3d9 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Wed, 22 Oct 2025 14:01:01 +0900 Subject: [PATCH 06/10] feat: progress bar --- .../client/internals/CurrentProgressBar.vue | 33 +++++++++++++++++++ packages/client/pages/notes.vue | 10 +++--- packages/client/pages/presenter.vue | 16 ++------- 3 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 packages/client/internals/CurrentProgressBar.vue diff --git a/packages/client/internals/CurrentProgressBar.vue b/packages/client/internals/CurrentProgressBar.vue new file mode 100644 index 0000000000..d1559f4b45 --- /dev/null +++ b/packages/client/internals/CurrentProgressBar.vue @@ -0,0 +1,33 @@ + + + diff --git a/packages/client/pages/notes.vue b/packages/client/pages/notes.vue index 37647163a2..b1f670a16e 100644 --- a/packages/client/pages/notes.vue +++ b/packages/client/pages/notes.vue @@ -6,6 +6,7 @@ import { createClicksContextBase } from '../composables/useClicks' import { useNav } from '../composables/useNav' import { slidesTitle } from '../env' import ClicksSlider from '../internals/ClicksSlider.vue' +import CurrentProgressBar from '../internals/CurrentProgressBar.vue' import IconButton from '../internals/IconButton.vue' import Modal from '../internals/Modal.vue' import NoteDisplay from '../internals/NoteDisplay.vue' @@ -58,14 +59,11 @@ const clicksContext = computed(() => {
-
-
+
+
{