snapshot.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { create } from 'zustand';
  2. import type { IndexableTypeArray } from 'dexie';
  3. import { db, type Snapshot } from '@/lib/utils/database';
  4. import { useStageStore } from './stage';
  5. import type { Scene } from '@/lib/types/stage';
  6. export interface SnapshotState {
  7. // State
  8. snapshotCursor: number; // Snapshot pointer
  9. snapshotLength: number; // Snapshot count
  10. // Computed
  11. canUndo: () => boolean;
  12. canRedo: () => boolean;
  13. // Actions
  14. setSnapshotCursor: (cursor: number) => void;
  15. setSnapshotLength: (length: number) => void;
  16. initSnapshotDatabase: () => Promise<void>;
  17. addSnapshot: () => Promise<void>;
  18. undo: () => Promise<void>;
  19. redo: () => Promise<void>;
  20. }
  21. /**
  22. * Snapshot store for undo/redo functionality
  23. * Based on PPTist's snapshot store, migrated to Zustand
  24. *
  25. * Uses IndexedDB (via Dexie) to store snapshot history
  26. */
  27. export const useSnapshotStore = create<SnapshotState>((set, get) => ({
  28. // Initial state
  29. snapshotCursor: -1,
  30. snapshotLength: 0,
  31. // Computed properties
  32. canUndo: () => get().snapshotCursor > 0,
  33. canRedo: () => get().snapshotCursor < get().snapshotLength - 1,
  34. // Actions
  35. setSnapshotCursor: (cursor: number) => set({ snapshotCursor: cursor }),
  36. setSnapshotLength: (length: number) => set({ snapshotLength: length }),
  37. /**
  38. * Initialize snapshot database with current state
  39. */
  40. initSnapshotDatabase: async () => {
  41. const stageStore = useStageStore.getState();
  42. const newFirstSnapshot = {
  43. index: stageStore.getSceneIndex(stageStore.currentSceneId || ''),
  44. slides: JSON.parse(JSON.stringify(stageStore.scenes)),
  45. };
  46. await db.snapshots.add(newFirstSnapshot);
  47. set({
  48. snapshotCursor: 0,
  49. snapshotLength: 1,
  50. });
  51. },
  52. /**
  53. * Add a new snapshot to the history
  54. * Handles snapshot length limit and cursor position
  55. */
  56. addSnapshot: async () => {
  57. const stageStore = useStageStore.getState();
  58. const { snapshotCursor } = get();
  59. // Get all snapshot IDs from IndexedDB
  60. const allKeys = await db.snapshots.orderBy('id').keys();
  61. let needDeleteKeys: IndexableTypeArray = [];
  62. // If cursor is not at the end, delete all snapshots after cursor
  63. // This happens when user undoes multiple times then performs a new action
  64. if (snapshotCursor >= 0 && snapshotCursor < allKeys.length - 1) {
  65. needDeleteKeys = allKeys.slice(snapshotCursor + 1);
  66. }
  67. // Add new snapshot
  68. const snapshot = {
  69. index: stageStore.getSceneIndex(stageStore.currentSceneId || ''),
  70. slides: JSON.parse(JSON.stringify(stageStore.scenes)),
  71. };
  72. await db.snapshots.add(snapshot);
  73. // Calculate new snapshot length
  74. let snapshotLength = allKeys.length - needDeleteKeys.length + 1;
  75. // Enforce snapshot length limit
  76. const snapshotLengthLimit = 20;
  77. if (snapshotLength > snapshotLengthLimit) {
  78. needDeleteKeys.push(allKeys[0]);
  79. snapshotLength--;
  80. }
  81. // Maintain page focus after undo: set the second-to-last snapshot's index to current scene
  82. // https://github.com/pipipi-pikachu/PPTist/issues/27
  83. if (snapshotLength >= 2) {
  84. const currentSceneIndex = stageStore.getSceneIndex(stageStore.currentSceneId || '');
  85. await db.snapshots.update(allKeys[snapshotLength - 2] as number, {
  86. index: currentSceneIndex,
  87. });
  88. }
  89. // Delete obsolete snapshots
  90. await db.snapshots.bulkDelete(needDeleteKeys as number[]);
  91. set({
  92. snapshotCursor: snapshotLength - 1,
  93. snapshotLength,
  94. });
  95. },
  96. /**
  97. * Undo: restore previous snapshot
  98. */
  99. undo: async () => {
  100. const { snapshotCursor } = get();
  101. if (snapshotCursor <= 0) return;
  102. const stageStore = useStageStore.getState();
  103. const newSnapshotCursor = snapshotCursor - 1;
  104. const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray();
  105. const snapshot = snapshots[newSnapshotCursor];
  106. const { index, slides } = snapshot;
  107. const sceneIndex = index > slides.length - 1 ? slides.length - 1 : index;
  108. // Restore scenes and current scene
  109. stageStore.setScenes(slides as unknown as Scene[]); // Type assertion needed due to Slide vs Scene difference
  110. if (slides[sceneIndex]) {
  111. stageStore.setCurrentSceneId(slides[sceneIndex].id);
  112. }
  113. set({ snapshotCursor: newSnapshotCursor });
  114. },
  115. /**
  116. * Redo: restore next snapshot
  117. */
  118. redo: async () => {
  119. const { snapshotCursor, snapshotLength } = get();
  120. if (snapshotCursor >= snapshotLength - 1) return;
  121. const stageStore = useStageStore.getState();
  122. const newSnapshotCursor = snapshotCursor + 1;
  123. const snapshots: Snapshot[] = await db.snapshots.orderBy('id').toArray();
  124. const snapshot = snapshots[newSnapshotCursor];
  125. const { index, slides } = snapshot;
  126. const sceneIndex = index > slides.length - 1 ? slides.length - 1 : index;
  127. // Restore scenes and current scene
  128. stageStore.setScenes(slides as unknown as Scene[]); // Type assertion needed due to Slide vs Scene difference
  129. if (slides[sceneIndex]) {
  130. stageStore.setCurrentSceneId(slides[sceneIndex].id);
  131. }
  132. set({ snapshotCursor: newSnapshotCursor });
  133. },
  134. }));