Spaces:
No application file
No application file
| 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<void>; | |
| addSnapshot: () => Promise<void>; | |
| undo: () => Promise<void>; | |
| redo: () => Promise<void>; | |
| } | |
| /** | |
| * 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<SnapshotState>((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 }); | |
| }, | |
| })); | |