diff --git a/monosketch-svelte/src/lib/mono/file/mono-file.test.ts b/monosketch-svelte/src/lib/mono/file/mono-file.test.ts new file mode 100644 index 000000000..e7aa963cb --- /dev/null +++ b/monosketch-svelte/src/lib/mono/file/mono-file.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Point, PointF } from "$libs/graphics-geo/point"; +import { Extra, MonoFile } from "$mono/file/mono-file"; +import { SerializableLineConnector } from "$mono/shape/serialization/connector"; +import { SerializableGroup, SerializableRectangle } from "$mono/shape/serialization/shapes"; +import { describe, expect, test } from "vitest"; + +describe("MonoFile serialization", () => { + const root = SerializableGroup.create({ + id: "root", + isIdTemporary: false, + versionCode: 1, + shapes: [ + SerializableRectangle.EMPTY, + SerializableRectangle.EMPTY, + ] + }); + const connectors: SerializableLineConnector[] = [ + SerializableLineConnector.create({ + lineId: "lineId", + anchor: 0, + targetId: "targetId", + ratio: PointF.create({ left: 0.5, top: 0.6 }), + offset: Point.of(10, 20), + }), + SerializableLineConnector.create({ + lineId: "lineId1", + anchor: 1, + targetId: "targetId", + ratio: PointF.create({ left: 0.5, top: 0.6 }), + offset: Point.of(10, 20), + }), + ]; + const extra = Extra.create("Test", Point.of(30, 50)); + const modifiedTimestampMillis = 1000; + + test("should serialize MonoFile correctly", () => { + const monoFile = MonoFile.create(root, connectors, extra, modifiedTimestampMillis); + // @ts-expect-error toJson is attached by Jsonizable + const json = monoFile.toJson(); + expect(json).toEqual({ + // @ts-expect-error toJson is attached by Jsonizable + root: root.toJson(), + // @ts-expect-error toJson is attached by Jsonizable + extra: extra.toJson(), + version: 2, + modified_timestamp_millis: modifiedTimestampMillis, + // @ts-expect-error toJson is attached by Json + connectors: connectors.map(connector => connector.toJson()), + }); + }); + + test("should deserialize MonoFile correctly", () => { + const json = { + // @ts-expect-error toJson is attached by Jsonizable + root: root.toJson(), + // @ts-expect-error toJson is attached by Jsonizable + extra: extra.toJson(), + version: 2, + modified_timestamp_millis: modifiedTimestampMillis, + // @ts-expect-error toJson is attached by Json + connectors: connectors.map(connector => connector.toJson()), + }; + // @ts-expect-error fromJson is attached by Jsonizable + const monoFile = MonoFile.fromJson(json); + expect(monoFile).toEqual(MonoFile.create(root, connectors, extra, modifiedTimestampMillis)); + }); +}); diff --git a/monosketch-svelte/src/lib/mono/file/mono-file.ts b/monosketch-svelte/src/lib/mono/file/mono-file.ts new file mode 100644 index 000000000..d6f95b096 --- /dev/null +++ b/monosketch-svelte/src/lib/mono/file/mono-file.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Point } from "$libs/graphics-geo/point"; +import { + type SerializableLineConnector, + SerializableLineConnectorArraySerializer, +} from "$mono/shape/serialization/connector"; +import { Jsonizable, Serializer, SerialName } from "$mono/shape/serialization/serializable"; +import { SerializableGroup } from "$mono/shape/serialization/shapes"; + +/** + * Version of the mono file + * + * # Version 0 + * No mono file format. It was just a serialization of the root group + * + * # Version 1 + * The first version of MonoFile. + * - root: root group content + * - extra: + * - name + * - offset + * - version: 1 + * - modified_timestamp_millis: timestamp in millisecond (local time) + * + * # Version 2 + * Include `connectors` + * - connectors: list of serialization of line connectors + */ +const MONO_FILE_VERSION = 2; + +/** + * A data class for serializing shape to Json and load shape from Json. + */ +@Jsonizable +export class MonoFile { + @SerialName("root") + root: SerializableGroup = SerializableGroup.EMPTY; + @SerialName("extra") + extra: Extra = Extra.create("", Point.ZERO); + @SerialName("version") + version: number = 0; + @SerialName("modified_timestamp_millis") + modifiedTimestampMillis: number = 0; + @SerialName("connectors") + @Serializer(SerializableLineConnectorArraySerializer) + connectors: SerializableLineConnector[] = []; + + static create( + root: SerializableGroup, + connectors: SerializableLineConnector[], + extra: Extra, + modifiedTimestampMillis: number = Date.now(), + ): MonoFile { + const file = new MonoFile(); + file.root = root; + file.connectors = connectors; + file.extra = extra; + file.version = MONO_FILE_VERSION; + file.modifiedTimestampMillis = modifiedTimestampMillis; + return file; + } +} + +@Jsonizable +export class Extra { + @SerialName("name") + name: string = ""; + @SerialName("offset") + offset: Point = Point.ZERO; + + private constructor() { + } + + static create(name: string, offset: Point): Extra { + const extra = new Extra(); + extra.name = name; + extra.offset = offset; + return extra; + } +} diff --git a/monosketch-svelte/src/lib/mono/file/shape-serialization-util.test.ts b/monosketch-svelte/src/lib/mono/file/shape-serialization-util.test.ts new file mode 100644 index 000000000..5df6074ea --- /dev/null +++ b/monosketch-svelte/src/lib/mono/file/shape-serialization-util.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { describe, expect, test } from "vitest"; +import { ShapeSerializationUtil } from './shape-serialization-util'; +import { SerializableGroup, SerializableRectangle } from '$mono/shape/serialization/shapes'; +import { SerializableLineConnector } from '$mono/shape/serialization/connector'; +import { Point, PointF } from '$libs/graphics-geo/point'; +import { MonoFile, Extra } from './mono-file'; + +describe('ShapeSerializationUtil', () => { + const root = SerializableGroup.create({ + id: "root", + isIdTemporary: false, + versionCode: 2, + shapes: [ + SerializableRectangle.EMPTY, + SerializableRectangle.EMPTY, + ] + }); + const connectors: SerializableLineConnector[] = [ + SerializableLineConnector.create({ + lineId: "lineId", + anchor: 1, + targetId: "targetId", + ratio: PointF.create({ left: 0.5, top: 0.6 }), + offset: Point.of(10, 20), + }), + SerializableLineConnector.create({ + lineId: "lineId1", + anchor: 0, + targetId: "targetId", + ratio: PointF.create({ left: 0.5, top: 0.6 }), + offset: Point.of(10, 20), + }), + ]; + const extra = Extra.create("Test", Point.of(30, 50)); + const modifiedTimestampMillis = 1000; + + test('should serialize and deserialize a shape correctly', () => { + const json = ShapeSerializationUtil.toShapeJson(root); + console.log(json); + const deserializedShape = ShapeSerializationUtil.fromShapeJson(json); + expect(deserializedShape).toEqual(root); + }); + + test('should serialize and deserialize connectors correctly', () => { + const json = ShapeSerializationUtil.toConnectorsJson(connectors); + console.log(json); + const deserializedConnectors = ShapeSerializationUtil.fromConnectorsJson(json); + expect(deserializedConnectors).toEqual(connectors); + }); + + test('should serialize and deserialize a MonoFile correctly', () => { + const monoFile = MonoFile.create(root, connectors, extra, modifiedTimestampMillis); + const json = ShapeSerializationUtil.toMonoFileJson(monoFile); + console.log(json); + const deserializedMonoFile = ShapeSerializationUtil.fromMonoFileJson(json); + expect(deserializedMonoFile).toEqual(monoFile); + }); +}); diff --git a/monosketch-svelte/src/lib/mono/file/shape-serialization-util.ts b/monosketch-svelte/src/lib/mono/file/shape-serialization-util.ts new file mode 100644 index 000000000..49b4f07fc --- /dev/null +++ b/monosketch-svelte/src/lib/mono/file/shape-serialization-util.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { SerializableGroup, AbstractSerializableShape, deserializeShape } from '$mono/shape/serialization/shapes'; +import { + SerializableLineConnector, + SerializableLineConnectorArraySerializer, +} from '$mono/shape/serialization/connector'; +import { Point } from '$libs/graphics-geo/point'; +import { MonoFile, Extra } from './mono-file'; + +export class ShapeSerializationUtil { + static toShapeJson(serializableShape: AbstractSerializableShape): string { + // @ts-expect-error toJson is attached by Jsonizable + return JSON.stringify(serializableShape.toJson()); + } + + static fromShapeJson(jsonString: string): AbstractSerializableShape | null { + try { + const json = JSON.parse(jsonString); + return deserializeShape(json); + } catch (e) { + console.error("Error while restoring shapes"); + console.error(e); + return null; + } + } + + static toConnectorsJson(connectors: SerializableLineConnector[]): string { + return JSON.stringify(SerializableLineConnectorArraySerializer.serialize(connectors)); + } + /* eslint-disable @typescript-eslint/no-explicit-any */ + static fromConnectorsJson(jsonString: string): SerializableLineConnector[] { + try { + const json = JSON.parse(jsonString) as any[]; + return SerializableLineConnectorArraySerializer.deserialize(json); + } catch (e) { + console.error("Error while restoring connectors"); + console.error(e); + return []; + } + } + /* eslint-enable @typescript-eslint/no-explicit-any */ + + static toMonoFileJson(monoFile: MonoFile): string { + // @ts-expect-error toJson is attached by Jsonizable + const json = monoFile.toJson(); + return JSON.stringify(json); + } + + static fromMonoFileJson(jsonString: string): MonoFile | null { + try { + const json = JSON.parse(jsonString); + // @ts-expect-error fromJson is attached by Jsonizable + return MonoFile.fromJson(json); + } catch (e) { + // Fallback to version 0 + const shape = this.fromShapeJson(jsonString) as SerializableGroup | null; + if (shape) { + return MonoFile.create(shape, [], Extra.create("", Point.ZERO)); + } else { + return null; + } + } + } +} diff --git a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts index de6c262a1..850a0225e 100644 --- a/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts +++ b/monosketch-svelte/src/lib/mono/monobitmap/board/board.ts @@ -183,6 +183,12 @@ export class MonoBoard { return painterBoard.toString(); }; + + toStringInBound = (bound: Rect): string => { + const painterBoard = new PainterBoard(bound); + Array.from(this.painterBoards.values()).forEach((pb) => painterBoard.fill(pb)); + return painterBoard.toString(); + }; } type BoardAddress = { diff --git a/monosketch-svelte/src/lib/mono/shape/serialization/serializable.ts b/monosketch-svelte/src/lib/mono/shape/serialization/serializable.ts index 131c94305..3289c9e94 100644 --- a/monosketch-svelte/src/lib/mono/shape/serialization/serializable.ts +++ b/monosketch-svelte/src/lib/mono/shape/serialization/serializable.ts @@ -85,12 +85,13 @@ export function Jsonizable(constructor: Function) { // 1st: Check if the field has a serializer // 2nd: Check if the field has a toJson method // 3rd: Use the value directly + const field = instance[key]; if (serializers[key]) { - json[serializedKey] = serializers[key].serialize(instance[key]); - } else if (instance[key].toJson) { - json[serializedKey] = instance[key].toJson(); + json[serializedKey] = serializers[key].serialize(field); + } else if (field && field.toJson) { + json[serializedKey] = field.toJson(); } else { - json[serializedKey] = instance[key]; + json[serializedKey] = field; } } return json; diff --git a/monosketch-svelte/src/lib/mono/shape/serialization/shapes.ts b/monosketch-svelte/src/lib/mono/shape/serialization/shapes.ts index 9595828a2..6fd13f609 100644 --- a/monosketch-svelte/src/lib/mono/shape/serialization/shapes.ts +++ b/monosketch-svelte/src/lib/mono/shape/serialization/shapes.ts @@ -68,6 +68,8 @@ export class SerializableRectangle extends AbstractSerializableShape { super(); } + static EMPTY: SerializableRectangle = new SerializableRectangle(); + static create( { id, @@ -221,6 +223,7 @@ export class SerializableLine extends AbstractSerializableShape { return result; } } + /* eslint-disable @typescript-eslint/no-explicit-any */ export const ShapeArraySerializer = { serialize: (value: AbstractSerializableShape[]): any[] => { @@ -229,27 +232,30 @@ export const ShapeArraySerializer = { }, deserialize: (value: any[]): AbstractSerializableShape[] => { - return value.map((json) => { - const type = json["type"]; - switch (type) { - case "R": - // @ts-expect-error fromJson is attached by Jsonizable - return SerializableRectangle.fromJson(json); - case "T": - // @ts-expect-error fromJson is attached by Jsonizable - return SerializableText.fromJson(json); - case "L": - // @ts-expect-error fromJson is attached by Jsonizable - return SerializableLine.fromJson(json); - case "G": - // @ts-expect-error fromJson is attached by Jsonizable - return SerializableGroup.fromJson(json); - default: - throw new Error(`Unrecognizable type ${type}`); - } - }); + return value.map(deserializeShape); }, }; + +export function deserializeShape(shape: any): AbstractSerializableShape { + const type = shape["type"]; + switch (type) { + case "R": + // @ts-expect-error fromJson is attached by Jsonizable + return SerializableRectangle.fromJson(shape); + case "T": + // @ts-expect-error fromJson is attached by Jsonizable + return SerializableText.fromJson(shape); + case "L": + // @ts-expect-error fromJson is attached by Jsonizable + return SerializableLine.fromJson(shape); + case "G": + // @ts-expect-error fromJson is attached by Jsonizable + return SerializableGroup.fromJson(shape); + default: + throw new Error(`Unrecognizable type ${type}`); + } +} + /* eslint-enable @typescript-eslint/no-explicit-any */ /** @@ -274,6 +280,17 @@ export class SerializableGroup extends AbstractSerializableShape { super(); } + copy({ isIdTemporary = this.isIdTemporary, }: { isIdTemporary: boolean; }): SerializableGroup { + return SerializableGroup.create({ + id: this.id, + isIdTemporary, + versionCode: this.versionCode, + shapes: this.shapes, + }); + } + + static EMPTY: SerializableGroup = new SerializableGroup(); + static create( { id, diff --git a/monosketch-svelte/src/lib/mono/state-manager/export/export-shapes-helper.ts b/monosketch-svelte/src/lib/mono/state-manager/export/export-shapes-helper.ts new file mode 100644 index 000000000..b421e3e8e --- /dev/null +++ b/monosketch-svelte/src/lib/mono/state-manager/export/export-shapes-helper.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +import { Rect } from "$libs/graphics-geo/rect"; +import { MonoBitmap } from "$mono/monobitmap/bitmap/monobitmap"; +import { MonoBoard } from "$mono/monobitmap/board"; +import { HighlightType } from "$mono/monobitmap/board/pixel"; +import type { AbstractShape } from "$mono/shape/shape/abstract-shape"; +import { Group } from "$mono/shape/shape/group"; + +/** + * A helper class for exporting selected shapes. + */ +export class ExportShapesHelper { + + constructor( + private getBitmap: (shape: AbstractShape) => MonoBitmap.Bitmap | null, + private setClipboardText: (text: string) => void, + ) { + } + + exportText(shapes: AbstractShape[], isModalRequired: boolean): void { + if (shapes.length === 0) { + return; + } + + const left = Math.min(...shapes.map(shape => shape.bound.left)); + const right = Math.max(...shapes.map(shape => shape.bound.right)); + const top = Math.min(...shapes.map(shape => shape.bound.top)); + const bottom = Math.max(...shapes.map(shape => shape.bound.bottom)); + const window = Rect.byLTRB(left, top, right, bottom); + + const exportingBoard = new MonoBoard(); + exportingBoard.clearAndSetWindow(window); + this.drawShapesOntoExportingBoard(exportingBoard, shapes); + + const text = exportingBoard.toStringInBound(window); + if (isModalRequired) { + // TODO: Show modal + // new ExportShapesModal().show(text); + } else { + this.setClipboardText(text); + } + } + + private drawShapesOntoExportingBoard(board: MonoBoard, shapes: Iterable): void { + for (const shape of shapes) { + if (shape instanceof Group) { + this.drawShapesOntoExportingBoard(board, shape.items); + continue; + } + const bitmap = this.getBitmap(shape); + if (bitmap) { + board.fillBitmap(shape.bound.position, bitmap, HighlightType.NO); + } + } + } +} diff --git a/monosketch-svelte/src/lib/mono/state-manager/onetimeaction/file-related-action-helper.ts b/monosketch-svelte/src/lib/mono/state-manager/onetimeaction/file-related-action-helper.ts index 69ed05733..4f8c35d9d 100644 --- a/monosketch-svelte/src/lib/mono/state-manager/onetimeaction/file-related-action-helper.ts +++ b/monosketch-svelte/src/lib/mono/state-manager/onetimeaction/file-related-action-helper.ts @@ -3,28 +3,39 @@ */ import type { ProjectActionType } from "$mono/action-manager/one-time-actions"; +import { Extra, MonoFile } from "$mono/file/mono-file"; +import { ShapeSerializationUtil } from "$mono/file/shape-serialization-util"; import type { MonoBitmapManager } from "$mono/monobitmap/manager/mono-bitmap-manager"; import { ShapeConnector } from "$mono/shape/connector/shape-connector"; import type { ShapeClipboardManager } from "$mono/shape/shape-clipboard-manager"; import { Group, RootGroup } from "$mono/shape/shape/group"; import type { CommandEnvironment } from "$mono/state-manager/command-environment"; +import { ExportShapesHelper } from "$mono/state-manager/export/export-shapes-helper"; import { FileMediator } from "$mono/state-manager/onetimeaction/file-mediator"; import type { WorkspaceDao } from "$mono/store-manager/dao/workspace-dao"; +import { DEFAULT_NAME } from "$mono/store-manager/dao/workspace-object-dao"; +import { modalViewModel } from "$ui/modal/viewmodel"; +/** + * A helper class to handle file-related one-time actions in the application. + * This class provides methods such as creating new projects, saving and loading shapes to/from + * files, exporting selected shapes to text format, etc. + */ export class FileRelatedActionsHelper { - private fileMediator: FileMediator = new FileMediator(); - // private exportShapesHelper: ExportShapesHelper; + private readonly fileMediator: FileMediator = new FileMediator(); + + private exportShapesHelper: ExportShapesHelper; constructor( private environment: CommandEnvironment, bitmapManager: MonoBitmapManager, shapeClipboardManager: ShapeClipboardManager, - private workspaceDao: WorkspaceDao + private workspaceDao: WorkspaceDao, ) { - // this.exportShapesHelper = new ExportShapesHelper( - // bitmapManager.getBitmap.bind(bitmapManager), - // shapeClipboardManager.setClipboardText.bind(shapeClipboardManager) - // ); + this.exportShapesHelper = new ExportShapesHelper( + (shape) => bitmapManager.getBitmap(shape), + shapeClipboardManager.setClipboardText.bind(shapeClipboardManager) + ); } handleProjectAction(projectAction: ProjectActionType) { @@ -79,71 +90,72 @@ export class FileRelatedActionsHelper { } private renameProject(newName: string) { - const currentRootId = this.environment.shapeManager.root.id; - this.workspaceDao.getObject(currentRootId).name = newName; + this.workspaceDao.getObject(this.environment.shapeManager.root.id).name = newName; this.environment.shapeManager.notifyProjectUpdate(); } private saveCurrentShapesToFile() { - // const currentRoot = this.environment.shapeManager.root; - // const objectDao = this.workspaceDao.getObject(currentRoot.id); - // const name = objectDao.name; - // const offset = objectDao.offset; - // const jsonString = ShapeSerializationUtil.toMonoFileJson({ - // name, - // serializableShape: currentRoot.toSerializableShape(true), - // connectors: this.environment.shapeManager.shapeConnector.toSerializable(), - // offset - // }); - // this.fileMediator.saveFile(name, jsonString); + const currentRoot = this.environment.shapeManager.root; + const objectDao = this.workspaceDao.getObject(currentRoot.id); + const name = objectDao.name; + const offset = objectDao.offset; + const monoFile = MonoFile.create( + currentRoot.toSerializableShape(true), + this.environment.shapeManager.shapeConnector.toSerializable(), + Extra.create(name, offset), + ); + const jsonString = ShapeSerializationUtil.toMonoFileJson(monoFile); + this.fileMediator.saveFile(name, jsonString); } private loadShapesFromFile() { - // this.fileMediator.openFile(jsonString => { - // const monoFile = ShapeSerializationUtil.fromMonoFileJson(jsonString); - // if (monoFile) { - // this.applyMonoFileToWorkspace(monoFile); - // } else { - // console.warn("Failed to load shapes from file."); - // // TODO: Show error dialog - // } - // }); + this.fileMediator.openFile(jsonString => { + const monoFile = ShapeSerializationUtil.fromMonoFileJson(jsonString); + if (monoFile) { + this.applyMonoFileToWorkspace(monoFile); + } else { + console.warn("Failed to load shapes from file."); + // TODO: Show error dialog + } + }); + } + + private applyMonoFileToWorkspace(monoFile: MonoFile) { + const rootGroup = RootGroup(monoFile.root); + const existingProject = this.workspaceDao.getObject(rootGroup.id); + if (existingProject.rootGroup) { + modalViewModel.existingProjectFlow.value = { + projectName: existingProject.name, + lastEditedTimeMillis: existingProject.lastModifiedTimestampMillis, + onReplace: () => { + this.prepareAndApplyNewRoot(RootGroup(monoFile.root.copy({ isIdTemporary: true })), monoFile.extra); + }, + onKeepBoth: () => { + this.prepareAndApplyNewRoot(rootGroup, monoFile.extra); + } + }; + } else { + this.prepareAndApplyNewRoot(rootGroup, monoFile.extra); + } } - // private applyMonoFileToWorkspace(monoFile: MonoFile) { - // const rootGroup = RootGroup(monoFile.root); - // const existingProject = this.workspaceDao.getObject(rootGroup.id); - // if (existingProject.rootGroup) { - // showExitingProjectDialog( - // existingProject.name, - // existingProject.lastModifiedTimestampMillis, - // () => { - // this.prepareAndApplyNewRoot(RootGroup(monoFile.root.copy({ isIdTemporary: true })), monoFile.extra); - // }, - // () => { - // this.prepareAndApplyNewRoot(rootGroup, monoFile.extra); - // } - // ); - // } else { - // this.prepareAndApplyNewRoot(rootGroup, monoFile.extra); - // } - // } - - // private prepareAndApplyNewRoot(rootGroup: Group, extra: Extra) { - // const objectDao = this.workspaceDao.getObject(rootGroup.id); - // objectDao.name = extra.name || WorkspaceObjectDao.DEFAULT_NAME; - // objectDao.offset = extra.offset; - // this.replaceWorkspace(rootGroup); - // } + private prepareAndApplyNewRoot(rootGroup: Group, extra: Extra) { + const objectDao = this.workspaceDao.getObject(rootGroup.id); + objectDao.name = extra.name || DEFAULT_NAME; + objectDao.offset = extra.offset; + this.replaceWorkspace(rootGroup); + } exportSelectedShapes(isModalRequired: boolean) { - // const selectedShapes = this.environment.getSelectedShapes(); - // const extractableShapes = selectedShapes.size > 0 - // ? this.environment.workingParentGroup.items.filter(item => selectedShapes.includes(item)) - // : isModalRequired - // ? [this.environment.workingParentGroup] - // : []; - // this.exportShapesHelper.exportText(extractableShapes, isModalRequired); + this.exportShapesHelper.exportText(this.createExtractableShapes(isModalRequired), isModalRequired); + } + + private createExtractableShapes(isModalRequired: boolean) { + const selectedShapes = this.environment.getSelectedShapes(); + if (selectedShapes.size === 0) { + return isModalRequired ? [this.environment.workingParentGroup] : []; + } + return Array.from(this.environment.workingParentGroup.items).filter(item => selectedShapes.has(item)); } private replaceWorkspace(rootGroup: Group) { diff --git a/monosketch-svelte/src/lib/mono/store-manager/dao/workspace-object-dao.ts b/monosketch-svelte/src/lib/mono/store-manager/dao/workspace-object-dao.ts index 39544c4c6..ca0ed7682 100644 --- a/monosketch-svelte/src/lib/mono/store-manager/dao/workspace-object-dao.ts +++ b/monosketch-svelte/src/lib/mono/store-manager/dao/workspace-object-dao.ts @@ -109,4 +109,4 @@ export class WorkspaceObjectDao { } } -const DEFAULT_NAME = "Undefined"; +export const DEFAULT_NAME = "Undefined"; diff --git a/monosketch-svelte/src/lib/style/theme.scss b/monosketch-svelte/src/lib/style/theme.scss index 7bf23a896..2ba57ba73 100644 --- a/monosketch-svelte/src/lib/style/theme.scss +++ b/monosketch-svelte/src/lib/style/theme.scss @@ -1,70 +1,98 @@ @import 'theme-utils'; $themeMap: ( - // Header overall - --header-bg: (#2c2c2c, #2c2c2c), - --header-color: (#fff, #fff), - // Header - App action icon - --nav-action-hover-bg: (#1c1c1c, #1c1c1c), - --nav-action-selected-border: (#fff, #fff), - // Tooltip - --tooltip-bg: (#000000dd, #000000dd), - --tooltip-color: (#fff, #fff), - // Workspace - --workspace-bg-color: (#fff, #121212), - --workspace-edittext-color: (#000, #fff), - // Shape tool - Overall - --shapetool-bg-color: (#fafafa, #1f1f1f), - --shapetool-main-divider-color: (#bbb, #616161), - // Shape tool - Text Input component - --text-input-border-color: (#e6e6e6, #565656), - --text-input-border-focus-color: (#027fe6, #e8b32f), - --text-input-label-color: (#6b6a6a, #9f9f9f), - --text-input-value-color: (#000, #fff), - // Shape tool - Option cloud - --comp-option-cloud-border-color: (#bbb, #f0f0f0), - --comp-option-cloud-border-selected-color: (#027fe6, #ffb800), - // Shape tool - Footer - --shapetool-footer-color: (#aaa, #aaa), - --shapetool-footer-hover-color: (#888, #888), - --shapetool-footer-active-color: (#999, #999), - // Shape tool - Indicator - --shapetool-indicator-color: (#888, #888), - // Shape tool - Section - --shapetool-section-divider-color: (#bbb, #6d6d6d), - --shapetool-section-title-color: (#6d6d6d, #eaeaea), - --shapetool-section-content-color: (#000, #f0f0f0), - // Shape tool - Tool - --shapetool-tool-title-color: (#1f1f1f, #e0e0e0), - --shapetool-tool-subtitle-color: (#a0a0a0, #979797), - // Shape tool - Reorder - --shapetool-reorder-hover-bg: (#efefef, #333333), - // Common modal - --modal-background-bg: (rgba(0, 0, 0, 0.7), rgba(30, 30, 30, 0.3)), - --modal-container-bg: (#f3f3f3, #313131), - --modal-close-button-color: (#666, #ffffff), - --modal-title-color: (rgba(0, 0, 0, 0.68), #fff), - --modal-content-color: (#000, #ffffff), - --modal-action-color: (#037de1, #c08b03), - --modal-action-hover-color: (#2598f7, #dfa104), - --modal-action-pressed-color: (#0360ac, #8e6601), - // Keyboard shortcuts - --keyboard-content-key-bg: (#fff, #262626), - --keyboard-content-key-border-color: (rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15)), - --keyboard-content-key-color: (rgba(0, 0, 0, 0.54), rgba(255, 255, 255, 0.88)), - // Main dropdown menu - --mainmenu-divider-color: (#bdbdbd, #6b6b6b), - --mainmenu-item-color: (rgb(36, 41, 47), #fff), - --mainmenu-item-hover-bg: (#e7e6e6, #414141), - --mainmenu-item-action-hover-bg: (#dcdcdc, #565555), - // Export text - --exporttext-container-bg: (#fff, #151515), - --exporttext-content-color: (#000, #fff), - // Project management - --danger-item-bg: (#e53935, #e53935) + // Header overall + --header-bg: (#2c2c2c, #2c2c2c), + --header-color: (#fff, #fff), + // Header - App action icon + --nav-action-hover-bg: (#1c1c1c, #1c1c1c), + --nav-action-selected-border: (#fff, #fff), + // Tooltip + --tooltip-bg: (#000000dd, #000000dd), + --tooltip-color: (#fff, #fff), + // Workspace + --workspace-bg-color: (#fff, #121212), + --workspace-edittext-color: (#000, #fff), + // Shape tool - Overall + --shapetool-bg-color: (#fafafa, #1f1f1f), + --shapetool-main-divider-color: (#bbb, #616161), + // Shape tool - Text Input component + --text-input-border-color: (#e6e6e6, #565656), + --text-input-border-focus-color: (#027fe6, #e8b32f), + --text-input-label-color: (#6b6a6a, #9f9f9f), + --text-input-value-color: (#000, #fff), + // Shape tool - Option cloud + --comp-option-cloud-border-color: (#bbb, #f0f0f0), + --comp-option-cloud-border-selected-color: (#027fe6, #ffb800), + // Shape tool - Footer + --shapetool-footer-color: (#aaa, #aaa), + --shapetool-footer-hover-color: (#888, #888), + --shapetool-footer-active-color: (#999, #999), + // Shape tool - Indicator + --shapetool-indicator-color: (#888, #888), + // Shape tool - Section + --shapetool-section-divider-color: (#bbb, #6d6d6d), + --shapetool-section-title-color: (#6d6d6d, #eaeaea), + --shapetool-section-content-color: (#000, #f0f0f0), + // Shape tool - Tool + --shapetool-tool-title-color: (#1f1f1f, #e0e0e0), + --shapetool-tool-subtitle-color: (#a0a0a0, #979797), + // Shape tool - Reorder + --shapetool-reorder-hover-bg: (#efefef, #333333), + // Common modal + --modal-background-bg: (rgba(0, 0, 0, 0.7), rgba(30, 30, 30, 0.3)), + --modal-container-bg: (#f3f3f3, #313131), + --modal-close-button-color: (#666, #ffffff), + --modal-title-color: (rgba(0, 0, 0, 0.68), #fff), + --modal-content-color: (#000, #ffffff), + --modal-action-color: (#037de1, #c08b03), + --modal-action-hover-color: (#2598f7, #dfa104), + --modal-action-pressed-color: (#0360ac, #8e6601), + // Keyboard shortcuts + --keyboard-content-key-bg: (#fff, #262626), + --keyboard-content-key-border-color: (rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15)), + --keyboard-content-key-color: (rgba(0, 0, 0, 0.54), rgba(255, 255, 255, 0.88)), + // Main dropdown menu + --mainmenu-divider-color: (#bdbdbd, #6b6b6b), + --mainmenu-item-color: (rgb(36, 41, 47), #fff), + --mainmenu-item-hover-bg: (#e7e6e6, #414141), + --mainmenu-item-action-hover-bg: (#dcdcdc, #565555), + // Export text + --exporttext-container-bg: (#fff, #151515), + --exporttext-content-color: (#000, #fff), + // Project management + --danger-item-bg: (#e53935, #e53935) +); + +$mainModalColors: ( + --titleText: (rgb(28, 26, 26), #FFF), + --contentText: (#5e5d62, #f2f2f2), +); + +$primaryActionThemeColors: ( + --primary-action-color: (#007bff, #cc8108), + --primary-action-hover-color: (#328ced, #daa113), + --primary-action-pressed-color: (#0062cc, #ab7a08), +); + +$primaryActionDangerThemeColors: ( + --primary-action-color: (#E53935, #E53935), + --primary-action-hover-color: (#FF5252, #FF5252), + --primary-action-pressed-color: (#C62828, #C62828), +); + +$secondaryActionThemeColors: ( + --secondary-action-border-color: (#D9D8DC, #74727a), + --secondary-action-text-color: (#323136, #e0dce9), + --secondary-action-hover-color: (#6c757d, #3e3d43), + --secondary-action-pressed-color: (#424950, #262628), ); // Apply the style values :root { - @include theme($themeMap); + @include theme($themeMap); + @include theme($mainModalColors, ".dialog-modal"); + @include theme($primaryActionThemeColors, ".dialog-modal .primary"); + @include theme($primaryActionDangerThemeColors, ".dialog-modal .primary.danger"); + @include theme($secondaryActionThemeColors, ".dialog-modal .secondary"); } diff --git a/monosketch-svelte/src/lib/ui/modal/ModalHolder.svelte b/monosketch-svelte/src/lib/ui/modal/ModalHolder.svelte index 2bc069110..606d6f4cb 100644 --- a/monosketch-svelte/src/lib/ui/modal/ModalHolder.svelte +++ b/monosketch-svelte/src/lib/ui/modal/ModalHolder.svelte @@ -12,6 +12,8 @@ import type { RenameProjectModel } from './rename-project/model'; import RenameProjectModal from './rename-project/RenameProjectModal.svelte'; import type { CurrentProjectModel } from './menu/current-project/model'; import type { Rect } from '$libs/graphics-geo/rect'; +import type { ExistingProjectModel } from "$ui/modal/existing-project/model"; +import ExistingProjectDialog from "$ui/modal/existing-project/ExistingProjectDialog.svelte"; let mainDropDownTarget: Rect | null = null; let currentProjectDropDownModel: CurrentProjectModel | null = null; @@ -20,6 +22,8 @@ let renamingProjectModel: RenameProjectModel | null = null; let tooltip: Tooltip | null = null; let shortcutModal: boolean = false; +let existingProjectModel: ExistingProjectModel | null = null; + const lifecycleOwner = new LifecycleOwner(); onMount(() => { lifecycleOwner.onStart(); @@ -47,6 +51,10 @@ onMount(() => { modalViewModel.keyboardShortcutVisibilityStateFlow.observe(lifecycleOwner, (value) => { shortcutModal = value; }); + + modalViewModel.existingProjectFlow.observe(lifecycleOwner, (value) => { + existingProjectModel = value; + }); }); onDestroy(() => { @@ -77,3 +85,7 @@ onDestroy(() => { {#if shortcutModal} {/if} + +{#if existingProjectModel} + +{/if} diff --git a/monosketch-svelte/src/lib/ui/modal/common/Dialog.svelte b/monosketch-svelte/src/lib/ui/modal/common/Dialog.svelte new file mode 100644 index 000000000..442ea0ed5 --- /dev/null +++ b/monosketch-svelte/src/lib/ui/modal/common/Dialog.svelte @@ -0,0 +1,156 @@ + + + + +
+ +
+ + diff --git a/monosketch-svelte/src/lib/ui/modal/existing-project/ExistingProjectDialog.svelte b/monosketch-svelte/src/lib/ui/modal/existing-project/ExistingProjectDialog.svelte new file mode 100644 index 000000000..82da364b7 --- /dev/null +++ b/monosketch-svelte/src/lib/ui/modal/existing-project/ExistingProjectDialog.svelte @@ -0,0 +1,56 @@ + + + + + model.onReplace()} + + cancelText="Keep both" + onCancel={() => model.onKeepBoth()} + + onDismiss={handleDismiss} +> +
+

Same project id exists in the data store

+
    +
  • Project name: {model.projectName}
  • +
  • Last edit: {createReadableDate(model.lastEditedTimeMillis)}
  • +
+
+
+ + diff --git a/monosketch-svelte/src/lib/ui/modal/existing-project/model.ts b/monosketch-svelte/src/lib/ui/modal/existing-project/model.ts new file mode 100644 index 000000000..15205cb77 --- /dev/null +++ b/monosketch-svelte/src/lib/ui/modal/existing-project/model.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2024, tuanchauict + */ + +export interface ExistingProjectModel { + projectName: string; + lastEditedTimeMillis: number; + + onKeepBoth: () => void; + onReplace: () => void; +} diff --git a/monosketch-svelte/src/lib/ui/modal/menu/common/MenuItem.svelte b/monosketch-svelte/src/lib/ui/modal/menu/common/MenuItem.svelte index 6a9052064..2ed4f8563 100644 --- a/monosketch-svelte/src/lib/ui/modal/menu/common/MenuItem.svelte +++ b/monosketch-svelte/src/lib/ui/modal/menu/common/MenuItem.svelte @@ -26,6 +26,7 @@ button { cursor: pointer; overflow: hidden; color: var(--mainmenu-item-color); + background: none; white-space: nowrap; font-size: 14px; line-height: 21px; diff --git a/monosketch-svelte/src/lib/ui/modal/viewmodel.ts b/monosketch-svelte/src/lib/ui/modal/viewmodel.ts index 5d8851325..936ea32cf 100644 --- a/monosketch-svelte/src/lib/ui/modal/viewmodel.ts +++ b/monosketch-svelte/src/lib/ui/modal/viewmodel.ts @@ -1,4 +1,5 @@ import { Flow } from '$libs/flow'; +import type { ExistingProjectModel } from "$ui/modal/existing-project/model"; import type { Tooltip } from './tooltip/model'; import type { RenameProjectModel } from './rename-project/model'; import type { CurrentProjectModel } from './menu/current-project/model'; @@ -14,6 +15,8 @@ class ModalViewModel { tooltipFlow: Flow = new Flow(null); keyboardShortcutVisibilityStateFlow: Flow = new Flow(false); + + existingProjectFlow: Flow = new Flow(null); } export const modalViewModel = new ModalViewModel();