File size: 5,132 Bytes
f56a29b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | 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 });
},
}));
|