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

View File

@ -270,7 +270,22 @@ function looksLikeIframeOption(option: unknown): boolean {
if ( if (
hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) || hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) ||
// Some exports store raw HTML instead of a URL. // 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; return true;
} }
@ -401,6 +416,12 @@ function normalizeOption(option: unknown): unknown {
return option; 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 { function toNumber(v: unknown, fallback: number): number {
if (typeof v === 'number' && Number.isFinite(v)) return v; if (typeof v === 'number' && Number.isFinite(v)) return v;
if (typeof v === 'string') { if (typeof v === 'string') {
@ -451,6 +472,15 @@ function pickSizeLike(option: unknown): { w?: number; h?: number } {
return { w, h }; 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 { export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewStorageLike): Screen {
// goView exports vary a lot; attempt a few common nesting shapes. // goView exports vary a lot; attempt a few common nesting shapes.
const root = input as unknown as Record<string, unknown>; const root = input as unknown as Record<string, unknown>;
@ -566,7 +596,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
const optSize = pickSizeLike(option); const optSize = pickSizeLike(option);
const attr = c.attr as unknown as Record<string, unknown> | undefined; const attr = c.attr as unknown as Record<string, unknown> | undefined;
const rect = attr const rawRect = attr
? { ? {
x: toNumber(attr.x, 0), x: toNumber(attr.x, 0),
y: toNumber(attr.y, 0), y: toNumber(attr.y, 0),
@ -575,13 +605,15 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
h: toNumber(attr.h, optSize.h ?? defaults.h), h: toNumber(attr.h, optSize.h ?? defaults.h),
} }
: { x: 0, y: 0, w: optSize.w ?? defaults.w, 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 zIndex = attr?.zIndex === undefined ? undefined : toNumber(attr.zIndex, 0);
const baseId = toMaybeString(c.id);
if (inferredType === 'text') { if (inferredType === 'text') {
const props = convertGoViewTextOptionToNodeProps(option as GoViewTextOption); const props = convertGoViewTextOptionToNodeProps(option as GoViewTextOption);
nodes.push({ 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', type: 'text',
rect, rect,
zIndex, zIndex,
@ -595,7 +627,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'image') { if (inferredType === 'image') {
const props = convertGoViewImageOptionToNodeProps(option as GoViewImageOption); const props = convertGoViewImageOptionToNodeProps(option as GoViewImageOption);
nodes.push({ 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', type: 'image',
rect, rect,
zIndex, zIndex,
@ -609,7 +641,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'iframe') { if (inferredType === 'iframe') {
const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption); const props = convertGoViewIframeOptionToNodeProps(option as GoViewIframeOption);
nodes.push({ 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', type: 'iframe',
rect, rect,
zIndex, zIndex,
@ -623,7 +655,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
if (inferredType === 'video') { if (inferredType === 'video') {
const props = convertGoViewVideoOptionToNodeProps(option as GoViewVideoOption); const props = convertGoViewVideoOptionToNodeProps(option as GoViewVideoOption);
nodes.push({ 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', type: 'video',
rect, rect,
zIndex, zIndex,