import { create } from "zustand"; import { persist, type PersistStorage, type StorageValue } from "zustand/middleware"; import { nanoid } from "nanoid"; import { localForageStorage } from "@/lib/localforage-storage"; import type { CanvasBackgroundMode } from "@/lib/canvas-theme"; import type { CanvasAssistantSession, CanvasConnection, CanvasNodeData, ViewportTransform } from "../types"; export type CanvasProject = { id: string; title: string; createdAt: string; updatedAt: string; nodes: CanvasNodeData[]; connections: CanvasConnection[]; chatSessions: CanvasAssistantSession[]; activeChatId: string | null; backgroundMode: CanvasBackgroundMode; showImageInfo: boolean; viewport: ViewportTransform; }; type CanvasStore = { hydrated: boolean; projects: CanvasProject[]; createProject: (title?: string) => string; importProject: (project: Partial) => string; openProject: (id: string) => CanvasProject | null; renameProject: (id: string, title: string) => void; deleteProjects: (ids: string[]) => void; updateProject: (id: string, patch: Partial>) => void; }; const initialViewport: ViewportTransform = { x: 0, y: 0, k: 1 }; const CANVAS_STORE_KEY = "infinite-canvas:canvas_store"; type PersistedCanvasState = Pick; let saveTimer: ReturnType | null = null; let queuedPersistState: PersistedCanvasState | null = null; const canvasStorage: PersistStorage = { getItem: async (name) => { const value = await localForageStorage.getItem(name); if (!value) return null; const parsed = JSON.parse(value) as StorageValue; queuedPersistState = parsed.state as PersistedCanvasState; return parsed; }, setItem: (name, value) => { const nextState = value.state as PersistedCanvasState; if (queuedPersistState && queuedPersistState.projects === nextState.projects) return; queuedPersistState = nextState; if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { saveTimer = null; void localForageStorage.setItem(name, JSON.stringify(value)); }, 400); }, removeItem: (name) => localForageStorage.removeItem(name), }; export const useCanvasStore = create()( persist( (set, get) => ({ hydrated: false, projects: [], createProject: (title = "未命名画布") => { const now = new Date().toISOString(); const id = nanoid(); const project: CanvasProject = { id, title, createdAt: now, updatedAt: now, nodes: [], connections: [], chatSessions: [], activeChatId: null, backgroundMode: "lines", showImageInfo: false, viewport: initialViewport, }; set((state) => ({ projects: [project, ...state.projects] })); return id; }, importProject: (source) => { const now = new Date().toISOString(); const project: CanvasProject = { id: nanoid(), title: source.title || "导入画布", createdAt: source.createdAt || now, updatedAt: now, nodes: source.nodes || [], connections: source.connections || [], chatSessions: source.chatSessions || [], activeChatId: source.activeChatId || null, backgroundMode: source.backgroundMode || "lines", showImageInfo: source.showImageInfo || false, viewport: source.viewport || initialViewport, }; set((state) => ({ projects: [project, ...state.projects] })); return project.id; }, openProject: (id) => { return get().projects.find((item) => item.id === id) || null; }, renameProject: (id, title) => set((state) => ({ projects: state.projects.map((project) => (project.id === id ? { ...project, title: title.trim() || project.title, updatedAt: new Date().toISOString() } : project)), })), deleteProjects: (ids) => set((state) => { const projects = state.projects.filter((project) => !ids.includes(project.id)); return { projects }; }), updateProject: (id, patch) => set((state) => ({ projects: state.projects.map((project) => (project.id === id ? { ...project, ...patch, updatedAt: new Date().toISOString() } : project)), })), }), { name: CANVAS_STORE_KEY, storage: canvasStorage, partialize: (state) => ({ projects: state.projects, }) as StorageValue["state"], onRehydrateStorage: () => () => { useCanvasStore.setState({ hydrated: true }); }, }, ), );