From 922029a35f44197e33d219df674aa6917c368f0c Mon Sep 17 00:00:00 2001 From: xigua Date: Wed, 10 Jan 2024 00:42:31 +0800 Subject: [PATCH] feat: import and export suika format file --- .../dropdown/dropdown-item/dropdown-item.scss | 3 +- .../src/components/dropdown/dropdown.tsx | 64 ++++---- .../src/components/dropdown/index.ts | 1 + .../src/components/popover/popover.tsx | 8 +- packages/components/src/index.ts | 1 + packages/icons/src/icons/tool/index.ts | 1 + .../icons/src/icons/tool/menu-outlined.tsx | 17 ++ .../components/Cards/AlignCard/AlignCard.tsx | 2 +- .../Cards/TextureCard/TextureCard.tsx | 154 +++++++++--------- .../Header/components/Toolbar/Toolbar.scss | 4 +- .../Header/components/Toolbar/Toolbar.tsx | 2 + .../Header/components/Toolbar/menu/Menu.tsx | 40 +++++ .../Header/components/Toolbar/menu/index.ts | 1 + .../Header/components/Toolbar/menu/menu.scss | 13 ++ .../components/ZoomActions/ZoomActions.scss | 1 - .../components/ActionItem/ActionItem.scss | 3 +- .../src/editor/commands/command_manager.ts | 5 + packages/suika/src/editor/editor.ts | 16 +- packages/suika/src/editor/export_manager.ts | 19 +++ packages/suika/src/editor/index.ts | 2 + packages/suika/src/editor/scene/graph.ts | 2 +- .../suika/src/editor/scene/scene_graph.ts | 12 +- .../src/editor/service/export_service.ts | 20 +++ .../src/editor/service/import_service.ts | 37 +++++ packages/suika/src/editor/service/index.ts | 2 + packages/suika/src/locale/en.json | 6 +- packages/suika/src/locale/zh.json | 5 +- packages/suika/src/type/index.ts | 3 +- 28 files changed, 311 insertions(+), 133 deletions(-) create mode 100644 packages/components/src/components/dropdown/index.ts create mode 100644 packages/icons/src/icons/tool/menu-outlined.tsx create mode 100644 packages/suika/src/components/Header/components/Toolbar/menu/Menu.tsx create mode 100644 packages/suika/src/components/Header/components/Toolbar/menu/index.ts create mode 100644 packages/suika/src/components/Header/components/Toolbar/menu/menu.scss create mode 100644 packages/suika/src/editor/export_manager.ts create mode 100644 packages/suika/src/editor/index.ts create mode 100644 packages/suika/src/editor/service/export_service.ts create mode 100644 packages/suika/src/editor/service/import_service.ts diff --git a/packages/components/src/components/dropdown/dropdown-item/dropdown-item.scss b/packages/components/src/components/dropdown/dropdown-item/dropdown-item.scss index 67cb0f6f..0c8ed5c8 100644 --- a/packages/components/src/components/dropdown/dropdown-item/dropdown-item.scss +++ b/packages/components/src/components/dropdown/dropdown-item/dropdown-item.scss @@ -3,7 +3,8 @@ justify-content: space-between; align-items: center; - padding: 0 16px 0 8px; + padding-left: 4px; + padding-right: 16px; height: 24px; line-height: 24px; font-size: 12px; diff --git a/packages/components/src/components/dropdown/dropdown.tsx b/packages/components/src/components/dropdown/dropdown.tsx index 9d9b1741..9ed29e64 100644 --- a/packages/components/src/components/dropdown/dropdown.tsx +++ b/packages/components/src/components/dropdown/dropdown.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import React, { FC, useState } from 'react'; import { Popover } from '../popover'; import { DropdownItem } from './dropdown-item'; import './dropdown.scss'; @@ -32,37 +32,35 @@ export const Dropdown: FC = (props) => { const [open, setOpen] = useState(false); return ( -
- { - setOpen(val); - }} - content={ -
- {items.map((item, index) => { - return isDivider(item) ? ( -
- ) : ( - { - setOpen(false); - props.onClick?.({ key: item.key }); - }} - > - {item.label} - - ); - })} -
- } - placement="bottom-start" - > - {children} - -
+ { + setOpen(val); + }} + content={ +
+ {items.map((item, index) => { + return isDivider(item) ? ( +
+ ) : ( + { + setOpen(false); + props.onClick?.({ key: item.key }); + }} + > + {item.label} + + ); + })} +
+ } + placement="bottom-start" + > + {React.cloneElement(children as React.ReactElement)} + ); }; diff --git a/packages/components/src/components/dropdown/index.ts b/packages/components/src/components/dropdown/index.ts new file mode 100644 index 00000000..b96a997b --- /dev/null +++ b/packages/components/src/components/dropdown/index.ts @@ -0,0 +1 @@ +export * from './dropdown'; diff --git a/packages/components/src/components/popover/popover.tsx b/packages/components/src/components/popover/popover.tsx index c7a35e34..f0a873b3 100644 --- a/packages/components/src/components/popover/popover.tsx +++ b/packages/components/src/components/popover/popover.tsx @@ -58,10 +58,10 @@ export const Popover: FC = (props) => { return ( <> - {/* TODO: remove span container el */} - - {children} - + {React.cloneElement(children as React.ReactElement, { + ...getReferenceProps(), + ref: refs.setReference, + })} {mixedOpen && (
{ + return ( + + + + + + ); +}); diff --git a/packages/suika/src/components/Cards/AlignCard/AlignCard.tsx b/packages/suika/src/components/Cards/AlignCard/AlignCard.tsx index 37cf3013..160fa4e9 100644 --- a/packages/suika/src/components/Cards/AlignCard/AlignCard.tsx +++ b/packages/suika/src/components/Cards/AlignCard/AlignCard.tsx @@ -13,7 +13,7 @@ import { AlignVCenter, IconAlignBottom, } from '@suika/icons'; -import { alignAndRecord } from '../../../editor/service'; +import { alignAndRecord } from '../../../editor'; export const AlignCard: FC = () => { const editor = useContext(EditorContext); diff --git a/packages/suika/src/components/Cards/TextureCard/TextureCard.tsx b/packages/suika/src/components/Cards/TextureCard/TextureCard.tsx index 284f4a48..20712a3d 100644 --- a/packages/suika/src/components/Cards/TextureCard/TextureCard.tsx +++ b/packages/suika/src/components/Cards/TextureCard/TextureCard.tsx @@ -98,87 +98,89 @@ export const TextureCard: FC = ({ placement="left-start" offset={2} > - - - - } - > - {arrMapRevert(textures, (texture, index) => { - /** SOLID **/ - if (texture.type === TextureType.Solid) { - return ( -
- { - setActiveIndex(index); - }} - /> - } - value={parseRGBToHex(texture.attrs)} - onBlur={(newHex) => { - const rgb = parseHexToRGB(newHex); - - if (rgb) { - const newSolidTexture: TextureSolid = { - type: TextureType.Solid, - attrs: { - ...rgb, - a: texture.attrs.a, - }, - }; - onChangeComplete(newSolidTexture, index); - } - }} - /> - onDelete(index)}> - - -
- ); +
+ + + } + > + {arrMapRevert(textures, (texture, index) => { + /** SOLID **/ + if (texture.type === TextureType.Solid) { + return ( +
+ { + setActiveIndex(index); + }} + /> + } + value={parseRGBToHex(texture.attrs)} + onBlur={(newHex) => { + const rgb = parseHexToRGB(newHex); - /** IMAGE */ - if (texture.type === TextureType.Image) { - return ( -
-
{ - setActiveIndex(index); - }} - > - img + onDelete(index)}> + +
- onDelete(index)}> - - -
- ); - } - })} - {appendedContent} - + ); + } + + /** IMAGE */ + if (texture.type === TextureType.Image) { + return ( +
+
{ + setActiveIndex(index); + }} + > + img +
+ onDelete(index)}> + + +
+ ); + } + })} + {appendedContent} + +
); }; diff --git a/packages/suika/src/components/Header/components/Toolbar/Toolbar.scss b/packages/suika/src/components/Header/components/Toolbar/Toolbar.scss index f050b4ef..e24fa266 100644 --- a/packages/suika/src/components/Header/components/Toolbar/Toolbar.scss +++ b/packages/suika/src/components/Header/components/Toolbar/Toolbar.scss @@ -3,7 +3,7 @@ display: flex; align-items: center; - margin-left: 24px; + margin-left: 8px; height: 48px; } -} \ No newline at end of file +} diff --git a/packages/suika/src/components/Header/components/Toolbar/Toolbar.tsx b/packages/suika/src/components/Header/components/Toolbar/Toolbar.tsx index 09fe2cc3..7cbb40f7 100644 --- a/packages/suika/src/components/Header/components/Toolbar/Toolbar.tsx +++ b/packages/suika/src/components/Header/components/Toolbar/Toolbar.tsx @@ -12,6 +12,7 @@ import { SelectOutlined, TextFilled, } from '@suika/icons'; +import { Menu } from './menu'; export const ToolBar = () => { const editor = useContext(EditorContext); @@ -29,6 +30,7 @@ export const ToolBar = () => { return (
+ {( [ { diff --git a/packages/suika/src/components/Header/components/Toolbar/menu/Menu.tsx b/packages/suika/src/components/Header/components/Toolbar/menu/Menu.tsx new file mode 100644 index 00000000..87010bdb --- /dev/null +++ b/packages/suika/src/components/Header/components/Toolbar/menu/Menu.tsx @@ -0,0 +1,40 @@ +import { Dropdown } from '@suika/components'; +import { FC, useContext } from 'react'; +import { useIntl } from 'react-intl'; +import { exportService, importService } from '../../../../../editor'; +import { EditorContext } from '../../../../../context'; +import { MenuOutlined } from '@suika/icons'; +import './menu.scss'; + +export const Menu: FC = () => { + const intl = useIntl(); + const editor = useContext(EditorContext); + + const items = [ + { + key: 'import', + label: intl.formatMessage({ id: 'import.originFile' }), + }, + { + key: 'export', + label: intl.formatMessage({ id: 'export.originFile' }), + }, + ]; + + const handleClick = ({ key }: { key: string }) => { + if (!editor) return; + if (key === 'import') { + importService.importOriginFile(editor); + } else if (key === 'export') { + exportService.exportOriginFile(editor); + } + }; + + return ( + +
+ +
+
+ ); +}; diff --git a/packages/suika/src/components/Header/components/Toolbar/menu/index.ts b/packages/suika/src/components/Header/components/Toolbar/menu/index.ts new file mode 100644 index 00000000..629d3d0a --- /dev/null +++ b/packages/suika/src/components/Header/components/Toolbar/menu/index.ts @@ -0,0 +1 @@ +export * from './Menu'; diff --git a/packages/suika/src/components/Header/components/Toolbar/menu/menu.scss b/packages/suika/src/components/Header/components/Toolbar/menu/menu.scss new file mode 100644 index 00000000..4fdbb940 --- /dev/null +++ b/packages/suika/src/components/Header/components/Toolbar/menu/menu.scss @@ -0,0 +1,13 @@ +.sk-ed-menu-btn { + margin-right: 8px; + border-radius: 4px; + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background-color: #eee; + } +} diff --git a/packages/suika/src/components/ZoomActions/ZoomActions.scss b/packages/suika/src/components/ZoomActions/ZoomActions.scss index ee037172..3d673551 100644 --- a/packages/suika/src/components/ZoomActions/ZoomActions.scss +++ b/packages/suika/src/components/ZoomActions/ZoomActions.scss @@ -27,7 +27,6 @@ right: 6px; padding: 8px 0; - border: 1px solid #eee; border-radius: 4px; width: 200px; background-color: #fff; diff --git a/packages/suika/src/components/ZoomActions/components/ActionItem/ActionItem.scss b/packages/suika/src/components/ZoomActions/components/ActionItem/ActionItem.scss index 632a174b..139f4aa8 100644 --- a/packages/suika/src/components/ZoomActions/components/ActionItem/ActionItem.scss +++ b/packages/suika/src/components/ZoomActions/components/ActionItem/ActionItem.scss @@ -4,7 +4,8 @@ justify-content: space-between; align-items: center; - padding: 0 16px 0 8px; + padding-left: 4px; + padding-right: 16px; height: 24px; line-height: 24px; font-size: 12px; diff --git a/packages/suika/src/editor/commands/command_manager.ts b/packages/suika/src/editor/commands/command_manager.ts index 57241446..f4961e69 100644 --- a/packages/suika/src/editor/commands/command_manager.ts +++ b/packages/suika/src/editor/commands/command_manager.ts @@ -90,4 +90,9 @@ export class CommandManager { off(eventName: T, listener: Events[T]) { this.emitter.off(eventName, listener); } + clearRecords() { + this.redoStack = []; + this.undoStack = []; + this.emitStatusChange(); + } } diff --git a/packages/suika/src/editor/editor.ts b/packages/suika/src/editor/editor.ts index e4842469..4032393b 100644 --- a/packages/suika/src/editor/editor.ts +++ b/packages/suika/src/editor/editor.ts @@ -25,6 +25,7 @@ import { GroupManager } from './group_manager'; import { ControlHandleManager } from './scene/control_handle_manager'; import { SelectedBox } from './selected_box'; import { CanvasDragger } from './canvas_dragger'; +import { IEditorPaperData } from '../type'; interface IEditorOptions { containerElement: HTMLDivElement; @@ -124,11 +125,7 @@ export class Editor { const data = this.autoSaveGraphs.load(); if (data) { - if (data.groups) { - this.groupManager.load(data.groups); - } - this.sceneGraph.load(data.data); - this.paperId = data.paperId; + this.loadData(data); } this.paperId ??= genId(); this.autoSaveGraphs.autoSave(); @@ -156,6 +153,15 @@ export class Editor { this.sceneGraph.render(); }); } + loadData(data: IEditorPaperData) { + if (data.groups) { + this.groupManager.load(data.groups); + } + this.sceneGraph.load(data.data); + this.commandManager.clearRecords(); + this.paperId = data.paperId; + this.paperId ??= genId(); + } destroy() { this.containerElement.removeChild(this.canvasElement); this.textEditor.destroy(); diff --git a/packages/suika/src/editor/export_manager.ts b/packages/suika/src/editor/export_manager.ts new file mode 100644 index 00000000..4650f972 --- /dev/null +++ b/packages/suika/src/editor/export_manager.ts @@ -0,0 +1,19 @@ +import { Editor } from './editor'; + +export const ExportServer = { + exportOriginFile: (editor: Editor) => { + const data = editor.sceneGraph.toJSON(); + const blob = new Blob([JSON.stringify(data)], { + type: 'application/json', + }); + download(blob, 'design.suika'); + }, +}; + +const download = (blob: Blob, filename: string) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.setAttribute('download', filename); + a.click(); +}; diff --git a/packages/suika/src/editor/index.ts b/packages/suika/src/editor/index.ts new file mode 100644 index 00000000..a860bc05 --- /dev/null +++ b/packages/suika/src/editor/index.ts @@ -0,0 +1,2 @@ +export * from './editor'; +export * from './service'; diff --git a/packages/suika/src/editor/scene/graph.ts b/packages/suika/src/editor/scene/graph.ts index 7bfafe49..b4da5248 100644 --- a/packages/suika/src/editor/scene/graph.ts +++ b/packages/suika/src/editor/scene/graph.ts @@ -440,7 +440,7 @@ export class Graph { } } - toJSON() { + toJSON(): GraphAttrs { return { type: this.type, id: this.id, diff --git a/packages/suika/src/editor/scene/scene_graph.ts b/packages/suika/src/editor/scene/scene_graph.ts index c7db33fa..271946c7 100644 --- a/packages/suika/src/editor/scene/scene_graph.ts +++ b/packages/suika/src/editor/scene/scene_graph.ts @@ -412,17 +412,19 @@ export class SceneGraph { } toJSON() { + const data = arrMap(this.children, (item) => item.toJSON()); const paperData: IEditorPaperData = { appVersion: 'suika-editor_0.0.1', paperId: this.editor.paperId, groups: this.editor.groupManager.export(), - data: JSON.stringify(arrMap(this.children, (item) => item.toJSON())), + data: data, }; return JSON.stringify(paperData); } - addGraphsByStr(str: string) { - const data: GraphAttrs[] = JSON.parse(str); + addGraphsByStr(info: string | GraphAttrs[]) { + const data: GraphAttrs[] = + typeof info === 'string' ? JSON.parse(info) : info; const newChildren = data.map((attrs) => { const type = attrs.type; const Ctor = graphCtorMap[type!]; @@ -437,9 +439,9 @@ export class SceneGraph { return newChildren; } - load(str: string) { + load(info: string | GraphAttrs[]) { this.children = []; - this.addGraphsByStr(str); + this.addGraphsByStr(info); } on(eventName: 'render', handler: () => void) { diff --git a/packages/suika/src/editor/service/export_service.ts b/packages/suika/src/editor/service/export_service.ts new file mode 100644 index 00000000..7b13848c --- /dev/null +++ b/packages/suika/src/editor/service/export_service.ts @@ -0,0 +1,20 @@ +import { Editor } from '../editor'; + +export const exportService = { + exportOriginFile: (editor: Editor, filename = 'design') => { + const data = editor.sceneGraph.toJSON(); + const blob = new Blob([data], { + type: 'application/json', + }); + download(blob, filename + '.suika'); + }, +}; + +const download = (blob: Blob, filename: string) => { + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.setAttribute('download', filename); + a.click(); +}; diff --git a/packages/suika/src/editor/service/import_service.ts b/packages/suika/src/editor/service/import_service.ts new file mode 100644 index 00000000..93b608a5 --- /dev/null +++ b/packages/suika/src/editor/service/import_service.ts @@ -0,0 +1,37 @@ +import { Editor } from '../editor'; + +export const importService = { + importOriginFile: (editor: Editor) => { + readTextFile('.suika', (content) => { + editor.loadData(JSON.parse(content)); + }); + }, +}; + +function readTextFile( + accept: string, + callback: (contents: string) => void, +): void { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = accept; + input.style.display = 'none'; + + input.addEventListener('change', function (event) { + const file = (event.target as HTMLInputElement).files?.[0]; + if (!file) return; + + const reader = new FileReader(); + + reader.onload = function (e) { + const contents = e.target?.result as string; + if (contents) { + callback(contents); + } + }; + + reader.readAsText(file); + }); + + input.click(); +} diff --git a/packages/suika/src/editor/service/index.ts b/packages/suika/src/editor/service/index.ts index 1354f531..33380121 100644 --- a/packages/suika/src/editor/service/index.ts +++ b/packages/suika/src/editor/service/index.ts @@ -1,3 +1,5 @@ export * from './arrange_and_record'; export * from './mutate_graphs_and_record'; export * from './align_and_record'; +export * from './export_service'; +export * from './import_service'; diff --git a/packages/suika/src/locale/en.json b/packages/suika/src/locale/en.json index c7be51b3..522da460 100644 --- a/packages/suika/src/locale/en.json +++ b/packages/suika/src/locale/en.json @@ -41,5 +41,9 @@ "group": "Group selection", "showOrHide": "Show/Hide", - "lockOrUnlock": "Lock/Unlock" + "lockOrUnlock": "Lock/Unlock", + + "import.originFile": "Import local file", + + "export.originFile": "Save local copy" } diff --git a/packages/suika/src/locale/zh.json b/packages/suika/src/locale/zh.json index 23ba5a3e..18dd7734 100644 --- a/packages/suika/src/locale/zh.json +++ b/packages/suika/src/locale/zh.json @@ -40,5 +40,8 @@ "group": "创建编组", "showOrHide": "显示/隐藏", - "lockOrUnlock": "锁定/解锁" + "lockOrUnlock": "锁定/解锁", + + "import.originFile": "从本地导入", + "export.originFile": "导出到本地" } diff --git a/packages/suika/src/type/index.ts b/packages/suika/src/type/index.ts index a6425b2d..891085d7 100644 --- a/packages/suika/src/type/index.ts +++ b/packages/suika/src/type/index.ts @@ -1,6 +1,7 @@ import { IGroupsData } from '../editor/group_manager'; import { IRect } from '@suika/geo'; +import { GraphAttrs } from '../editor/scene/graph'; export type IBox = IRect; @@ -68,7 +69,7 @@ export interface IEditorPaperData { appVersion: string; paperId: string; groups: IGroupsData; - data: string; + data: GraphAttrs[]; } export interface IVerticalLine {