diff --git a/apps/vscode-extension/package.json b/apps/vscode-extension/package.json index 3580f90a..d946d090 100644 --- a/apps/vscode-extension/package.json +++ b/apps/vscode-extension/package.json @@ -656,6 +656,24 @@ "title": "Toggle Redaction Mode", "category": "Demo Time", "icon": "$(eye-closed)" + }, + { + "command": "demo-time.toggleAutoProceed", + "title": "Toggle Auto-Proceed (Pause/Resume)", + "category": "Demo Time", + "icon": "$(debug-pause)" + }, + { + "command": "demo-time.pauseAutoProceed", + "title": "Pause Auto-Proceed", + "category": "Demo Time", + "icon": "$(debug-pause)" + }, + { + "command": "demo-time.resumeAutoProceed", + "title": "Resume Auto-Proceed", + "category": "Demo Time", + "icon": "$(play-circle)" } ], "menus": { @@ -727,68 +745,21 @@ { "command": "demo-time.continueDemo", "when": "demo-time.isWaitingForNext == true" - } - ], - "view/title": [ - { - "command": "demo-time.togglePresentationMode", - "when": "view == demo-time-scenes && !demo-time.presentation", - "group": "navigation@-1" - }, - { - "command": "demo-time.startCountdown", - "when": "view == demo-time-scenes && !demo-time.countdown && demo-time.showTimer", - "group": "navigation@0" - }, - { - "command": "demo-time.showPresenterView", - "when": "view == demo-time-scenes", - "group": "navigation@1" }, { - "command": "demo-time.reset", - "when": "view == demo-time-scenes", - "group": "navigation@2" + "command": "demo-time.toggleAutoProceed", + "when": "demo-time.autoProceedActive == true" }, { - "command": "demo-time.exportToPdf", - "when": "view == demo-time-scenes", - "group": "navigation@3" + "command": "demo-time.pauseAutoProceed", + "when": "demo-time.autoProceedActive == true && demo-time.autoProceedPaused != true" }, { - "command": "demo-time.treeviewFind", - "when": "view == demo-time-scenes", - "group": "navigation@4" - }, - { - "command": "demo-time.createDemoFile", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.docs", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.showOverview", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.showSettings", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.showProFeatures", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.showGallery", - "when": "view == demo-time-scenes" - }, - { - "command": "demo-time.openSupportTheProject", - "when": "view == demo-time-scenes" + "command": "demo-time.resumeAutoProceed", + "when": "demo-time.autoProceedActive == true && demo-time.autoProceedPaused == true" } ], + "view/title": [], "view/item/context": [ { "command": "demo-time.start", @@ -885,6 +856,12 @@ }, "views": { "demo-time-container": [ + { + "id": "demo-time-quick-actions", + "name": "Quick Actions", + "contextualTitle": "Demo Time Quick Actions", + "icon": "$(zap)" + }, { "id": "demo-time-scenes", "name": "Acts & Scenes", @@ -934,6 +911,11 @@ "command": "demo-time.hackerTyperNextChunk", "key": "down", "when": "demo-time.isHackerTyper == true" + }, + { + "command": "demo-time.toggleAutoProceed", + "key": "space", + "when": "demo-time.presentation && demo-time.autoProceedActive == true" } ], "viewsWelcome": [ diff --git a/apps/vscode-extension/src/constants/ContextKeys.ts b/apps/vscode-extension/src/constants/ContextKeys.ts index 13ec262f..f943cfab 100644 --- a/apps/vscode-extension/src/constants/ContextKeys.ts +++ b/apps/vscode-extension/src/constants/ContextKeys.ts @@ -12,4 +12,6 @@ export const ContextKeys = { isHackerTyper: 'demo-time.isHackerTyper', isWaitingForNext: 'demo-time.isWaitingForNext', isSponsor: 'demo-time.isSponsor', + autoProceedActive: 'demo-time.autoProceedActive', + autoProceedPaused: 'demo-time.autoProceedPaused', }; diff --git a/apps/vscode-extension/src/extension.ts b/apps/vscode-extension/src/extension.ts index a1abae69..6ad6afa1 100644 --- a/apps/vscode-extension/src/extension.ts +++ b/apps/vscode-extension/src/extension.ts @@ -23,6 +23,7 @@ import { } from './services'; import { DemoPanel } from './panels/DemoPanel'; import { ResourcesPanel } from './panels/ResourcesPanel'; +import { QuickActionsPanel } from './panels/QuickActionsPanel'; import { Preview } from './preview/Preview'; import { PresenterView } from './presenterView/PresenterView'; import { ConfigEditorProvider } from './providers/ConfigEditorProvider'; @@ -55,6 +56,7 @@ export async function activate(context: vscode.ExtensionContext) { // Services DecoratorService.register(); DemoPanel.register(); + QuickActionsPanel.register(); ResourcesPanel.register(); DemoRunner.registerCommands(); DemoCreator.registerCommands(); diff --git a/apps/vscode-extension/src/panels/QuickActionsPanel.ts b/apps/vscode-extension/src/panels/QuickActionsPanel.ts new file mode 100644 index 00000000..0eff6302 --- /dev/null +++ b/apps/vscode-extension/src/panels/QuickActionsPanel.ts @@ -0,0 +1,31 @@ +import { TreeView, window } from 'vscode'; +import { + QuickActionsTreeviewProvider, + QuickActionTreeItem, +} from '../providers/QuickActionsTreeviewProvider'; + +export class QuickActionsPanel { + private static treeView: TreeView; + private static quickActionsProvider: QuickActionsTreeviewProvider; + + public static register() { + QuickActionsPanel.quickActionsProvider = new QuickActionsTreeviewProvider(); + QuickActionsPanel.treeView = window.createTreeView('demo-time-quick-actions', { + treeDataProvider: QuickActionsPanel.quickActionsProvider, + }); + } + + public static update() { + QuickActionsPanel.quickActionsProvider.update(); + } + + public static updatePresentationMode(isPresentationMode: boolean) { + QuickActionsPanel.quickActionsProvider.setPresentationMode(isPresentationMode); + QuickActionsPanel.quickActionsProvider.update(); + } + + public static updateAutoProceed(isActive: boolean, isPaused: boolean, countdown: number) { + QuickActionsPanel.quickActionsProvider.setAutoProceedState(isActive, isPaused, countdown); + QuickActionsPanel.quickActionsProvider.update(); + } +} \ No newline at end of file diff --git a/apps/vscode-extension/src/providers/QuickActionsTreeviewProvider.ts b/apps/vscode-extension/src/providers/QuickActionsTreeviewProvider.ts new file mode 100644 index 00000000..ed470101 --- /dev/null +++ b/apps/vscode-extension/src/providers/QuickActionsTreeviewProvider.ts @@ -0,0 +1,179 @@ +import { + Event, + EventEmitter, + ProviderResult, + ThemeColor, + ThemeIcon, + TreeDataProvider, + TreeItem, + TreeItemCollapsibleState, +} from 'vscode'; +import { COMMAND } from '@demotime/common'; + +type QuickAction = { + label: string; + description: string; + icon: string; + color?: string; + command: string; +}; + +export class QuickActionsTreeviewProvider implements TreeDataProvider { + private isPresentationMode = false; + private isAutoProceedActive = false; + private isAutoProceedPaused = false; + private autoProceedCountdown = 0; + private _onDidChangeTreeData = new EventEmitter(); + public readonly onDidChangeTreeData: Event = + this._onDidChangeTreeData.event; + + public setPresentationMode(isPresentationMode: boolean): void { + this.isPresentationMode = isPresentationMode; + } + + public setAutoProceedState(isActive: boolean, isPaused: boolean, countdown: number): void { + this.isAutoProceedActive = isActive; + this.isAutoProceedPaused = isPaused; + this.autoProceedCountdown = countdown; + } + + getTreeItem(element: QuickActionTreeItem): TreeItem | Thenable { + return element; + } + + getChildren(): ProviderResult { + const presentationAction: QuickAction = this.isPresentationMode + ? { + label: 'Stop Presentation Mode', + description: 'Leave presentation mode', + icon: 'debug-stop', + color: 'charts.red', + command: COMMAND.togglePresentationMode, + } + : { + label: 'Start Presentation Mode', + description: 'Enter presentation mode', + icon: 'play', + color: 'charts.green', + command: COMMAND.togglePresentationMode, + }; + + const autoProceedAction: QuickAction | null = this.isAutoProceedActive + ? this.isAutoProceedPaused + ? { + label: 'Resume Auto-Proceed', + description: 'Resume auto-advance countdown', + icon: 'play-circle', + color: 'charts.green', + command: COMMAND.resumeAutoProceed, + } + : { + label: `Pause Auto-Proceed`, + description: `Next scene in ${this.autoProceedCountdown}s — click to pause`, + icon: 'debug-pause', + color: 'charts.yellow', + command: COMMAND.pauseAutoProceed, + } + : null; + + const actions: QuickAction[] = [ + presentationAction, + ...(autoProceedAction ? [autoProceedAction] : []), + { + label: 'Start Timer', + description: 'Start countdown timer for your session', + icon: 'watch', + color: 'charts.yellow', + command: COMMAND.startCountdown, + }, + { + label: 'Open Presenter View', + description: 'Show presenter notes and controls', + icon: 'open-preview', + command: COMMAND.showPresenterView, + }, + { + label: 'Reset Demo State', + description: 'Clear highlights and reset runner state', + icon: 'discard', + color: 'charts.orange', + command: COMMAND.reset, + }, + { + label: 'Export Slides to PDF', + description: 'Export your presentation to a PDF file', + icon: 'file-pdf', + command: COMMAND.exportToPdf, + }, + { + label: 'Find in Acts & Scenes', + description: 'Focus and search in the Acts & Scenes tree', + icon: 'search', + command: COMMAND.treeviewFind, + }, + { + label: 'Create Act File', + description: 'Create a new act file in the .demo folder', + icon: 'new-file', + command: COMMAND.createDemoFile, + }, + { + label: 'Open Overview', + description: 'Open the Demo Time overview panel', + icon: 'home', + command: COMMAND.showOverview, + }, + { + label: 'Open Settings', + description: 'Open Demo Time settings panel', + icon: 'settings-gear', + command: COMMAND.showSettings, + }, + { + label: 'Open PRO Features', + description: 'Discover or unlock PRO capabilities', + icon: 'lock', + color: 'charts.red', + command: COMMAND.showProFeatures, + }, + { + label: 'Open Documentation', + description: 'Read docs, guides, and references', + icon: 'book', + command: COMMAND.documentation, + }, + { + label: 'Support the Project', + description: 'Sponsor Demo Time development', + icon: 'heart', + color: 'charts.red', + command: COMMAND.openSupportTheProject, + }, + ]; + + return actions.map((action) => new QuickActionTreeItem(action)); + } + + update(): void { + this._onDidChangeTreeData.fire(undefined); + } +} + +export class QuickActionTreeItem extends TreeItem { + constructor(action: QuickAction) { + super(action.label, TreeItemCollapsibleState.None); + + this.label = action.label; + this.description = action.description; + this.tooltip = action.description; + this.iconPath = new ThemeIcon( + action.icon, + action.color ? new ThemeColor(action.color) : undefined, + ); + this.command = { + command: action.command, + title: action.label, + }; + this.contextValue = 'demo-time.quick-action'; + } +} \ No newline at end of file diff --git a/apps/vscode-extension/src/services/DemoRunner.ts b/apps/vscode-extension/src/services/DemoRunner.ts index 92ea363c..8953241b 100644 --- a/apps/vscode-extension/src/services/DemoRunner.ts +++ b/apps/vscode-extension/src/services/DemoRunner.ts @@ -15,6 +15,7 @@ import { workspace, } from 'vscode'; import { DemoPanel } from '../panels/DemoPanel'; +import { QuickActionsPanel } from '../panels/QuickActionsPanel'; import { getVariables, getFileContents, @@ -102,6 +103,12 @@ export class DemoRunner { private static currentDemoIndex: number = 0; private static currentStepIndex: number = 0; + // Auto-proceed state + private static autoProceedTimer: NodeJS.Timeout | undefined; + private static autoProceedCountdownInterval: NodeJS.Timeout | undefined; + private static isAutoProceedPaused = false; + private static autoProceedCountdown: number = 0; + /** * Registers the commands for the demo runner. */ @@ -126,6 +133,19 @@ export class DemoRunner { DemoRunner.toggleSelectionHighlight, ), ); + subscriptions.push( + commands.registerCommand(COMMAND.toggleAutoProceed, DemoRunner.toggleAutoProceed), + ); + subscriptions.push( + commands.registerCommand(COMMAND.pauseAutoProceed, () => + DemoRunner.setAutoProceedPaused(true), + ), + ); + subscriptions.push( + commands.registerCommand(COMMAND.resumeAutoProceed, () => + DemoRunner.setAutoProceedPaused(false), + ), + ); window.onDidChangeActiveTextEditor(async (editor) => { if (editor && editor.document.fileName === DemoRunner.crntFilePath) { @@ -204,6 +224,65 @@ export class DemoRunner { return DemoRunner.isPresentationMode; } + public static getIsAutoProceedPaused(): boolean { + return DemoRunner.isAutoProceedPaused; + } + + public static getAutoProceedCountdown(): number { + return DemoRunner.autoProceedCountdown; + } + + private static clearAutoProceedTimers(): void { + if (DemoRunner.autoProceedTimer) { + clearTimeout(DemoRunner.autoProceedTimer); + DemoRunner.autoProceedTimer = undefined; + } + if (DemoRunner.autoProceedCountdownInterval) { + clearInterval(DemoRunner.autoProceedCountdownInterval); + DemoRunner.autoProceedCountdownInterval = undefined; + } + DemoRunner.autoProceedCountdown = 0; + } + + private static startAutoProceedTimer(delaySeconds: number): void { + DemoRunner.clearAutoProceedTimers(); + DemoRunner.autoProceedCountdown = delaySeconds; + + DemoRunner.autoProceedCountdownInterval = setInterval(() => { + DemoRunner.autoProceedCountdown = Math.max(0, DemoRunner.autoProceedCountdown - 1); + QuickActionsPanel.updateAutoProceed(true, DemoRunner.isAutoProceedPaused, DemoRunner.autoProceedCountdown); + }, 1000); + + DemoRunner.autoProceedTimer = setTimeout(async () => { + DemoRunner.clearAutoProceedTimers(); + QuickActionsPanel.updateAutoProceed(false, false, 0); + await commands.executeCommand(COMMAND.start); + }, delaySeconds * 1000); + } + + private static async setAutoProceedPaused(paused: boolean): Promise { + DemoRunner.isAutoProceedPaused = paused; + await setContext(ContextKeys.autoProceedPaused, paused); + + if (paused) { + DemoRunner.clearAutoProceedTimers(); + QuickActionsPanel.updateAutoProceed(true, true, 0); + DemoPanel.updateMessage('Auto-proceed paused'); + } else { + // Resume with the full duration of the current scene + const demo = DemoRunner.currentDemo; + if (demo?.autoAdvanceAfter) { + DemoRunner.startAutoProceedTimer(demo.autoAdvanceAfter); + QuickActionsPanel.updateAutoProceed(true, false, demo.autoAdvanceAfter); + DemoPanel.updateMessage('Presentation mode enabled'); + } + } + } + + private static async toggleAutoProceed(): Promise { + await DemoRunner.setAutoProceedPaused(!DemoRunner.isAutoProceedPaused); + } + /** * Retrieves the current version of the executing act file. * @@ -260,6 +339,7 @@ export class DemoRunner { typeof enable !== 'undefined' ? enable : !DemoRunner.isPresentationMode; DemoStatusBar.setPresenting(DemoRunner.isPresentationMode); await setContext(ContextKeys.presentation, DemoRunner.isPresentationMode); + QuickActionsPanel.updatePresentationMode(DemoRunner.isPresentationMode); PresenterView.postMessage( WebViewMessages.toWebview.updatePresentationStarted, DemoRunner.isPresentationMode, @@ -290,6 +370,12 @@ export class DemoRunner { await AnalyticsService.endSession(); } + DemoRunner.clearAutoProceedTimers(); + DemoRunner.isAutoProceedPaused = false; + await setContext(ContextKeys.autoProceedPaused, false); + await setContext(ContextKeys.autoProceedActive, false); + QuickActionsPanel.updateAutoProceed(false, false, 0); + const ext = Extension.getInstance(); const resetContent = Object.assign({}, DEFAULT_START_VALUE); await ext.setState(StateKeys.executingDemoFile, resetContent); @@ -327,6 +413,9 @@ export class DemoRunner { return; } + // Cancel any running auto-proceed timer (manual advance or new scene) + DemoRunner.clearAutoProceedTimers(); + const executingFile = await DemoRunner.getExecutedDemoFile(); const demoFile = await DemoRunner.getDemoFile(item); @@ -361,6 +450,22 @@ export class DemoRunner { } if (!nextDemo) { + // Auto-loop: restart from the first scene if configured + if (fileDemo?.autoLoop) { + const enabledDemos = demos.filter((d) => !d.disabled); + const missingTimings = enabledDemos.filter((d) => !d.autoAdvanceAfter); + if (missingTimings.length > 0) { + Notifications.error( + `Auto-loop blocked: ${missingTimings.length} scene(s) missing 'autoAdvanceAfter': ${missingTimings.map((d) => `"${d.title}"`).join(', ')}`, + ); + return; + } + executingFile.demo = []; + await DemoRunner.setExecutedDemoFile(executingFile); + DemoRunner.start(item); + return; + } + // Check if there is a next act file const nextFile = await getNextDemoFile(demoFile); if (!nextFile) { @@ -436,6 +541,20 @@ export class DemoRunner { !!demos[followingDemoIdx].steps?.some((s: Step) => s.action === Action.Highlight); NotesService.showNotes(nextDemo); + + // Start auto-proceed countdown if configured for this scene + if (nextDemo.autoAdvanceAfter && nextDemo.autoAdvanceAfter > 0) { + await setContext(ContextKeys.autoProceedActive, true); + if (!DemoRunner.isAutoProceedPaused) { + DemoRunner.startAutoProceedTimer(nextDemo.autoAdvanceAfter); + QuickActionsPanel.updateAutoProceed(true, false, nextDemo.autoAdvanceAfter); + } else { + QuickActionsPanel.updateAutoProceed(true, true, 0); + } + } else { + await setContext(ContextKeys.autoProceedActive, false); + QuickActionsPanel.updateAutoProceed(false, false, 0); + } } /** @@ -454,6 +573,11 @@ export class DemoRunner { return; } + // Cancel any running auto-proceed timer when going back + DemoRunner.clearAutoProceedTimers(); + await setContext(ContextKeys.autoProceedActive, false); + QuickActionsPanel.updateAutoProceed(false, false, 0); + const executingFile = await DemoRunner.getExecutedDemoFile(); const filePath = executingFile.filePath; if (!filePath) { diff --git a/packages/common/src/constants/Command.ts b/packages/common/src/constants/Command.ts index 833a5900..79b7c939 100644 --- a/packages/common/src/constants/Command.ts +++ b/packages/common/src/constants/Command.ts @@ -76,4 +76,8 @@ export const COMMAND = { analyticsToggle: `${EXTENSION_NAME}.analytics.toggle`, // Redaction toggleRedaction: `${EXTENSION_NAME}.toggleRedaction`, + // Auto-proceed + toggleAutoProceed: `${EXTENSION_NAME}.toggleAutoProceed`, + pauseAutoProceed: `${EXTENSION_NAME}.pauseAutoProceed`, + resumeAutoProceed: `${EXTENSION_NAME}.resumeAutoProceed`, }; diff --git a/packages/common/src/models/Demos.ts b/packages/common/src/models/Demos.ts index 780e2a6b..2fec68f1 100644 --- a/packages/common/src/models/Demos.ts +++ b/packages/common/src/models/Demos.ts @@ -17,6 +17,7 @@ export interface ActConfig { version: 3; timer?: number; engageTime?: EngageTimeConfig; + autoLoop?: boolean; scenes: Scene[]; } @@ -28,6 +29,7 @@ export interface DemoConfig { version?: Version; timer?: number; engageTime?: EngageTimeConfig; + autoLoop?: boolean; demos: Demo[]; } @@ -49,6 +51,7 @@ export interface Scene { icons?: Icons; notes?: Notes; disabled?: boolean; + autoAdvanceAfter?: number; } // Version 1 & 2: Legacy Demo structure @@ -60,6 +63,7 @@ export interface Demo { icons?: Icons; notes?: Notes; disabled?: boolean; + autoAdvanceAfter?: number; } export interface Notes { diff --git a/packages/common/src/utils/demoNormalizer.ts b/packages/common/src/utils/demoNormalizer.ts index 308c486e..667196de 100644 --- a/packages/common/src/utils/demoNormalizer.ts +++ b/packages/common/src/utils/demoNormalizer.ts @@ -12,6 +12,7 @@ export function normalizeActConfig(config: ActConfig): DemoConfig { version: config.version, timer: config.timer, engageTime: config.engageTime, + autoLoop: config.autoLoop, demos: (config.scenes || []).map((scene) => sceneToDemo(scene)), }; } @@ -28,6 +29,7 @@ function sceneToDemo(scene: Scene): Demo { icons: scene.icons, notes: scene.notes, disabled: scene.disabled, + autoAdvanceAfter: scene.autoAdvanceAfter, }; } @@ -88,5 +90,6 @@ function demoToScene(demo: Demo): Scene { icons: demo.icons, notes: demo.notes, disabled: demo.disabled, + autoAdvanceAfter: demo.autoAdvanceAfter, }; }