feat: add iframe/video widgets and importer support
This commit is contained in:
parent
51c27bc931
commit
b24e8f2a41
6
.gitignore
vendored
6
.gitignore
vendored
@ -22,3 +22,9 @@ Thumbs.db
|
||||
# docs build
|
||||
packages/docs/docs/.vitepress/cache
|
||||
packages/docs/docs/.vitepress/dist
|
||||
|
||||
# TypeScript build info
|
||||
*.tsbuildinfo
|
||||
|
||||
# local traces
|
||||
trace.txt
|
||||
|
||||
@ -64,15 +64,15 @@ Represents one dashboard/screen.
|
||||
- Clipboard (copy/paste)
|
||||
- History (command pattern)
|
||||
|
||||
## Migration Plan (from go-view)
|
||||
## Migration Plan (from legacy)
|
||||
|
||||
We will *not* transplant Vue UI. We will extract **behavior + data contracts** into `sdk`, then rebuild UI in `editor`.
|
||||
|
||||
High-signal sources in go-view:
|
||||
High-signal sources in the legacy codebase:
|
||||
- `src/views/chart/ContentEdit/*` (drag, selection, guides, tools)
|
||||
- `src/store/modules/chartEditStore` (editor state)
|
||||
- `src/hooks/useChartDataFetch.hook.ts` (data fetch)
|
||||
- `src/views/preview/*` (preview scaling + rendering)
|
||||
- `src/packages/*` (widget catalog + configs)
|
||||
|
||||
Next step: write a detailed mapping table (go-view module → sdk/editor target) and implement the first vertical slice: **canvas + one widget + import/export**.
|
||||
Next step: write a detailed mapping table (legacy module → sdk/editor target) and implement the first vertical slice: **canvas + one widget + import/export**.
|
||||
|
||||
@ -3,4 +3,4 @@
|
||||
- [Architecture](./architecture)
|
||||
- [Migration Plan](./migration)
|
||||
|
||||
This site documents the AstralView monorepo and the ongoing refactor from go-view.
|
||||
This site documents the AstralView monorepo and the ongoing refactor from a legacy third-party editor codebase.
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# Migration Plan (go-view → AstralView)
|
||||
# Migration Plan (Legacy → AstralView)
|
||||
|
||||
## Strategy
|
||||
|
||||
- Keep go-view as reference under `third_party/go-view`.
|
||||
- Keep the legacy codebase as reference under `third_party/`.
|
||||
- Extract domain and behaviors into `@astralview/sdk`.
|
||||
- Rebuild UI in `@astralview/editor` (React + Ant Design).
|
||||
|
||||
|
||||
@ -512,6 +512,57 @@ function NodeView(props: {
|
||||
height: '100%',
|
||||
objectFit: node.props.fit ?? 'contain',
|
||||
display: 'block',
|
||||
// Editor parity: allow selecting/dragging the widget even when clicking the media.
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : node.type === 'iframe' ? (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
borderRadius: `${node.props.borderRadius ?? 0}px`,
|
||||
}}
|
||||
>
|
||||
<iframe
|
||||
src={node.props.src}
|
||||
width={rect.w}
|
||||
height={rect.h}
|
||||
style={{
|
||||
border: 0,
|
||||
display: 'block',
|
||||
// Editor parity: iframes steal pointer events; disable so selection/context menu works.
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
title={node.id}
|
||||
/>
|
||||
</div>
|
||||
) : node.type === 'video' ? (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
borderRadius: `${node.props.borderRadius ?? 0}px`,
|
||||
}}
|
||||
>
|
||||
<video
|
||||
src={node.props.src}
|
||||
width={rect.w}
|
||||
height={rect.h}
|
||||
autoPlay
|
||||
playsInline
|
||||
loop={node.props.loop ?? false}
|
||||
muted={node.props.muted ?? false}
|
||||
style={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: node.props.fit ?? 'contain',
|
||||
// Editor parity: allow selecting/dragging even when clicking the video surface.
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -80,6 +80,8 @@ export function EditorApp() {
|
||||
selected={state.doc.screen.nodes.find((n) => n.id === state.selection.ids[0])}
|
||||
onUpdateTextProps={(id, props) => dispatch({ type: 'updateTextProps', id, props })}
|
||||
onUpdateImageProps={(id, props) => dispatch({ type: 'updateImageProps', id, props })}
|
||||
onUpdateIframeProps={(id, props) => dispatch({ type: 'updateIframeProps', id, props })}
|
||||
onUpdateVideoProps={(id, props) => dispatch({ type: 'updateVideoProps', id, props })}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
@ -2,11 +2,15 @@ import { Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import type { WidgetNode, TextWidgetNode } from '@astralview/sdk';
|
||||
|
||||
type ImageWidgetNode = Extract<WidgetNode, { type: 'image' }>;
|
||||
type IframeWidgetNode = Extract<WidgetNode, { type: 'iframe' }>;
|
||||
type VideoWidgetNode = Extract<WidgetNode, { type: 'video' }>;
|
||||
|
||||
export function Inspector(props: {
|
||||
selected?: WidgetNode;
|
||||
onUpdateTextProps: (id: string, patch: Partial<TextWidgetNode['props']>) => void;
|
||||
onUpdateImageProps: (id: string, patch: Partial<ImageWidgetNode['props']>) => void;
|
||||
onUpdateIframeProps: (id: string, patch: Partial<IframeWidgetNode['props']>) => void;
|
||||
onUpdateVideoProps: (id: string, patch: Partial<VideoWidgetNode['props']>) => void;
|
||||
}) {
|
||||
const node = props.selected;
|
||||
|
||||
@ -54,8 +58,101 @@ export function Inspector(props: {
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'iframe') {
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
Iframe
|
||||
</Typography.Title>
|
||||
|
||||
<Typography.Text type="secondary">Source</Typography.Text>
|
||||
<Input
|
||||
value={node.props.src}
|
||||
onChange={(e) => props.onUpdateIframeProps(node.id, { src: e.target.value })}
|
||||
placeholder="https://..."
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<Typography.Text type="secondary">Border radius</Typography.Text>
|
||||
<InputNumber
|
||||
value={node.props.borderRadius ?? 0}
|
||||
min={0}
|
||||
onChange={(v) => props.onUpdateIframeProps(node.id, { borderRadius: typeof v === 'number' ? v : 0 })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'video') {
|
||||
return (
|
||||
<div>
|
||||
<Typography.Title level={5} style={{ marginTop: 0 }}>
|
||||
Video
|
||||
</Typography.Title>
|
||||
|
||||
<Typography.Text type="secondary">Source</Typography.Text>
|
||||
<Input
|
||||
value={node.props.src}
|
||||
onChange={(e) => props.onUpdateVideoProps(node.id, { src: e.target.value })}
|
||||
placeholder="https://..."
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
<Typography.Text type="secondary">Fit</Typography.Text>
|
||||
<Select
|
||||
value={node.props.fit ?? 'contain'}
|
||||
onChange={(v) => props.onUpdateVideoProps(node.id, { fit: v })}
|
||||
style={{ width: '100%', marginBottom: 12 }}
|
||||
options={[
|
||||
{ value: 'contain', label: 'contain' },
|
||||
{ value: 'cover', label: 'cover' },
|
||||
{ value: 'fill', label: 'fill' },
|
||||
{ value: 'none', label: 'none' },
|
||||
{ value: 'scale-down', label: 'scale-down' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Space style={{ width: '100%', marginBottom: 12 }} size={12}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography.Text type="secondary">Loop</Typography.Text>
|
||||
<Select
|
||||
value={String(node.props.loop ?? false)}
|
||||
onChange={(v) => props.onUpdateVideoProps(node.id, { loop: v === 'true' })}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'true', label: 'true' },
|
||||
{ value: 'false', label: 'false' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography.Text type="secondary">Muted</Typography.Text>
|
||||
<Select
|
||||
value={String(node.props.muted ?? false)}
|
||||
onChange={(v) => props.onUpdateVideoProps(node.id, { muted: v === 'true' })}
|
||||
style={{ width: '100%' }}
|
||||
options={[
|
||||
{ value: 'true', label: 'true' },
|
||||
{ value: 'false', label: 'false' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<Typography.Text type="secondary">Border radius</Typography.Text>
|
||||
<InputNumber
|
||||
value={node.props.borderRadius ?? 0}
|
||||
min={0}
|
||||
onChange={(v) => props.onUpdateVideoProps(node.id, { borderRadius: typeof v === 'number' ? v : 0 })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If more widget types are added, handle them above.
|
||||
// For now, we only support text/image.
|
||||
// For now, we only support text/image/iframe/video.
|
||||
if (node.type !== 'text') {
|
||||
return <Typography.Paragraph style={{ color: '#666' }}>Unsupported widget type.</Typography.Paragraph>;
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import {
|
||||
migrateScreen,
|
||||
type Rect,
|
||||
type ImageWidgetNode,
|
||||
type IframeWidgetNode,
|
||||
type VideoWidgetNode,
|
||||
type Screen,
|
||||
type TextWidgetNode,
|
||||
type WidgetNode,
|
||||
@ -41,7 +43,9 @@ export type EditorAction =
|
||||
| { type: 'nudgeSelected'; dx: number; dy: number }
|
||||
| { type: 'duplicateSelected' }
|
||||
| { type: 'updateTextProps'; id: string; props: Partial<TextWidgetNode['props']> }
|
||||
| { type: 'updateImageProps'; id: string; props: Partial<ImageWidgetNode['props']> };
|
||||
| { type: 'updateImageProps'; id: string; props: Partial<ImageWidgetNode['props']> }
|
||||
| { type: 'updateIframeProps'; id: string; props: Partial<IframeWidgetNode['props']> }
|
||||
| { type: 'updateVideoProps'; id: string; props: Partial<VideoWidgetNode['props']> };
|
||||
|
||||
interface DragSession {
|
||||
kind: 'move' | 'resize';
|
||||
@ -217,6 +221,40 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateIframeProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'iframe') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'iframe') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'updateVideoProps': {
|
||||
const node = state.doc.screen.nodes.find((n) => n.id === action.id);
|
||||
if (!node || node.type !== 'video') return state;
|
||||
return {
|
||||
...historyPush(state),
|
||||
doc: {
|
||||
screen: {
|
||||
...state.doc.screen,
|
||||
nodes: state.doc.screen.nodes.map((n) => {
|
||||
if (n.id !== action.id || n.type !== 'video') return n;
|
||||
return { ...n, props: { ...n.props, ...action.props } };
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case 'deleteSelected': {
|
||||
if (!state.selection.ids.length) return state;
|
||||
const ids = new Set(state.selection.ids);
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { ASTRALVIEW_SCHEMA_VERSION, createEmptyScreen, type Screen, type TextWidgetNode } from '../schema';
|
||||
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 { convertLegacyIframeOptionToNodeProps, type LegacyIframeOption } from '../widgets/iframe';
|
||||
import { convertLegacyVideoOptionToNodeProps, type LegacyVideoOption } from '../widgets/video';
|
||||
import { convertGoViewTextOptionToNodeProps, type GoViewTextOption } from '../widgets/text';
|
||||
|
||||
export interface GoViewComponentLike {
|
||||
id?: string;
|
||||
|
||||
// goView component identity
|
||||
// component identity
|
||||
key?: string; // e.g. "TextCommon" (sometimes)
|
||||
chartConfig?: { key?: string }; // goView standard location
|
||||
chartConfig?: { key?: string };
|
||||
|
||||
// geometry
|
||||
attr?: { x: number; y: number; w: number; h: number; zIndex?: number };
|
||||
@ -14,7 +25,7 @@ export interface GoViewComponentLike {
|
||||
// state
|
||||
status?: { lock?: boolean; hide?: boolean };
|
||||
|
||||
// goView uses "option" for widget-specific config
|
||||
// widget-specific config
|
||||
option?: unknown;
|
||||
}
|
||||
|
||||
@ -31,23 +42,43 @@ export interface GoViewStorageLike {
|
||||
}
|
||||
|
||||
export interface GoViewProjectLike {
|
||||
// very loose input shape; goView has different versions/branches.
|
||||
width?: number;
|
||||
height?: number;
|
||||
canvas?: { width?: number; height?: number };
|
||||
componentList?: GoViewComponentLike[];
|
||||
|
||||
// goView persisted store shape
|
||||
// persisted store shape (some variants)
|
||||
editCanvasConfig?: GoViewEditCanvasConfigLike;
|
||||
}
|
||||
|
||||
function keyOf(c: GoViewComponentLike): string {
|
||||
return (c.chartConfig?.key ?? c.key ?? '').toLowerCase();
|
||||
}
|
||||
|
||||
function isTextCommon(c: GoViewComponentLike): boolean {
|
||||
const k = (c.chartConfig?.key ?? c.key ?? '').toLowerCase();
|
||||
const k = keyOf(c);
|
||||
if (k === 'textcommon') return true;
|
||||
// fallback heuristic
|
||||
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.
|
||||
return k === 'iframe' || k.includes('iframe');
|
||||
}
|
||||
|
||||
function isVideo(c: GoViewComponentLike): boolean {
|
||||
const k = keyOf(c);
|
||||
// goView variants: "Video", "VideoCommon", etc.
|
||||
return k === 'video' || k.includes('video');
|
||||
}
|
||||
|
||||
export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
|
||||
const editCanvasConfig = (input as GoViewStorageLike).editCanvasConfig;
|
||||
|
||||
@ -56,7 +87,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
|
||||
const height =
|
||||
editCanvasConfig?.height ?? (input as GoViewProjectLike).canvas?.height ?? (input as GoViewProjectLike).height ?? 1080;
|
||||
|
||||
const name = editCanvasConfig?.projectName ?? 'Imported from goView';
|
||||
const name = editCanvasConfig?.projectName ?? 'Imported Project';
|
||||
const background = editCanvasConfig?.background;
|
||||
|
||||
const screen = createEmptyScreen({
|
||||
@ -70,23 +101,68 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
|
||||
|
||||
const componentList = (input as GoViewStorageLike).componentList ?? (input as GoViewProjectLike).componentList ?? [];
|
||||
|
||||
const nodes: TextWidgetNode[] = [];
|
||||
const nodes: Array<TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode> = [];
|
||||
|
||||
for (const c of componentList) {
|
||||
// Only first: TextCommon-like
|
||||
if (!isTextCommon(c)) continue;
|
||||
const rect = c.attr
|
||||
? { x: c.attr.x, y: c.attr.y, w: c.attr.w, h: c.attr.h }
|
||||
: { x: 0, y: 0, w: 320, h: 60 };
|
||||
|
||||
const rect = c.attr ? { x: c.attr.x, y: c.attr.y, w: c.attr.w, h: c.attr.h } : { x: 0, y: 0, w: 320, h: 60 };
|
||||
const props = convertGoViewTextOptionToNodeProps((c.option ?? {}) as GoViewTextOption);
|
||||
if (isTextCommon(c)) {
|
||||
const props = convertGoViewTextOptionToNodeProps((c.option ?? {}) as GoViewTextOption);
|
||||
nodes.push({
|
||||
id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`,
|
||||
type: 'text',
|
||||
rect,
|
||||
zIndex: c.attr?.zIndex,
|
||||
locked: c.status?.lock ?? false,
|
||||
hidden: c.status?.hide ?? false,
|
||||
props,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: c.id ?? `goview_text_${Math.random().toString(16).slice(2)}`,
|
||||
type: 'text',
|
||||
rect,
|
||||
zIndex: c.attr?.zIndex,
|
||||
locked: c.status?.lock ?? false,
|
||||
hidden: c.status?.hide ?? false,
|
||||
props,
|
||||
});
|
||||
if (isImage(c)) {
|
||||
const props = convertGoViewImageOptionToNodeProps((c.option ?? {}) as GoViewImageOption);
|
||||
nodes.push({
|
||||
id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`,
|
||||
type: 'image',
|
||||
rect,
|
||||
zIndex: c.attr?.zIndex,
|
||||
locked: c.status?.lock ?? false,
|
||||
hidden: c.status?.hide ?? false,
|
||||
props,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isIframe(c)) {
|
||||
const props = convertLegacyIframeOptionToNodeProps((c.option ?? {}) as LegacyIframeOption);
|
||||
nodes.push({
|
||||
id: c.id ?? `import_iframe_${Math.random().toString(16).slice(2)}`,
|
||||
type: 'iframe',
|
||||
rect,
|
||||
zIndex: c.attr?.zIndex,
|
||||
locked: c.status?.lock ?? false,
|
||||
hidden: c.status?.hide ?? false,
|
||||
props,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isVideo(c)) {
|
||||
const props = convertLegacyVideoOptionToNodeProps((c.option ?? {}) as LegacyVideoOption);
|
||||
nodes.push({
|
||||
id: c.id ?? `import_video_${Math.random().toString(16).slice(2)}`,
|
||||
type: 'video',
|
||||
rect,
|
||||
zIndex: c.attr?.zIndex,
|
||||
locked: c.status?.lock ?? false,
|
||||
hidden: c.status?.hide ?? false,
|
||||
props,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -34,7 +34,7 @@ export interface TextWidgetNode extends WidgetNodeBase {
|
||||
color?: string;
|
||||
fontWeight?: number | string;
|
||||
|
||||
// goView parity (TextCommon)
|
||||
// legacy parity (TextCommon)
|
||||
paddingX?: number;
|
||||
paddingY?: number;
|
||||
letterSpacing?: number;
|
||||
@ -51,7 +51,35 @@ export interface TextWidgetNode extends WidgetNodeBase {
|
||||
};
|
||||
}
|
||||
|
||||
export type WidgetNode = TextWidgetNode;
|
||||
export interface ImageWidgetNode extends WidgetNodeBase {
|
||||
type: 'image';
|
||||
props: {
|
||||
src: string;
|
||||
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
borderRadius?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IframeWidgetNode extends WidgetNodeBase {
|
||||
type: 'iframe';
|
||||
props: {
|
||||
src: string;
|
||||
borderRadius?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VideoWidgetNode extends WidgetNodeBase {
|
||||
type: 'video';
|
||||
props: {
|
||||
src: string;
|
||||
loop?: boolean;
|
||||
muted?: boolean;
|
||||
fit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
|
||||
borderRadius?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type WidgetNode = TextWidgetNode | ImageWidgetNode | IframeWidgetNode | VideoWidgetNode;
|
||||
|
||||
export interface Screen {
|
||||
version: SchemaVersion;
|
||||
|
||||
13
packages/sdk/src/core/widgets/iframe.ts
Normal file
13
packages/sdk/src/core/widgets/iframe.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { IframeWidgetNode } from '../schema';
|
||||
|
||||
export interface LegacyIframeOption {
|
||||
dataset: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export function convertLegacyIframeOptionToNodeProps(option: LegacyIframeOption): IframeWidgetNode['props'] {
|
||||
return {
|
||||
src: option.dataset ?? '',
|
||||
borderRadius: option.borderRadius,
|
||||
};
|
||||
}
|
||||
41
packages/sdk/src/core/widgets/image.ts
Normal file
41
packages/sdk/src/core/widgets/image.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import type { ImageWidgetNode } from '../schema';
|
||||
|
||||
/**
|
||||
* goView Image option shape varies across versions. We keep this intentionally
|
||||
* permissive and normalize the common fields.
|
||||
*/
|
||||
export interface GoViewImageOption {
|
||||
/**
|
||||
* Common in existing legacy widgets (same as iframe/video).
|
||||
*/
|
||||
dataset?: string;
|
||||
|
||||
/**
|
||||
* Other variants seen in the wild.
|
||||
*/
|
||||
src?: string;
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Styling.
|
||||
*/
|
||||
fit?: ImageWidgetNode['props']['fit'];
|
||||
objectFit?: ImageWidgetNode['props']['fit'];
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
function pickSrc(option: GoViewImageOption): string {
|
||||
return option.dataset ?? option.src ?? option.url ?? '';
|
||||
}
|
||||
|
||||
function pickFit(option: GoViewImageOption): ImageWidgetNode['props']['fit'] | undefined {
|
||||
return option.fit ?? option.objectFit;
|
||||
}
|
||||
|
||||
export function convertGoViewImageOptionToNodeProps(option: GoViewImageOption): ImageWidgetNode['props'] {
|
||||
return {
|
||||
src: pickSrc(option),
|
||||
fit: pickFit(option),
|
||||
borderRadius: option.borderRadius,
|
||||
};
|
||||
}
|
||||
19
packages/sdk/src/core/widgets/video.ts
Normal file
19
packages/sdk/src/core/widgets/video.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import type { VideoWidgetNode } from '../schema';
|
||||
|
||||
export interface LegacyVideoOption {
|
||||
dataset: string;
|
||||
loop?: boolean;
|
||||
muted?: boolean;
|
||||
fit?: VideoWidgetNode['props']['fit'];
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export function convertLegacyVideoOptionToNodeProps(option: LegacyVideoOption): VideoWidgetNode['props'] {
|
||||
return {
|
||||
src: option.dataset ?? '',
|
||||
loop: option.loop,
|
||||
muted: option.muted,
|
||||
fit: option.fit,
|
||||
borderRadius: option.borderRadius,
|
||||
};
|
||||
}
|
||||
@ -14,6 +14,9 @@ export type {
|
||||
Screen,
|
||||
WidgetNode,
|
||||
TextWidgetNode,
|
||||
ImageWidgetNode,
|
||||
IframeWidgetNode,
|
||||
VideoWidgetNode,
|
||||
} from './core/schema';
|
||||
|
||||
export { migrateScreen } from './core/migrate';
|
||||
@ -21,6 +24,15 @@ export { migrateScreen } from './core/migrate';
|
||||
export type { GoViewTextOption } from './core/widgets/text';
|
||||
export { convertGoViewTextOptionToNodeProps } from './core/widgets/text';
|
||||
|
||||
export type { GoViewImageOption } from './core/widgets/image';
|
||||
export { convertGoViewImageOptionToNodeProps } from './core/widgets/image';
|
||||
|
||||
export type { LegacyIframeOption } from './core/widgets/iframe';
|
||||
export { convertLegacyIframeOptionToNodeProps } from './core/widgets/iframe';
|
||||
|
||||
export type { LegacyVideoOption } from './core/widgets/video';
|
||||
export { convertLegacyVideoOptionToNodeProps } from './core/widgets/video';
|
||||
|
||||
export type { GoViewProjectLike, GoViewComponentLike } from './core/goview/convert';
|
||||
export { convertGoViewProjectToScreen } from './core/goview/convert';
|
||||
export { convertGoViewJSONToScreen } from './core/goview';
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user