AstralView/packages/sdk/src/core/goview/convert.ts

273 lines
8.3 KiB
TypeScript

import {
ASTRALVIEW_SCHEMA_VERSION,
createEmptyScreen,
type ImageWidgetNode,
type IframeWidgetNode,
type Screen,
type TextWidgetNode,
type VideoWidgetNode,
} from '../schema';
import { convertGoViewImageOptionToNodeProps, type GoViewImageOption } from '../widgets/image';
import { convertGoViewIframeOptionToNodeProps, type GoViewIframeOption } from '../widgets/iframe';
import { convertGoViewVideoOptionToNodeProps, type GoViewVideoOption } from '../widgets/video';
import { convertGoViewTextOptionToNodeProps, type GoViewTextOption } from '../widgets/text';
export interface GoViewComponentLike {
id?: string;
// some exports wrap the actual component under a nested field
component?: GoViewComponentLike;
// component identity
key?: string; // e.g. "TextCommon" (sometimes)
componentKey?: string;
chartConfig?: {
key?: string;
chartKey?: string;
type?: string;
option?: unknown;
data?: unknown;
};
// geometry
attr?: { x: number; y: number; w: number; h: number; zIndex?: number };
// state
status?: { lock?: boolean; hide?: boolean };
// widget-specific config
option?: unknown;
}
export interface GoViewEditCanvasConfigLike {
projectName?: string;
width?: number;
height?: number;
background?: string;
}
export interface GoViewStorageLike {
editCanvasConfig?: GoViewEditCanvasConfigLike;
componentList?: GoViewComponentLike[];
}
export interface GoViewProjectLike {
width?: number;
height?: number;
canvas?: { width?: number; height?: number };
componentList?: GoViewComponentLike[];
// persisted store shape (some variants)
editCanvasConfig?: GoViewEditCanvasConfigLike;
}
function unwrapComponent(c: GoViewComponentLike): GoViewComponentLike {
// Prefer the nested component shape but keep outer fields as fallback.
// This handles exports like: { id, attr, component: { chartConfig, option } }
const inner = c.component;
if (!inner) return c;
return {
...inner,
...c,
// ensure the nested component doesn't get lost
component: inner.component,
};
}
function keyOf(cIn: GoViewComponentLike): string {
const c = unwrapComponent(cIn);
return (
c.chartConfig?.key ??
c.chartConfig?.chartKey ??
c.chartConfig?.type ??
c.componentKey ??
c.key ??
''
).toLowerCase();
}
function isTextCommon(c: GoViewComponentLike): boolean {
const k = keyOf(c);
if (k === 'textcommon') return true;
return k.includes('text');
}
function isImage(c: GoViewComponentLike): boolean {
const k = keyOf(c);
// goView variants: "Image", "image", sometimes with suffixes.
return k === 'image' || k.includes('image') || k.includes('picture');
}
function isIframe(c: GoViewComponentLike): boolean {
const k = keyOf(c);
// goView variants: "Iframe", "IframeCommon", etc.
if (k === 'iframe' || k.includes('iframe')) return true;
// Other names seen in low-code editors for embedded web content.
return k.includes('embed') || k.includes('web') || k.includes('html');
}
function isVideo(c: GoViewComponentLike): boolean {
const k = keyOf(c);
// goView variants: "Video", "VideoCommon", etc.
if (k === 'video' || k.includes('video')) return true;
// Other names seen in the wild.
return k.includes('mp4') || k.includes('media') || k.includes('player');
}
function pick<T>(...values: Array<T | undefined | null>): T | undefined {
for (const v of values) {
if (v !== undefined && v !== null) return v as T;
}
return undefined;
}
function optionOf(cIn: GoViewComponentLike): unknown {
const c = unwrapComponent(cIn);
const chartData = c.chartConfig?.data as Record<string, unknown> | undefined;
return (
c.option ??
c.chartConfig?.option ??
chartData?.option ??
chartData?.options ??
chartData?.config ??
chartData ??
{}
);
}
function toNumber(v: unknown, fallback: number): number {
if (typeof v === 'number' && Number.isFinite(v)) return v;
if (typeof v === 'string') {
const n = Number(v);
if (Number.isFinite(n)) return n;
}
return fallback;
}
export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
// goView exports vary a lot; attempt a few common nesting shapes.
const root = input as unknown as Record<string, unknown>;
const data = (root.data as Record<string, unknown> | undefined) ?? undefined;
const state = (root.state as Record<string, unknown> | undefined) ?? undefined;
const project = (root.project as Record<string, unknown> | undefined) ?? undefined;
const editCanvasConfig = pick<GoViewEditCanvasConfigLike>(
(input as GoViewStorageLike).editCanvasConfig,
data?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
state?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
project?.editCanvasConfig as GoViewEditCanvasConfigLike | undefined,
);
const width =
editCanvasConfig?.width ??
(input as GoViewProjectLike).canvas?.width ??
(input as GoViewProjectLike).width ??
(data?.width as number | undefined) ??
1920;
const height =
editCanvasConfig?.height ??
(input as GoViewProjectLike).canvas?.height ??
(input as GoViewProjectLike).height ??
(data?.height as number | undefined) ??
1080;
const name = editCanvasConfig?.projectName ?? 'Imported Project';
const background = editCanvasConfig?.background;
const screen = createEmptyScreen({
version: ASTRALVIEW_SCHEMA_VERSION,
width,
height,
name,
background: background ? { color: background } : undefined,
nodes: [],
});
const componentList =
(input as GoViewStorageLike).componentList ??
(input as GoViewProjectLike).componentList ??
(data?.componentList as GoViewComponentLike[] | undefined) ??
(state?.componentList as GoViewComponentLike[] | undefined) ??
(project?.componentList as GoViewComponentLike[] | undefined) ??
[];
const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = [];
for (const raw of componentList) {
const c = unwrapComponent(raw);
const rect = c.attr
? {
x: toNumber((c.attr as unknown as Record<string, unknown>).x, 0),
y: toNumber((c.attr as unknown as Record<string, unknown>).y, 0),
w: toNumber((c.attr as unknown as Record<string, unknown>).w, 320),
h: toNumber((c.attr as unknown as Record<string, unknown>).h, 60),
}
: { x: 0, y: 0, w: 320, h: 60 };
if (isTextCommon(c)) {
const props = convertGoViewTextOptionToNodeProps(optionOf(c) as GoViewTextOption);
nodes.push({
id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`,
type: 'text',
rect,
zIndex: c.attr?.zIndex === undefined ? undefined : toNumber((c.attr as unknown as Record<string, unknown>).zIndex, 0),
locked: c.status?.lock ?? false,
hidden: c.status?.hide ?? false,
props,
});
continue;
}
if (isImage(c)) {
const props = convertGoViewImageOptionToNodeProps(optionOf(c) as GoViewImageOption);
nodes.push({
id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`,
type: 'image',
rect,
zIndex: c.attr?.zIndex === undefined ? undefined : toNumber((c.attr as unknown as Record<string, unknown>).zIndex, 0),
locked: c.status?.lock ?? false,
hidden: c.status?.hide ?? false,
props,
});
continue;
}
if (isIframe(c)) {
const props = convertGoViewIframeOptionToNodeProps(optionOf(c) as GoViewIframeOption);
nodes.push({
id: c.id ?? `import_iframe_${Math.random().toString(16).slice(2)}`,
type: 'iframe',
rect,
zIndex: c.attr?.zIndex === undefined ? undefined : toNumber((c.attr as unknown as Record<string, unknown>).zIndex, 0),
locked: c.status?.lock ?? false,
hidden: c.status?.hide ?? false,
props,
});
continue;
}
if (isVideo(c)) {
const props = convertGoViewVideoOptionToNodeProps(optionOf(c) as GoViewVideoOption);
nodes.push({
id: c.id ?? `import_video_${Math.random().toString(16).slice(2)}`,
type: 'video',
rect,
zIndex: c.attr?.zIndex === undefined ? undefined : toNumber((c.attr as unknown as Record<string, unknown>).zIndex, 0),
locked: c.status?.lock ?? false,
hidden: c.status?.hide ?? false,
props,
});
continue;
}
}
return {
...screen,
nodes,
};
}