refine goview import heuristics + context menu parity

This commit is contained in:
clawdbot 2026-01-28 09:33:35 +08:00
parent 06868fed42
commit 858097bfba
3 changed files with 80 additions and 32 deletions

View File

@ -13,7 +13,9 @@ export function ContextMenu(props: {
state: ContextMenuState | null; state: ContextMenuState | null;
selectionIds: string[]; selectionIds: string[];
selectionAllLocked: boolean; selectionAllLocked: boolean;
selectionSomeLocked?: boolean;
selectionAllHidden: boolean; selectionAllHidden: boolean;
selectionSomeHidden?: boolean;
hasAnyNodes?: boolean; hasAnyNodes?: boolean;
onClose: () => void; onClose: () => void;
onAddTextAt: (x: number, y: number) => void; onAddTextAt: (x: number, y: number) => void;
@ -160,7 +162,13 @@ export function ContextMenu(props: {
/> />
<MenuItem <MenuItem
label={props.selectionAllLocked ? 'Unlock' : 'Lock'} label={
props.selectionAllLocked
? 'Unlock'
: props.selectionSomeLocked
? 'Toggle Lock'
: 'Lock'
}
disabled={!hasSelection || !props.onToggleLockSelected} disabled={!hasSelection || !props.onToggleLockSelected}
onClick={() => { onClick={() => {
props.onToggleLockSelected?.(); props.onToggleLockSelected?.();
@ -168,7 +176,13 @@ export function ContextMenu(props: {
}} }}
/> />
<MenuItem <MenuItem
label={props.selectionAllHidden ? 'Show' : 'Hide'} label={
props.selectionAllHidden
? 'Show'
: props.selectionSomeHidden
? 'Toggle Visibility'
: 'Hide'
}
disabled={!hasSelection || !props.onToggleHideSelected} disabled={!hasSelection || !props.onToggleHideSelected}
onClick={() => { onClick={() => {
props.onToggleHideSelected?.(); props.onToggleHideSelected?.();

View File

@ -54,7 +54,10 @@ export function EditorApp() {
}, [state.doc.screen.nodes, state.selection.ids]); }, [state.doc.screen.nodes, state.selection.ids]);
const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked); const selectionAllLocked = selection.length > 0 && selection.every((n) => n.locked);
const selectionSomeLocked = selection.length > 0 && selection.some((n) => n.locked) && !selectionAllLocked;
const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden); const selectionAllHidden = selection.length > 0 && selection.every((n) => n.hidden);
const selectionSomeHidden = selection.length > 0 && selection.some((n) => n.hidden) && !selectionAllHidden;
const bounds = useMemo( const bounds = useMemo(
() => ({ w: state.doc.screen.width, h: state.doc.screen.height }), () => ({ w: state.doc.screen.width, h: state.doc.screen.height }),
@ -508,7 +511,9 @@ export function EditorApp() {
state={ctxMenu} state={ctxMenu}
selectionIds={state.selection.ids} selectionIds={state.selection.ids}
selectionAllLocked={selectionAllLocked} selectionAllLocked={selectionAllLocked}
selectionSomeLocked={selectionSomeLocked}
selectionAllHidden={selectionAllHidden} selectionAllHidden={selectionAllHidden}
selectionSomeHidden={selectionSomeHidden}
hasAnyNodes={state.doc.screen.nodes.length > 0} hasAnyNodes={state.doc.screen.nodes.length > 0}
onClose={closeContextMenu} onClose={closeContextMenu}
onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })} onAddTextAt={(x, y) => dispatch({ type: 'addTextAt', x, y })}

View File

@ -236,6 +236,23 @@ function looksLikeTextOption(option: unknown): boolean {
return false; return false;
} }
function hasAnyKeyDeep(input: unknown, keys: string[], depth = 2): boolean {
if (!input || typeof input !== 'object') return false;
const obj = input as Record<string, unknown>;
for (const k of keys) {
if (k in obj) return true;
}
if (depth <= 0) return false;
for (const nestKey of ['style', 'config', 'option', 'options', 'props', 'dataset', 'data']) {
if (hasAnyKeyDeep(obj[nestKey], keys, depth - 1)) return true;
}
return false;
}
function looksLikeIframeOption(option: unknown): boolean { function looksLikeIframeOption(option: unknown): boolean {
if (!option) return false; if (!option) return false;
@ -248,14 +265,15 @@ function looksLikeIframeOption(option: unknown): boolean {
if (trimmed.startsWith('<') && trimmed.includes('>')) return true; if (trimmed.startsWith('<') && trimmed.includes('>')) return true;
} }
// Prefer explicit iframe-ish keys when option is an object. // Prefer explicit iframe-ish keys when option is an object (including nested shapes).
if (typeof option === 'object') { if (typeof option === 'object') {
const o = option as Record<string, unknown>; if (
if ('iframeUrl' in o || 'iframeSrc' in o || 'embedUrl' in o || 'frameUrl' in o || 'frameSrc' in o) return true; 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)
if ('srcdoc' in o || 'srcDoc' in o) return true; ) {
if ('html' in o || 'htmlContent' in o || 'content' in o || 'template' in o) return true; return true;
}
} }
const url = pickUrlLike(option); const url = pickUrlLike(option);
@ -284,31 +302,42 @@ function looksLikeVideoOption(option: unknown): boolean {
// Avoid false positives: some TextCommon widgets carry link-like fields. // Avoid false positives: some TextCommon widgets carry link-like fields.
if (looksLikeTextOption(option)) return false; if (looksLikeTextOption(option)) return false;
// Prefer explicit video-ish keys when option is an object. // Prefer explicit video-ish keys when option is an object (including nested shapes).
if (typeof option === 'object') { if (typeof option === 'object') {
const o = option as Record<string, unknown>;
if ( if (
'videoUrl' in o || hasAnyKeyDeep(
'videoSrc' in o || option,
'playUrl' in o || [
'srcUrl' in o || 'videoUrl',
'sourceUrl' in o || 'videoSrc',
'liveUrl' in o || 'playUrl',
'streamUrl' in o || 'srcUrl',
'mp4' in o || 'sourceUrl',
'm3u8' in o || 'liveUrl',
'flv' in o || 'streamUrl',
'hls' in o || 'mp4',
'rtsp' in o || 'm3u8',
// list-ish shapes 'flv',
'sources' in o || 'hls',
'sourceList' in o || 'rtsp',
'urlList' in o || 'rtmp',
'autoplay' in o || // list-ish shapes
'autoPlay' in o || 'sources',
'isAutoPlay' in o || 'sourceList',
'poster' in o || 'urlList',
'posterUrl' in o 'srcList',
'playlist',
'playList',
// playback flags
'autoplay',
'autoPlay',
'isAutoPlay',
// common UI fields
'poster',
'posterUrl',
],
2,
)
) { ) {
return true; return true;
} }