拆分图片画布编辑器外壳状态
新增编辑器外壳状态 hook 抽出项目重命名、背景设置、侧栏和工具状态 补充外壳状态单测并更新拆分记录
This commit is contained in:
@@ -131,3 +131,4 @@
|
|||||||
- 2026-06-17 前端拆分第十四阶段:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。
|
- 2026-06-17 前端拆分第十四阶段:新增 `useImageCanvasLayerCommands`,把画布剪贴板、右键目标解析、复制 / 剪切 / 粘贴、创建副本、层级移动、分组 / 解组、显隐、锁定、翻转、删除选中图层、按 id 删除和单图导出委托从主视图抽出;主视图保留菜单定位、画布事件、生成、上传、项目持久化和实际导出下载。验证命令:`npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭后 `画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后新标签素材、画布图层、返回项目入口、小地图和底部工具栏可见,控制台无前端 error。
|
||||||
- 2026-06-17 前端拆分第十五阶段:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。
|
- 2026-06-17 前端拆分第十五阶段:新增 `useImageCanvasGenerationWorkflow`,把生成入口、规范 / 角色 / 图标 / 修改 / 快速编辑 / 角色动画状态机、真实生成提交、结果落图、失败恢复和删除图层后的生成态清理从主视图抽出;主视图保留画布事件、浮层定位、上传、项目资源持久化和历史捕获。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空会话后未登录刷新弹出 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` 面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator` 占位框、`生成图片` 对话框和 `AI画布工具栏` 均可见;登录临时开发账号后上传素材成功,素材数增加,点击素材可加入画布,切换 `图层` 面板可看到对应图层,登录后控制台无前端 error。
|
||||||
- 2026-06-17 上传鉴权回归修正:普通素材上传入口在未登录时先打开 `账号入口`,不再先弹系统文件选择器;登录后用户再次点击上传即可打开文件选择器,避免浏览器拦截登录后异步触发的系统选择器。拖拽 / 已选中文件的续传逻辑仍保留,角色 / 图标生成参考图仍作为本地引用上传,不强制登录。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,`画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;未登录点击侧栏上传直接弹登录,不出现文件选择器;登录后再次点击上传可以打开文件选择器并上传成功,素材计数从 4 增至 5,`AI画布工具栏` 保持可见。
|
- 2026-06-17 上传鉴权回归修正:普通素材上传入口在未登录时先打开 `账号入口`,不再先弹系统文件选择器;登录后用户再次点击上传即可打开文件选择器,避免浏览器拦截登录后异步触发的系统选择器。拖拽 / 已选中文件的续传逻辑仍保留,角色 / 图标生成参考图仍作为本地引用上传,不强制登录。验证命令:`npm run test -- src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,`画布背景色` 打开完整 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;未登录点击侧栏上传直接弹登录,不出现文件选择器;登录后再次点击上传可以打开文件选择器并上传成功,素材计数从 4 增至 5,`AI画布工具栏` 保持可见。
|
||||||
|
- 2026-06-17 前端拆分第十六阶段:新增 `useImageCanvasEditorChrome`,把项目标题 / 重命名、侧栏开关、当前工具、缩放菜单、背景设置、小地图和背景 HEX 状态从主视图抽出;主视图继续保留上传 / 生成 / 键盘 Escape 的跨工作流编排。新增 hook 单测覆盖重命名、鉴权登录、背景色输入、面板开关和工具状态;主视图从 2039 行降至 1966 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`。
|
||||||
|
|||||||
@@ -139,14 +139,21 @@
|
|||||||
- 主视图继续负责画布事件、生成对话框定位样式、占位框拖拽、DOM 渲染、上传入口、工程资源持久化和历史捕获;生成 hook 只接收状态 setter 与落图 / 选中 / 适合视图回调。
|
- 主视图继续负责画布事件、生成对话框定位样式、占位框拖拽、DOM 渲染、上传入口、工程资源持久化和历史捕获;生成 hook 只接收状态 setter 与落图 / 选中 / 适合视图回调。
|
||||||
- 该 hook 用独立单测覆盖打开生成占位、普通生图落图、快速编辑落图并适配源图、删除源图时清理 quick edit / edit dialog、角色动画入口过滤和隐藏生成面板不删除占位框,避免后续拆分再次导致生成工具或底部工具栏状态回退。
|
- 该 hook 用独立单测覆盖打开生成占位、普通生图落图、快速编辑落图并适配源图、删除源图时清理 quick edit / edit dialog、角色动画入口过滤和隐藏生成面板不删除占位框,避免后续拆分再次导致生成工具或底部工具栏状态回退。
|
||||||
|
|
||||||
|
## 第十六阶段模块
|
||||||
|
|
||||||
|
- `useImageCanvasEditorChrome.ts`
|
||||||
|
- 承载编辑器 chrome 状态:项目标题 / 重命名、左侧栏开关、当前工具、缩放菜单、背景设置面板、小地图开关、画布背景色和 HEX 输入。
|
||||||
|
- 主视图继续负责真正跨工作流的动作编排,例如上传工具触发上传工作流、生成工具触发生成工作流、项目加载后注入标题、键盘 Escape 同时关闭生成 / 快速编辑 / 图片菜单等非 chrome 面板。
|
||||||
|
- 该 hook 用独立单测覆盖项目重命名、鉴权失败登录、背景色合法 / 非法 HEX、侧栏切换、缩放 / 背景面板关闭、小地图和工具状态,避免后续改顶部栏或左下 dock 时把这些状态重新散回主视图。
|
||||||
|
|
||||||
## 后续阶段
|
## 后续阶段
|
||||||
|
|
||||||
- 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。
|
- 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。
|
||||||
- 生成对象定位、画布 pointer 事件、工程资源持久化和历史捕获仍在主视图编排,拆分前需要先确认不会破坏多生成对象同时存在、完成时读取最新占位框和角色动画优先传 `objectKey` 的历史保护规则。
|
- 生成对象定位、画布 pointer 事件、素材入画布、工程资源持久化和历史捕获仍在主视图编排,拆分前需要先确认不会破坏多生成对象同时存在、完成时读取最新占位框、素材拖拽上传位置和角色动画优先传 `objectKey` 的历史保护规则。
|
||||||
|
|
||||||
## 验证计划
|
## 验证计划
|
||||||
|
|
||||||
- `npm run test -- src/components/image-editor/useImageCanvasLayerCommands.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`
|
- `npm run test -- src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`
|
||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run check:encoding`
|
- `npm run check:encoding`
|
||||||
- `git diff --check`
|
- `git diff --check`
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { ApiClientError } from '../../services/apiClient';
|
|
||||||
import { renameEditorProject } from '../../services/image-editor/editorProjectClient';
|
|
||||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||||
import { PlatformTextField } from '../common/PlatformTextField';
|
import { PlatformTextField } from '../common/PlatformTextField';
|
||||||
import { UnifiedModal } from '../common/UnifiedModal';
|
import { UnifiedModal } from '../common/UnifiedModal';
|
||||||
@@ -43,7 +41,6 @@ import { ImageCanvasSidebarView } from './ImageCanvasSidebarView';
|
|||||||
import { ImageCanvasStageView } from './ImageCanvasStageView';
|
import { ImageCanvasStageView } from './ImageCanvasStageView';
|
||||||
import {
|
import {
|
||||||
ASSET_DRAG_MIME_TYPE,
|
ASSET_DRAG_MIME_TYPE,
|
||||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
|
||||||
DEFAULT_CANVAS_SIZE,
|
DEFAULT_CANVAS_SIZE,
|
||||||
TOOLBAR_HALF_WIDTH,
|
TOOLBAR_HALF_WIDTH,
|
||||||
clamp,
|
clamp,
|
||||||
@@ -52,7 +49,6 @@ import {
|
|||||||
hasDataTransferType,
|
hasDataTransferType,
|
||||||
isGeneratedLayer,
|
isGeneratedLayer,
|
||||||
isLayerLinkedToAsset,
|
isLayerLinkedToAsset,
|
||||||
normalizeCanvasBackgroundHex,
|
|
||||||
resolveContextMenuPosition,
|
resolveContextMenuPosition,
|
||||||
serializeLayer,
|
serializeLayer,
|
||||||
} from './ImageCanvasEditorModel';
|
} from './ImageCanvasEditorModel';
|
||||||
@@ -76,13 +72,13 @@ import type {
|
|||||||
DragState,
|
DragState,
|
||||||
EditorAsset,
|
EditorAsset,
|
||||||
ImageContextMenuState,
|
ImageContextMenuState,
|
||||||
SidebarPanel,
|
|
||||||
SnapGuide,
|
SnapGuide,
|
||||||
} from './ImageCanvasEditorTypes';
|
} from './ImageCanvasEditorTypes';
|
||||||
import { useCanvasHistory } from './useCanvasHistory';
|
import { useCanvasHistory } from './useCanvasHistory';
|
||||||
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
||||||
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
|
import { useImageCanvasAssetLibrary } from './useImageCanvasAssetLibrary';
|
||||||
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
|
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
|
||||||
|
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
||||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||||
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
|
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
|
||||||
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
|
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
|
||||||
@@ -145,13 +141,6 @@ function getPointerId(event: ReactPointerEvent<HTMLElement>) {
|
|||||||
return Number.isFinite(nativeId) ? nativeId : -1;
|
return Number.isFinite(nativeId) ? nativeId : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEditorAuthError(error: unknown) {
|
|
||||||
return (
|
|
||||||
error instanceof ApiClientError &&
|
|
||||||
(error.status === 401 || error.status === 403)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImageCanvasEditorView() {
|
export function ImageCanvasEditorView() {
|
||||||
const authUi = useAuthUi();
|
const authUi = useAuthUi();
|
||||||
const editorRootRef = useRef<HTMLElement | null>(null);
|
const editorRootRef = useRef<HTMLElement | null>(null);
|
||||||
@@ -177,15 +166,6 @@ export function ImageCanvasEditorView() {
|
|||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
const suppressAssetClickRef = useRef(false);
|
const suppressAssetClickRef = useRef(false);
|
||||||
const [projectTitle, setProjectTitle] = useState('未命名画布');
|
|
||||||
const [projectRenameValue, setProjectRenameValue] = useState('未命名画布');
|
|
||||||
const [isRenamingProject, setIsRenamingProject] = useState(false);
|
|
||||||
const [isProjectRenameSaving, setIsProjectRenameSaving] = useState(false);
|
|
||||||
const [projectRenameError, setProjectRenameError] = useState<string | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const [activeSidebarPanel, setActiveSidebarPanel] =
|
|
||||||
useState<SidebarPanel | null>('assets');
|
|
||||||
const [viewport, setViewport] = useState<CanvasViewport>({
|
const [viewport, setViewport] = useState<CanvasViewport>({
|
||||||
x: -260,
|
x: -260,
|
||||||
y: 70,
|
y: 70,
|
||||||
@@ -199,20 +179,9 @@ export function ImageCanvasEditorView() {
|
|||||||
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
const [selectedLayerId, setSelectedLayerId] = useState<string | null>(null);
|
||||||
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
const [selectedLayerIds, setSelectedLayerIds] = useState<string[]>([]);
|
||||||
const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
|
const [hoveredLayerId, setHoveredLayerId] = useState<string | null>(null);
|
||||||
const [activeTool, setActiveTool] = useState<CanvasTool>('select');
|
|
||||||
const [isSpacePanning, setIsSpacePanning] = useState(false);
|
const [isSpacePanning, setIsSpacePanning] = useState(false);
|
||||||
const [isPanning, setIsPanning] = useState(false);
|
const [isPanning, setIsPanning] = useState(false);
|
||||||
const [snapGuide, setSnapGuide] = useState<SnapGuide | null>(null);
|
const [snapGuide, setSnapGuide] = useState<SnapGuide | null>(null);
|
||||||
const [isZoomMenuOpen, setIsZoomMenuOpen] = useState(false);
|
|
||||||
const [isBackgroundSettingsOpen, setIsBackgroundSettingsOpen] =
|
|
||||||
useState(false);
|
|
||||||
const [isMinimapOpen, setIsMinimapOpen] = useState(true);
|
|
||||||
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState(
|
|
||||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
|
||||||
);
|
|
||||||
const [canvasBackgroundHexValue, setCanvasBackgroundHexValue] = useState(
|
|
||||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
|
||||||
);
|
|
||||||
const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
const [metadataLayer, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
||||||
const [imageContextMenu, setImageContextMenu] =
|
const [imageContextMenu, setImageContextMenu] =
|
||||||
useState<ImageContextMenuState | null>(null);
|
useState<ImageContextMenuState | null>(null);
|
||||||
@@ -241,15 +210,36 @@ export function ImageCanvasEditorView() {
|
|||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
const applyCanvasBackgroundColor = useCallback((color: string) => {
|
const {
|
||||||
const normalizedColor = normalizeCanvasBackgroundHex(color);
|
projectTitle,
|
||||||
if (!normalizedColor) {
|
setProjectTitle,
|
||||||
return false;
|
projectRenameValue,
|
||||||
}
|
setProjectRenameValue,
|
||||||
setCanvasBackgroundColor(normalizedColor);
|
isRenamingProject,
|
||||||
setCanvasBackgroundHexValue(normalizedColor);
|
isProjectRenameSaving,
|
||||||
return true;
|
projectRenameError,
|
||||||
}, []);
|
activeSidebarPanel,
|
||||||
|
setActiveSidebarPanel,
|
||||||
|
activeTool,
|
||||||
|
setActiveTool,
|
||||||
|
isZoomMenuOpen,
|
||||||
|
isBackgroundSettingsOpen,
|
||||||
|
isMinimapOpen,
|
||||||
|
canvasBackgroundColor,
|
||||||
|
canvasBackgroundHexValue,
|
||||||
|
startProjectRename,
|
||||||
|
cancelProjectRename,
|
||||||
|
submitProjectRename,
|
||||||
|
resetProjectRenameError,
|
||||||
|
applyCanvasBackgroundColor,
|
||||||
|
handleCanvasBackgroundHexChange,
|
||||||
|
closeEditorChromePanels,
|
||||||
|
toggleSidebarPanel,
|
||||||
|
toggleZoomMenu,
|
||||||
|
closeZoomMenu,
|
||||||
|
toggleBackgroundSettings,
|
||||||
|
toggleMinimap,
|
||||||
|
} = useImageCanvasEditorChrome({ openEditorLoginModal });
|
||||||
const removeCanvasLayersLinkedToAssets = useCallback(
|
const removeCanvasLayersLinkedToAssets = useCallback(
|
||||||
(deletedAssets: EditorAsset[]) => {
|
(deletedAssets: EditorAsset[]) => {
|
||||||
if (!deletedAssets.length) {
|
if (!deletedAssets.length) {
|
||||||
@@ -804,9 +794,7 @@ export function ImageCanvasEditorView() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
setActiveSidebarPanel(null);
|
closeEditorChromePanels();
|
||||||
setIsZoomMenuOpen(false);
|
|
||||||
setIsBackgroundSettingsOpen(false);
|
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
setImageContextMenu(null);
|
setImageContextMenu(null);
|
||||||
setContextMenu(null);
|
setContextMenu(null);
|
||||||
@@ -863,7 +851,7 @@ export function ImageCanvasEditorView() {
|
|||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
window.removeEventListener('keyup', handleKeyUp);
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
};
|
};
|
||||||
}, [redoCanvasChange, undoCanvasChange]);
|
}, [closeEditorChromePanels, redoCanvasChange, undoCanvasChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const blockBrowserZoom = (event: WheelEvent) => {
|
const blockBrowserZoom = (event: WheelEvent) => {
|
||||||
@@ -1025,50 +1013,6 @@ export function ImageCanvasEditorView() {
|
|||||||
};
|
};
|
||||||
addAssetLayerRef.current = addAssetLayer;
|
addAssetLayerRef.current = addAssetLayer;
|
||||||
|
|
||||||
const startProjectRename = () => {
|
|
||||||
setProjectRenameValue(projectTitle);
|
|
||||||
setProjectRenameError(null);
|
|
||||||
setIsRenamingProject(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelProjectRename = () => {
|
|
||||||
setProjectRenameValue(projectTitle);
|
|
||||||
setProjectRenameError(null);
|
|
||||||
setIsRenamingProject(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitProjectRename = () => {
|
|
||||||
const nextTitle = projectRenameValue.trim();
|
|
||||||
if (!nextTitle) {
|
|
||||||
setProjectRenameError('项目名称不能为空');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!projectId || nextTitle === projectTitle) {
|
|
||||||
setProjectRenameValue(projectTitle);
|
|
||||||
setProjectRenameError(null);
|
|
||||||
setIsRenamingProject(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsProjectRenameSaving(true);
|
|
||||||
setProjectRenameError(null);
|
|
||||||
renameEditorProject(projectId, nextTitle)
|
|
||||||
.then((project) => {
|
|
||||||
const savedTitle = project.title?.trim() || nextTitle;
|
|
||||||
setProjectTitle(savedTitle);
|
|
||||||
setProjectRenameValue(savedTitle);
|
|
||||||
setIsRenamingProject(false);
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
if (isEditorAuthError(error)) {
|
|
||||||
openEditorLoginModal();
|
|
||||||
}
|
|
||||||
setProjectRenameError(
|
|
||||||
error instanceof Error ? error.message : '重命名项目失败',
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => setIsProjectRenameSaving(false));
|
|
||||||
};
|
|
||||||
|
|
||||||
moveAssetToFolderRef.current = moveAssetToFolder;
|
moveAssetToFolderRef.current = moveAssetToFolder;
|
||||||
|
|
||||||
deleteLayerByIdRef.current = deleteLayerById;
|
deleteLayerByIdRef.current = deleteLayerById;
|
||||||
@@ -1366,15 +1310,6 @@ export function ImageCanvasEditorView() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasBackgroundHexChange = (nextValue: string) => {
|
|
||||||
setCanvasBackgroundHexValue(nextValue);
|
|
||||||
const normalizedColor = normalizeCanvasBackgroundHex(nextValue);
|
|
||||||
if (normalizedColor) {
|
|
||||||
setCanvasBackgroundColor(normalizedColor);
|
|
||||||
setCanvasBackgroundHexValue(normalizedColor);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerationFramePointerDown = (
|
const handleGenerationFramePointerDown = (
|
||||||
event: ReactPointerEvent<HTMLDivElement>,
|
event: ReactPointerEvent<HTMLDivElement>,
|
||||||
dialog: CanvasGenerationDialogState,
|
dialog: CanvasGenerationDialogState,
|
||||||
@@ -1606,12 +1541,6 @@ export function ImageCanvasEditorView() {
|
|||||||
setActiveTool(tool);
|
setActiveTool(tool);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSidebarPanel = (panel: SidebarPanel) => {
|
|
||||||
setActiveSidebarPanel((currentPanel) =>
|
|
||||||
currentPanel === panel ? null : panel,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
ref={editorRootRef}
|
ref={editorRootRef}
|
||||||
@@ -1719,7 +1648,7 @@ export function ImageCanvasEditorView() {
|
|||||||
className="image-canvas-editor__project-title-form"
|
className="image-canvas-editor__project-title-form"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submitProjectRename();
|
submitProjectRename(projectId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlatformTextField
|
<PlatformTextField
|
||||||
@@ -1730,7 +1659,7 @@ export function ImageCanvasEditorView() {
|
|||||||
className="image-canvas-editor__project-title-input"
|
className="image-canvas-editor__project-title-input"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setProjectRenameValue(event.target.value);
|
setProjectRenameValue(event.target.value);
|
||||||
setProjectRenameError(null);
|
resetProjectRenameError();
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
@@ -1884,15 +1813,13 @@ export function ImageCanvasEditorView() {
|
|||||||
onFitLayers={fitLayers}
|
onFitLayers={fitLayers}
|
||||||
onUndoCanvasChange={undoCanvasChange}
|
onUndoCanvasChange={undoCanvasChange}
|
||||||
onRedoCanvasChange={redoCanvasChange}
|
onRedoCanvasChange={redoCanvasChange}
|
||||||
onToggleZoomMenu={() => setIsZoomMenuOpen((open) => !open)}
|
onToggleZoomMenu={toggleZoomMenu}
|
||||||
onCloseZoomMenu={() => setIsZoomMenuOpen(false)}
|
onCloseZoomMenu={closeZoomMenu}
|
||||||
onToggleBackgroundSettings={() =>
|
onToggleBackgroundSettings={toggleBackgroundSettings}
|
||||||
setIsBackgroundSettingsOpen((isOpen) => !isOpen)
|
|
||||||
}
|
|
||||||
onApplyCanvasBackgroundColor={applyCanvasBackgroundColor}
|
onApplyCanvasBackgroundColor={applyCanvasBackgroundColor}
|
||||||
onCanvasBackgroundHexChange={handleCanvasBackgroundHexChange}
|
onCanvasBackgroundHexChange={handleCanvasBackgroundHexChange}
|
||||||
onToggleSidebarPanel={toggleSidebarPanel}
|
onToggleSidebarPanel={toggleSidebarPanel}
|
||||||
onToggleMinimap={() => setIsMinimapOpen((open) => !open)}
|
onToggleMinimap={toggleMinimap}
|
||||||
onMinimapPointerDown={handleMinimapPointerDown}
|
onMinimapPointerDown={handleMinimapPointerDown}
|
||||||
onSwitchTool={switchTool}
|
onSwitchTool={switchTool}
|
||||||
>
|
>
|
||||||
|
|||||||
224
src/components/image-editor/useImageCanvasEditorChrome.test.tsx
Normal file
224
src/components/image-editor/useImageCanvasEditorChrome.test.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { ApiClientError } from '../../services/apiClient';
|
||||||
|
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
||||||
|
|
||||||
|
const renameEditorProjectMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
|
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||||
|
const actual = await vi.importActual<
|
||||||
|
typeof import('../../services/image-editor/editorProjectClient')
|
||||||
|
>('../../services/image-editor/editorProjectClient');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
renameEditorProject: renameEditorProjectMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function ChromeHarness({
|
||||||
|
openEditorLoginModal = vi.fn(),
|
||||||
|
}: {
|
||||||
|
openEditorLoginModal?: (postLoginAction?: (() => void) | null) => void;
|
||||||
|
}) {
|
||||||
|
const chrome = useImageCanvasEditorChrome({ openEditorLoginModal });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span data-testid="title">{chrome.projectTitle}</span>
|
||||||
|
<span data-testid="rename-value">{chrome.projectRenameValue}</span>
|
||||||
|
<span data-testid="renaming">{String(chrome.isRenamingProject)}</span>
|
||||||
|
<span data-testid="saving">{String(chrome.isProjectRenameSaving)}</span>
|
||||||
|
<span data-testid="rename-error">{chrome.projectRenameError ?? '-'}</span>
|
||||||
|
<span data-testid="sidebar">{chrome.activeSidebarPanel ?? '-'}</span>
|
||||||
|
<span data-testid="tool">{chrome.activeTool}</span>
|
||||||
|
<span data-testid="zoom">{String(chrome.isZoomMenuOpen)}</span>
|
||||||
|
<span data-testid="background-open">
|
||||||
|
{String(chrome.isBackgroundSettingsOpen)}
|
||||||
|
</span>
|
||||||
|
<span data-testid="minimap">{String(chrome.isMinimapOpen)}</span>
|
||||||
|
<span data-testid="background-color">
|
||||||
|
{chrome.canvasBackgroundColor}
|
||||||
|
</span>
|
||||||
|
<span data-testid="background-hex">
|
||||||
|
{chrome.canvasBackgroundHexValue}
|
||||||
|
</span>
|
||||||
|
<button type="button" onClick={chrome.startProjectRename}>
|
||||||
|
start rename
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={chrome.cancelProjectRename}>
|
||||||
|
cancel rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.submitProjectRename('project-1')}
|
||||||
|
>
|
||||||
|
submit rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.submitProjectRename(null)}
|
||||||
|
>
|
||||||
|
submit without project
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.setProjectRenameValue(' 新项目 ')}
|
||||||
|
>
|
||||||
|
set rename
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.setProjectRenameValue(' ')}
|
||||||
|
>
|
||||||
|
blank rename
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => chrome.setProjectTitle('已有项目')}>
|
||||||
|
set title
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => chrome.setActiveTool('hand')}>
|
||||||
|
set hand
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => chrome.toggleSidebarPanel('assets')}>
|
||||||
|
toggle assets
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => chrome.toggleSidebarPanel('layers')}>
|
||||||
|
toggle layers
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={chrome.toggleZoomMenu}>
|
||||||
|
toggle zoom
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={chrome.toggleBackgroundSettings}>
|
||||||
|
toggle background
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={chrome.toggleMinimap}>
|
||||||
|
toggle minimap
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={chrome.closeEditorChromePanels}>
|
||||||
|
close panels
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.applyCanvasBackgroundColor('#abc')}
|
||||||
|
>
|
||||||
|
apply short hex
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.handleCanvasBackgroundHexChange('#not-a-color')}
|
||||||
|
>
|
||||||
|
invalid hex
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => chrome.handleCanvasBackgroundHexChange('#ffffff')}
|
||||||
|
>
|
||||||
|
valid hex
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useImageCanvasEditorChrome', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renames projects and resets rename state from the saved title', async () => {
|
||||||
|
renameEditorProjectMock.mockResolvedValueOnce({
|
||||||
|
projectId: 'project-1',
|
||||||
|
title: '后端项目名',
|
||||||
|
viewport: { x: 0, y: 0, scale: 1 },
|
||||||
|
layers: [],
|
||||||
|
resources: [],
|
||||||
|
updatedAt: '2026-06-17T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
render(<ChromeHarness />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'set title' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'start rename' }));
|
||||||
|
expect(screen.getByTestId('rename-value').textContent).toBe('已有项目');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'set rename' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
|
||||||
|
expect(screen.getByTestId('saving').textContent).toBe('true');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(renameEditorProjectMock).toHaveBeenCalledWith(
|
||||||
|
'project-1',
|
||||||
|
'新项目',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('title').textContent).toBe('后端项目名');
|
||||||
|
expect(screen.getByTestId('rename-value').textContent).toBe('后端项目名');
|
||||||
|
expect(screen.getByTestId('renaming').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('saving').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates rename input and opens login on rename auth errors', async () => {
|
||||||
|
const openEditorLoginModal = vi.fn();
|
||||||
|
renameEditorProjectMock.mockRejectedValueOnce(
|
||||||
|
new ApiClientError({
|
||||||
|
message: '未授权访问',
|
||||||
|
status: 401,
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
render(<ChromeHarness openEditorLoginModal={openEditorLoginModal} />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'blank rename' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
|
||||||
|
expect(screen.getByTestId('rename-error').textContent).toBe(
|
||||||
|
'项目名称不能为空',
|
||||||
|
);
|
||||||
|
expect(renameEditorProjectMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'set rename' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'submit rename' }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(openEditorLoginModal).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('rename-error').textContent).toBe('未授权访问');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manages background colors and chrome panel toggles', () => {
|
||||||
|
render(<ChromeHarness />);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'apply short hex' }));
|
||||||
|
expect(screen.getByTestId('background-color').textContent).toBe('#aabbcc');
|
||||||
|
expect(screen.getByTestId('background-hex').textContent).toBe('#aabbcc');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'invalid hex' }));
|
||||||
|
expect(screen.getByTestId('background-color').textContent).toBe('#aabbcc');
|
||||||
|
expect(screen.getByTestId('background-hex').textContent).toBe(
|
||||||
|
'#not-a-color',
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'valid hex' }));
|
||||||
|
expect(screen.getByTestId('background-color').textContent).toBe('#ffffff');
|
||||||
|
expect(screen.getByTestId('background-hex').textContent).toBe('#ffffff');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'toggle assets' }));
|
||||||
|
expect(screen.getByTestId('sidebar').textContent).toBe('-');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'toggle layers' }));
|
||||||
|
expect(screen.getByTestId('sidebar').textContent).toBe('layers');
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'toggle zoom' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'toggle background' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'toggle minimap' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'set hand' }));
|
||||||
|
expect(screen.getByTestId('zoom').textContent).toBe('true');
|
||||||
|
expect(screen.getByTestId('background-open').textContent).toBe('true');
|
||||||
|
expect(screen.getByTestId('minimap').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('tool').textContent).toBe('hand');
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
screen.getByRole('button', { name: 'close panels' }).click();
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId('sidebar').textContent).toBe('-');
|
||||||
|
expect(screen.getByTestId('zoom').textContent).toBe('false');
|
||||||
|
expect(screen.getByTestId('background-open').textContent).toBe('false');
|
||||||
|
});
|
||||||
|
});
|
||||||
177
src/components/image-editor/useImageCanvasEditorChrome.ts
Normal file
177
src/components/image-editor/useImageCanvasEditorChrome.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { ApiClientError } from '../../services/apiClient';
|
||||||
|
import { renameEditorProject } from '../../services/image-editor/editorProjectClient';
|
||||||
|
import {
|
||||||
|
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||||
|
normalizeCanvasBackgroundHex,
|
||||||
|
} from './ImageCanvasEditorModel';
|
||||||
|
import type {
|
||||||
|
CanvasTool,
|
||||||
|
SidebarPanel,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
|
||||||
|
type UseImageCanvasEditorChromeOptions = {
|
||||||
|
openEditorLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function isEditorAuthError(error: unknown) {
|
||||||
|
return (
|
||||||
|
error instanceof ApiClientError &&
|
||||||
|
(error.status === 401 || error.status === 403)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useImageCanvasEditorChrome({
|
||||||
|
openEditorLoginModal,
|
||||||
|
}: UseImageCanvasEditorChromeOptions) {
|
||||||
|
const [projectTitle, setProjectTitle] = useState('未命名画布');
|
||||||
|
const [projectRenameValue, setProjectRenameValue] = useState('未命名画布');
|
||||||
|
const [isRenamingProject, setIsRenamingProject] = useState(false);
|
||||||
|
const [isProjectRenameSaving, setIsProjectRenameSaving] = useState(false);
|
||||||
|
const [projectRenameError, setProjectRenameError] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [activeSidebarPanel, setActiveSidebarPanel] =
|
||||||
|
useState<SidebarPanel | null>('assets');
|
||||||
|
const [activeTool, setActiveTool] = useState<CanvasTool>('select');
|
||||||
|
const [isZoomMenuOpen, setIsZoomMenuOpen] = useState(false);
|
||||||
|
const [isBackgroundSettingsOpen, setIsBackgroundSettingsOpen] =
|
||||||
|
useState(false);
|
||||||
|
const [isMinimapOpen, setIsMinimapOpen] = useState(true);
|
||||||
|
const [canvasBackgroundColor, setCanvasBackgroundColor] = useState(
|
||||||
|
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||||
|
);
|
||||||
|
const [canvasBackgroundHexValue, setCanvasBackgroundHexValue] = useState(
|
||||||
|
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyCanvasBackgroundColor = useCallback((color: string) => {
|
||||||
|
const normalizedColor = normalizeCanvasBackgroundHex(color);
|
||||||
|
if (!normalizedColor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setCanvasBackgroundColor(normalizedColor);
|
||||||
|
setCanvasBackgroundHexValue(normalizedColor);
|
||||||
|
return true;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCanvasBackgroundHexChange = useCallback((nextValue: string) => {
|
||||||
|
setCanvasBackgroundHexValue(nextValue);
|
||||||
|
const normalizedColor = normalizeCanvasBackgroundHex(nextValue);
|
||||||
|
if (normalizedColor) {
|
||||||
|
setCanvasBackgroundColor(normalizedColor);
|
||||||
|
setCanvasBackgroundHexValue(normalizedColor);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startProjectRename = useCallback(() => {
|
||||||
|
setProjectRenameValue(projectTitle);
|
||||||
|
setProjectRenameError(null);
|
||||||
|
setIsRenamingProject(true);
|
||||||
|
}, [projectTitle]);
|
||||||
|
|
||||||
|
const cancelProjectRename = useCallback(() => {
|
||||||
|
setProjectRenameValue(projectTitle);
|
||||||
|
setProjectRenameError(null);
|
||||||
|
setIsRenamingProject(false);
|
||||||
|
}, [projectTitle]);
|
||||||
|
|
||||||
|
const submitProjectRename = useCallback(
|
||||||
|
(projectId: string | null) => {
|
||||||
|
const nextTitle = projectRenameValue.trim();
|
||||||
|
if (!nextTitle) {
|
||||||
|
setProjectRenameError('项目名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!projectId || nextTitle === projectTitle) {
|
||||||
|
setProjectRenameValue(projectTitle);
|
||||||
|
setProjectRenameError(null);
|
||||||
|
setIsRenamingProject(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsProjectRenameSaving(true);
|
||||||
|
setProjectRenameError(null);
|
||||||
|
renameEditorProject(projectId, nextTitle)
|
||||||
|
.then((project: Awaited<ReturnType<typeof renameEditorProject>>) => {
|
||||||
|
const savedTitle = project.title?.trim() || nextTitle;
|
||||||
|
setProjectTitle(savedTitle);
|
||||||
|
setProjectRenameValue(savedTitle);
|
||||||
|
setIsRenamingProject(false);
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (isEditorAuthError(error)) {
|
||||||
|
openEditorLoginModal();
|
||||||
|
}
|
||||||
|
setProjectRenameError(
|
||||||
|
error instanceof Error ? error.message : '重命名项目失败',
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => setIsProjectRenameSaving(false));
|
||||||
|
},
|
||||||
|
[openEditorLoginModal, projectRenameValue, projectTitle],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetProjectRenameError = useCallback(() => {
|
||||||
|
setProjectRenameError(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeEditorChromePanels = useCallback(() => {
|
||||||
|
setActiveSidebarPanel(null);
|
||||||
|
setIsZoomMenuOpen(false);
|
||||||
|
setIsBackgroundSettingsOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleSidebarPanel = useCallback((panel: SidebarPanel) => {
|
||||||
|
setActiveSidebarPanel((currentPanel) =>
|
||||||
|
currentPanel === panel ? null : panel,
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleZoomMenu = useCallback(() => {
|
||||||
|
setIsZoomMenuOpen((open) => !open);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeZoomMenu = useCallback(() => {
|
||||||
|
setIsZoomMenuOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleBackgroundSettings = useCallback(() => {
|
||||||
|
setIsBackgroundSettingsOpen((isOpen) => !isOpen);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleMinimap = useCallback(() => {
|
||||||
|
setIsMinimapOpen((open) => !open);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectTitle,
|
||||||
|
setProjectTitle,
|
||||||
|
projectRenameValue,
|
||||||
|
setProjectRenameValue,
|
||||||
|
isRenamingProject,
|
||||||
|
isProjectRenameSaving,
|
||||||
|
projectRenameError,
|
||||||
|
activeSidebarPanel,
|
||||||
|
setActiveSidebarPanel,
|
||||||
|
activeTool,
|
||||||
|
setActiveTool,
|
||||||
|
isZoomMenuOpen,
|
||||||
|
isBackgroundSettingsOpen,
|
||||||
|
isMinimapOpen,
|
||||||
|
canvasBackgroundColor,
|
||||||
|
canvasBackgroundHexValue,
|
||||||
|
startProjectRename,
|
||||||
|
cancelProjectRename,
|
||||||
|
submitProjectRename,
|
||||||
|
resetProjectRenameError,
|
||||||
|
applyCanvasBackgroundColor,
|
||||||
|
handleCanvasBackgroundHexChange,
|
||||||
|
closeEditorChromePanels,
|
||||||
|
toggleSidebarPanel,
|
||||||
|
toggleZoomMenu,
|
||||||
|
closeZoomMenu,
|
||||||
|
toggleBackgroundSettings,
|
||||||
|
toggleMinimap,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user