refactor: improve goView import (video/iframe) + editor state

This commit is contained in:
clawdbot 2026-01-28 10:01:54 +08:00
parent 858097bfba
commit 01b7a3a722
2 changed files with 70 additions and 35 deletions

View File

@ -1,4 +1,5 @@
import {
assertNever,
convertGoViewProjectToScreen,
createEmptyScreen,
migrateScreen,
@ -78,13 +79,13 @@ interface BoxSelectSession {
baseIds: string[];
}
type InternalEditorState = EditorState & {
type EditorRuntimeState = EditorState & {
__pan?: PanSession;
__boxSelect?: BoxSelectSession;
__drag?: DragSession;
};
function historyPush(state: EditorState): EditorState {
function historyPush(state: EditorRuntimeState): EditorRuntimeState {
return {
...state,
history: {
@ -94,12 +95,12 @@ function historyPush(state: EditorState): EditorState {
};
}
function ensureSelected(state: EditorState, id: string): EditorState {
function ensureSelected(state: EditorRuntimeState, id: string): EditorRuntimeState {
if (state.selection.ids.includes(id)) return state;
return { ...state, selection: { ids: [id] } };
}
export function createInitialState(): EditorState {
export function createInitialState(): EditorRuntimeState {
const screen = createEmptyScreen({
width: 1920,
height: 1080,
@ -141,8 +142,8 @@ export function createInitialState(): EditorState {
};
}
export function editorReducer(state: EditorState, action: EditorAction): EditorState {
const s = state as InternalEditorState;
export function editorReducer(state: EditorRuntimeState, action: EditorAction): EditorRuntimeState {
const s = state;
switch (action.type) {
case 'keyboard':
return { ...state, keyboard: { ctrl: action.ctrl, space: action.space } };
@ -454,7 +455,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
startPanX: state.canvas.panX,
startPanY: state.canvas.panY,
},
} as EditorState;
};
}
case 'updatePan': {
@ -476,15 +477,14 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
case 'endPan': {
if (!state.canvas.isPanning) return state;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __pan: _pan, ...rest } = s;
return {
...rest,
...state,
canvas: {
...state.canvas,
isPanning: false,
},
} as EditorState;
__pan: undefined,
};
}
case 'selectSingle':
@ -525,7 +525,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
},
selection: { ids: baseIds },
__boxSelect: { additive: action.additive, baseIds },
} as EditorState;
};
}
case 'updateBoxSelect': {
@ -567,16 +567,15 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
case 'endBoxSelect': {
if (!state.canvas.isBoxSelecting) return state;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __boxSelect: _box, ...rest } = s;
return {
...rest,
...state,
canvas: {
...state.canvas,
isBoxSelecting: false,
mouse: { ...state.canvas.mouse, offsetX: 0, offsetY: 0, offsetStartX: 0, offsetStartY: 0 },
},
} as EditorState;
__boxSelect: undefined,
};
}
case 'beginMove': {
@ -614,7 +613,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
},
},
__drag: drag,
} as EditorState;
};
}
case 'updateMove': {
@ -669,17 +668,19 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
case 'endMove': {
const drag = s.__drag;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __drag: _drag, ...rest } = s;
if (!drag || drag.kind !== 'move') {
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
return {
...state,
canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } },
__drag: undefined,
};
}
const changed = didRectsChange(drag.snapshot, state.doc.screen);
return {
...rest,
...state,
canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } },
__drag: undefined,
history: changed
? {
past: [...state.history.past, { screen: drag.beforeScreen }],
@ -709,7 +710,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
...next,
canvas: { ...next.canvas, isDragging: true, guides: { xs: [], ys: [] } },
__drag: drag,
} as EditorState;
};
}
case 'updateResize': {
@ -774,17 +775,19 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
case 'endResize': {
const drag = s.__drag;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __drag: _drag, ...rest } = s;
if (!drag || drag.kind !== 'resize') {
return { ...rest, canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } } };
return {
...state,
canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } },
__drag: undefined,
};
}
const changed = didRectsChange(drag.snapshot, state.doc.screen);
return {
...rest,
...state,
canvas: { ...state.canvas, isDragging: false, guides: { xs: [], ys: [] } },
__drag: undefined,
history: changed
? {
past: [...state.history.past, { screen: drag.beforeScreen }],
@ -830,7 +833,7 @@ export function editorReducer(state: EditorState, action: EditorAction): EditorS
}
default:
return state;
return assertNever(action);
}
}

View File

@ -270,7 +270,22 @@ function looksLikeIframeOption(option: unknown): boolean {
if (
hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) ||
// Some exports store raw HTML instead of a URL.
hasAnyKeyDeep(option, ['srcdoc', 'srcDoc', 'html', 'htmlContent', 'content', 'template'], 2)
hasAnyKeyDeep(
option,
[
'srcdoc',
'srcDoc',
'html',
'htmlContent',
'htmlString',
'iframeHtml',
'embedHtml',
'embedCode',
'content',
'template',
],
2,
)
) {
return true;
}
@ -401,6 +416,12 @@ function normalizeOption(option: unknown): unknown {
return option;
}
function toMaybeString(v: unknown): string | undefined {
if (typeof v === 'string') return v;
if (typeof v === 'number' && Number.isFinite(v)) return String(v);
return undefined;
}
function toNumber(v: unknown, fallback: number): number {
if (typeof v === 'number' && Number.isFinite(v)) return v;
if (typeof v === 'string') {
@ -451,6 +472,15 @@ function pickSizeLike(option: unknown): { w?: number; h?: number } {
return { w, h };
}
function normalizeRect(
rect: { x: number; y: number; w: number; h: number },
fallback: { w: number; h: number },
): { x: number; y: number; w: number; h: number } {
const w = rect.w > 0 ? rect.w : fallback.w;
const h = rect.h > 0 ? rect.h : fallback.h;
return { x: rect.x, y: rect.y, w, h };
}
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>;
@ -566,7 +596,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
const optSize = pickSizeLike(option);
const attr = c.attr as unknown as Record<string, unknown> | undefined;
const rect = attr
const rawRect = attr
? {
x: toNumber(attr.x, 0),
y: toNumber(attr.y, 0),
@ -575,13 +605,15 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
h: toNumber(attr.h, optSize.h ?? defaults.h),
}
: { x: 0, y: 0, w: optSize.w ?? defaults.w, h: optSize.h ?? defaults.h };
const rect = normalizeRect(rawRect, defaults);
const zIndex = attr?.zIndex === undefined ? undefined : toNumber(attr.zIndex, 0);
const baseId = toMaybeString(c.id);
if (inferredType === 'text') {
const props = convertGoViewTextOptionToNodeProps(option as GoViewTextOption);
nodes.push({
id: c.id ?? `import_text_${Math.random().toString(16).slice(2)}`,
id: baseId ?? `import_text_${Math.random().toString(16).slice(2)}`,
type: 'text',
rect,
zIndex,
@ -595,7 +627,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'image') {
const props = convertGoViewImageOptionToNodeProps(option as GoViewImageOption);
nodes.push({
id: c.id ?? `import_image_${Math.random().toString(16).slice(2)}`,
id: baseId ?? `import_image_${Math.random().toString(16).slice(2)}`,
type: 'image',
rect,
zIndex,
@ -609,7 +641,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'iframe') {
const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption);
nodes.push({
id: c.id ?? `import_iframe_${Math.random().toString(16).slice(2)}`,
id: baseId ?? `import_iframe_${Math.random().toString(16).slice(2)}`,
type: 'iframe',
rect,
zIndex,
@ -623,7 +655,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'video') {
const props = convertGoViewVideoOptionToNodeProps(option as GoViewVideoOption);
nodes.push({
id: c.id ?? `import_video_${Math.random().toString(16).slice(2)}`,
id: baseId ?? `import_video_${Math.random().toString(16).slice(2)}`,
type: 'video',
rect,
zIndex,