import { useEffect, useMemo, useRef, useState } from 'react'; import { ipcChannels } from '../../../shared/ipc/contracts'; import type { AssetId, GraphNodeId, Project, ProjectId, Scene, SceneId } from '../../../shared/types'; import { getDndApi } from '../../shared/dndApi'; type ProjectSummary = { id: ProjectId; name: string; updatedAt: string; fileName: string }; type State = { projects: ProjectSummary[]; project: Project | null; selectedSceneId: SceneId | null; }; type Actions = { refreshProjects: () => Promise; createProject: (name: string) => Promise; openProject: (id: ProjectId) => Promise; closeProject: () => Promise; createScene: () => Promise; selectScene: (id: SceneId) => Promise; updateScene: ( sceneId: SceneId, patch: { title?: string; description?: string; previewAssetId?: AssetId | null; previewAssetType?: 'image' | 'video' | null; previewVideoAutostart?: boolean; previewRotationDeg?: 0 | 90 | 180 | 270; settings?: Partial; media?: Partial; layout?: { x: number; y: number }; }, ) => Promise; updateConnections: (sceneId: SceneId, connections: SceneId[]) => Promise; importMediaToScene: (sceneId: SceneId) => Promise; importScenePreview: (sceneId: SceneId) => Promise; clearScenePreview: (sceneId: SceneId) => Promise; updateSceneGraphNodePosition: (nodeId: GraphNodeId, x: number, y: number) => Promise; addSceneGraphNode: (sceneId: SceneId, x: number, y: number) => Promise; removeSceneGraphNode: (nodeId: GraphNodeId) => Promise; addSceneGraphEdge: (sourceGraphNodeId: GraphNodeId, targetGraphNodeId: GraphNodeId) => Promise; removeSceneGraphEdge: (edgeId: string) => Promise; setSceneGraphNodeStart: (graphNodeId: GraphNodeId | null) => Promise; deleteScene: (sceneId: SceneId) => Promise; renameProject: (name: string, fileBaseName: string) => Promise; importProject: () => Promise; exportProject: (projectId: ProjectId) => Promise; deleteProject: (projectId: ProjectId) => Promise; }; function randomId(prefix: string): string { return `${prefix}_${Math.random().toString(16).slice(2)}_${Date.now().toString(16)}`; } export function useProjectState(licenseActive: boolean): readonly [State, Actions] { const api = getDndApi(); const [state, setState] = useState({ projects: [], project: null, selectedSceneId: null }); const projectRef = useRef(null); useEffect(() => { projectRef.current = state.project; }, [state.project]); const actions = useMemo(() => { const refreshProjects = async () => { const res = await api.invoke(ipcChannels.project.list, {}); setState((s) => ({ ...s, projects: res.projects })); }; const createProject = async (name: string) => { const res = await api.invoke(ipcChannels.project.create, { name }); setState((s) => ({ ...s, project: res.project, selectedSceneId: res.project.currentSceneId })); await refreshProjects(); }; const openProject = async (id: ProjectId) => { const res = await api.invoke(ipcChannels.project.open, { projectId: id }); setState((s) => ({ ...s, project: res.project, selectedSceneId: res.project.currentSceneId })); }; const closeProject = async () => { setState((s) => ({ ...s, project: null, selectedSceneId: null })); if (licenseActive) await refreshProjects(); }; const createScene = async () => { const p = projectRef.current; if (!p) return; const sceneId = randomId('scene') as SceneId; const scene: Scene = { id: sceneId, title: `Новая сцена`, description: '', previewAssetId: null, previewAssetType: null, previewVideoAutostart: false, previewRotationDeg: 0, media: { videos: [], audios: [] }, settings: { autoplayVideo: false, autoplayAudio: true, loopVideo: true, loopAudio: true }, connections: [], layout: { x: 0, y: 0 }, }; await api.invoke(ipcChannels.project.updateScene, { sceneId, patch: { title: scene.title, description: scene.description, media: scene.media, settings: scene.settings, layout: scene.layout, previewAssetId: scene.previewAssetId, previewAssetType: scene.previewAssetType, previewVideoAutostart: scene.previewVideoAutostart, }, }); await api.invoke(ipcChannels.project.setCurrentScene, { sceneId }); const res = await api.invoke(ipcChannels.project.get, {}); setState((s) => ({ ...s, project: res.project, selectedSceneId: sceneId })); }; const selectScene = async (id: SceneId) => { setState((s) => ({ ...s, selectedSceneId: id })); await api.invoke(ipcChannels.project.setCurrentScene, { sceneId: id }); }; const updateScene = async ( sceneId: SceneId, patch: { title?: string; description?: string; previewAssetId?: AssetId | null; previewAssetType?: 'image' | 'video' | null; previewVideoAutostart?: boolean; previewRotationDeg?: 0 | 90 | 180 | 270; settings?: Partial; media?: Partial; layout?: { x: number; y: number }; }, ) => { setState((s) => { const p = s.project; if (!p) return s; const scene = p.scenes[sceneId]; if (!scene) return s; const next: Scene = { ...scene, ...(patch.title !== undefined ? { title: patch.title } : null), ...(patch.description !== undefined ? { description: patch.description } : null), ...(patch.previewAssetId !== undefined ? { previewAssetId: patch.previewAssetId } : null), ...(patch.previewAssetType !== undefined ? { previewAssetType: patch.previewAssetType } : null), ...(patch.previewVideoAutostart !== undefined ? { previewVideoAutostart: patch.previewVideoAutostart } : null), ...(patch.previewRotationDeg !== undefined ? { previewRotationDeg: patch.previewRotationDeg } : null), ...(patch.settings ? { settings: { ...scene.settings, ...patch.settings } } : null), ...(patch.media ? { media: { ...scene.media, ...patch.media } } : null), layout: patch.layout ? { ...scene.layout, ...patch.layout } : scene.layout, }; const scenes = { ...p.scenes, [sceneId]: next }; const project: Project = { ...p, scenes }; return { ...s, project }; }); await api.invoke(ipcChannels.project.updateScene, { sceneId, patch }); }; const updateConnections = async (sceneId: SceneId, connections: SceneId[]) => { setState((s) => { const p = s.project; if (!p) return s; const scene = p.scenes[sceneId]; if (!scene) return s; const next: Scene = { ...scene, connections }; const scenes = { ...p.scenes, [sceneId]: next }; const project: Project = { ...p, scenes }; return { ...s, project }; }); await api.invoke(ipcChannels.project.updateConnections, { sceneId, connections }); }; const importMediaToScene = async (sceneId: SceneId) => { const res = await api.invoke(ipcChannels.project.importMedia, { sceneId }); setState((s) => ({ ...s, project: res.project })); await refreshProjects(); }; const importScenePreview = async (sceneId: SceneId) => { const res = await api.invoke(ipcChannels.project.importScenePreview, { sceneId }); setState((s) => ({ ...s, project: res.project })); await refreshProjects(); }; const clearScenePreview = async (sceneId: SceneId) => { const res = await api.invoke(ipcChannels.project.clearScenePreview, { sceneId }); setState((s) => ({ ...s, project: res.project })); await refreshProjects(); }; const updateSceneGraphNodePosition = async (nodeId: GraphNodeId, x: number, y: number) => { setState((s) => { const p = s.project; if (!p) return s; return { ...s, project: { ...p, sceneGraphNodes: p.sceneGraphNodes.map((n) => (n.id === nodeId ? { ...n, x, y } : n)), }, }; }); const res = await api.invoke(ipcChannels.project.updateSceneGraphNodePosition, { nodeId, x, y }); setState((s) => ({ ...s, project: res.project })); }; const addSceneGraphNode = async (sceneId: SceneId, x: number, y: number) => { const res = await api.invoke(ipcChannels.project.addSceneGraphNode, { sceneId, x, y }); setState((s) => ({ ...s, project: res.project })); }; const removeSceneGraphNode = async (nodeId: GraphNodeId) => { const res = await api.invoke(ipcChannels.project.removeSceneGraphNode, { nodeId }); setState((s) => ({ ...s, project: res.project })); }; const addSceneGraphEdge = async (sourceGraphNodeId: GraphNodeId, targetGraphNodeId: GraphNodeId) => { const res = await api.invoke(ipcChannels.project.addSceneGraphEdge, { sourceGraphNodeId, targetGraphNodeId, }); setState((s) => ({ ...s, project: res.project })); }; const removeSceneGraphEdge = async (edgeId: string) => { const res = await api.invoke(ipcChannels.project.removeSceneGraphEdge, { edgeId }); setState((s) => ({ ...s, project: res.project })); }; const setSceneGraphNodeStart = async (graphNodeId: GraphNodeId | null) => { const res = await api.invoke(ipcChannels.project.setSceneGraphNodeStart, { graphNodeId }); setState((s) => ({ ...s, project: res.project })); }; const deleteScene = async (sceneId: SceneId) => { const res = await api.invoke(ipcChannels.project.deleteScene, { sceneId }); setState((s) => ({ ...s, project: res.project, selectedSceneId: res.project.currentSceneId ?? null, })); await refreshProjects(); }; const renameProject = async (name: string, fileBaseName: string) => { const res = await api.invoke(ipcChannels.project.rename, { name, fileBaseName }); setState((s) => ({ ...s, project: res.project })); await refreshProjects(); }; const importProject = async () => { const res = await api.invoke(ipcChannels.project.importZip, {}); if (res.canceled) return; setState((s) => ({ ...s, project: res.project, selectedSceneId: res.project.currentSceneId, })); await refreshProjects(); }; const exportProject = async (projectId: ProjectId) => { const res = await api.invoke(ipcChannels.project.exportZip, { projectId }); if (res.canceled) return; }; const deleteProject = async (projectId: ProjectId) => { await api.invoke(ipcChannels.project.deleteProject, { projectId }); const listRes = await api.invoke(ipcChannels.project.list, {}); const res = await api.invoke(ipcChannels.project.get, {}); setState((s) => ({ ...s, projects: listRes.projects, project: res.project, selectedSceneId: res.project?.currentSceneId ?? null, })); }; return { refreshProjects, createProject, openProject, closeProject, createScene, selectScene, updateScene, updateConnections, importMediaToScene, importScenePreview, clearScenePreview, updateSceneGraphNodePosition, addSceneGraphNode, removeSceneGraphNode, addSceneGraphEdge, removeSceneGraphEdge, setSceneGraphNodeStart, deleteScene, renameProject, importProject, exportProject, deleteProject, }; }, [api, licenseActive]); useEffect(() => { if (!licenseActive) { queueMicrotask(() => { setState({ projects: [], project: null, selectedSceneId: null }); }); return; } void (async () => { const listRes = await api.invoke(ipcChannels.project.list, {}); setState((s) => ({ ...s, projects: listRes.projects })); const res = await api.invoke(ipcChannels.project.get, {}); setState((s) => ({ ...s, project: res.project, selectedSceneId: res.project?.currentSceneId ?? null })); })(); }, [licenseActive, api]); return [state, actions] as const; }