import { create } from 'zustand'; import type { IndexableTypeArray } from 'dexie'; import { db, type Snapshot } from '@/lib/utils/database'; import { useStageStore } from './stage'; import type { Scene } from '@/lib/types/stage'; export interface SnapshotState { // State snapshotCursor: number; // Snapshot pointer snapshotLength: number; // Snapshot count // Computed canUndo: () => boolean; canRedo: () => boolean; // Actions setSnapshotCursor: (cursor: number) => void; setSnapshotLength: (length: number) => void; initSnapshotDatabase: () => Promise; addSnapshot: () => Promise; undo: () => Promise; redo: () => Promise; } /** * Snapshot store for undo/redo functionality * Based on PPTist's snapshot store, migrated to Zustand * * Uses IndexedDB (via Dexie) to store snapshot history */ export const useSnapshotStore = create((set, get) => ({ // Initial state snapshotCursor: -1, snapshotLength: 0, // Computed properties canUndo: () => get().snapshotCursor > 0, canRedo: () => get().snapshotCursor < get().snapshotLength - 1, // Actions setSnapshotCursor: (cursor: number) => set({ snapshotCursor: cursor }), setSnapshotLength: (length: number) => set({ snapshotLength: length }), /** * Initialize snapshot database with current state */ initSnapshotDatabase: async () => { const stageStore = useStageStore.getState(); const newFirstSnapshot = { index: stageStore.getSceneIndex(stageStore.currentSceneId || ''), slides: JSON.parse(JSON.stringify(stageStore.scenes)), }; await db.snapshots.add(newFirstSnapshot); set({ snapshotCursor: 0, snapshotLength: 1, }); }, /** * Add a new snapshot to the history * Handles snapshot length limit and cursor position */ addSnapshot: async () => { const stageStore = useStageStore.getState(); const { snapshotCursor } = get(); // Get all snapshot IDs from IndexedDB const allKeys = await db.snapshots.orderBy('id').keys(); let needDeleteKeys: IndexableTypeArray = []; // If cursor is not at the end, delete all snapshots after cursor // This happens when user undoes multiple times then performs a new action if (snapshotCursor >= 0 && snapshotCursor < allKeys.length - 1) { needDeleteKeys = allKeys.slice(snapshotCursor + 1); } // Add new snapshot const snapshot = { index: stageStore.getSceneIndex(stageStore.currentSceneId || ''), slides: JSON.parse(JSON.stringify(stageStore.scenes)), }; await db.snapshots.add(snapshot); // Calculate new snapshot length let snapshotLength = allKeys.length - needDeleteKeys.length + 1; // Enforce snapshot length limit const snapshotLengthLimit = 20; if (snapshotLength > snapshotLengthLimit) { needDeleteKeys.push(allKeys[0]); snapshotLength--; } // Maintain page focus after undo: set the second-to-last snapshot's index to current scene // https://github.com/pipipi-pikachu/PPTist/issues/27 if (snapshotLength >= 2) { const currentSceneIndex = stageStore.getSceneIndex(stageStore.currentSceneId || ''); await db.snapshots.update(allKeys[snapshotLength - 2] as number, { index: currentSceneIndex, }); } // Delete obsolete snapshots await db.snapshots.bulkDelete(needDeleteKeys as number[]); set({ snapshotCursor: snapshotLength - 1, snapshotLength, }); }, /** * Undo: restore previous snapshot */ undo: async () => { const { snapshotCursor } = get(); if (snapshotCursor <= 0) return; const stageStore = useStageStore.getState(); const newSnapshotCursor = snapshotCursor - 1; const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray(); const snapshot = snapshots[newSnapshotCursor]; const { index, slides } = snapshot; const sceneIndex = index > slides.length - 1 ? slides.length - 1 : index; // Restore scenes and current scene stageStore.setScenes(slides as unknown as Scene[]); // Type assertion needed due to Slide vs Scene difference if (slides[sceneIndex]) { stageStore.setCurrentSceneId(slides[sceneIndex].id); } set({ snapshotCursor: newSnapshotCursor }); }, /** * Redo: restore next snapshot */ redo: async () => { const { snapshotCursor, snapshotLength } = get(); if (snapshotCursor >= snapshotLength - 1) return; const stageStore = useStageStore.getState(); const newSnapshotCursor = snapshotCursor + 1; const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray(); const snapshot = snapshots[newSnapshotCursor]; const { index, slides } = snapshot; const sceneIndex = index > slides.length - 1 ? slides.length - 1 : index; // Restore scenes and current scene stageStore.setScenes(slides as unknown as Scene[]); // Type assertion needed due to Slide vs Scene difference if (slides[sceneIndex]) { stageStore.setCurrentSceneId(slides[sceneIndex].id); } set({ snapshotCursor: newSnapshotCursor }); }, }));