|
|
|
|
| import React, { |
| createContext, |
| useContext, |
| useMemo, |
| useCallback, |
| useSyncExternalStore, |
| useRef, |
| useEffect, |
| } from 'react'; |
| import { useStageStore } from '@/lib/store/stage'; |
| import type { Scene } from '@/lib/types/stage'; |
| import { produce } from 'immer'; |
|
|
| interface SceneContextValue<T = unknown> { |
| sceneId: string; |
| sceneType: Scene['type']; |
| sceneData: T; |
| updateSceneData: (updater: (draft: T) => void) => void; |
| |
| subscribe: (callback: () => void) => () => void; |
| getSnapshot: () => T; |
| } |
|
|
| const SceneContext = createContext<SceneContextValue | null>(null); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function SceneProvider({ children }: { children: React.ReactNode }) { |
| |
| const currentScene = useStageStore((state) => { |
| if (!state.currentSceneId) return null; |
| return state.scenes.find((s) => s.id === state.currentSceneId) || null; |
| }); |
|
|
| const updateScene = useStageStore((state) => state.updateScene); |
|
|
| const sceneId = currentScene?.id || ''; |
| const sceneType = currentScene?.type || 'slide'; |
| const sceneData = currentScene?.content || null; |
|
|
| |
| const listenersRef = useRef(new Set<() => void>()); |
|
|
| |
| const subscribe = useCallback((callback: () => void) => { |
| listenersRef.current.add(callback); |
| return () => { |
| listenersRef.current.delete(callback); |
| }; |
| }, []); |
|
|
| |
| const getSnapshot = useCallback(() => { |
| return sceneData; |
| }, [sceneData]); |
|
|
| |
| useEffect(() => { |
| listenersRef.current.forEach((listener) => listener()); |
| }, [sceneData]); |
|
|
| |
| const updateSceneData = useCallback( |
| (updater: (draft: unknown) => void) => { |
| if (!currentScene) return; |
|
|
| const newContent = produce(currentScene.content, updater); |
| updateScene(currentScene.id, { |
| content: newContent, |
| }); |
| }, |
| [currentScene, updateScene], |
| ); |
|
|
| const value = useMemo( |
| () => ({ |
| sceneId, |
| sceneType, |
| sceneData, |
| updateSceneData, |
| subscribe, |
| getSnapshot, |
| }), |
| [sceneId, sceneType, sceneData, updateSceneData, subscribe, getSnapshot], |
| ); |
|
|
| |
| if (!currentScene) { |
| return null; |
| } |
|
|
| return <SceneContext.Provider value={value}>{children}</SceneContext.Provider>; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function useSceneData<T = unknown>(): SceneContextValue<T> { |
| const context = useContext(SceneContext); |
| if (!context) { |
| throw new Error('useSceneData must be used within SceneProvider'); |
| } |
| return context as SceneContextValue<T>; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function useSceneSelector<T = unknown, R = unknown>(selector: (data: T) => R): R { |
| const context = useContext(SceneContext); |
| if (!context) { |
| throw new Error('useSceneSelector must be used within SceneProvider'); |
| } |
|
|
| const { subscribe, getSnapshot } = context as SceneContextValue<T>; |
|
|
| |
| const selectorRef = useRef(selector); |
| const snapshotRef = useRef<R | undefined>(undefined); |
|
|
| |
| useEffect(() => { |
| selectorRef.current = selector; |
| }, [selector]); |
|
|
| |
| return useSyncExternalStore( |
| subscribe, |
| () => { |
| const snapshot = getSnapshot(); |
| const newValue = selectorRef.current(snapshot); |
|
|
| |
| if (snapshotRef.current !== undefined && shallowEqual(snapshotRef.current, newValue)) { |
| return snapshotRef.current; |
| } |
|
|
| snapshotRef.current = newValue; |
| return newValue; |
| }, |
| () => { |
| |
| const snapshot = getSnapshot(); |
| return selectorRef.current(snapshot); |
| }, |
| ); |
| } |
|
|
| |
| |
| |
| |
| function shallowEqual(a: unknown, b: unknown): boolean { |
| if (Object.is(a, b)) { |
| return true; |
| } |
|
|
| if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { |
| return false; |
| } |
|
|
| const objA = a as Record<string, unknown>; |
| const objB = b as Record<string, unknown>; |
| const keysA = Object.keys(objA); |
| const keysB = Object.keys(objB); |
|
|
| if (keysA.length !== keysB.length) { |
| return false; |
| } |
|
|
| for (const key of keysA) { |
| if (!Object.prototype.hasOwnProperty.call(objB, key) || !Object.is(objA[key], objB[key])) { |
| return false; |
| } |
| } |
|
|
| return true; |
| } |
|
|