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

View File

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

View File

@ -236,6 +236,23 @@ function looksLikeTextOption(option: unknown): boolean {
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 {
if (!option) return false;
@ -248,14 +265,15 @@ function looksLikeIframeOption(option: unknown): boolean {
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') {
const o = option as Record<string, unknown>;
if ('iframeUrl' in o || 'iframeSrc' in o || 'embedUrl' in o || 'frameUrl' in o || 'frameSrc' in o) return true;
if (
hasAnyKeyDeep(option, ['iframeUrl', 'iframeSrc', 'embedUrl', 'frameUrl', 'frameSrc'], 2) ||
// Some exports store raw HTML instead of a URL.
if ('srcdoc' in o || 'srcDoc' in o) return true;
if ('html' in o || 'htmlContent' in o || 'content' in o || 'template' in o) return true;
hasAnyKeyDeep(option, ['srcdoc', 'srcDoc', 'html', 'htmlContent', 'content', 'template'], 2)
) {
return true;
}
}
const url = pickUrlLike(option);
@ -284,31 +302,42 @@ function looksLikeVideoOption(option: unknown): boolean {
// Avoid false positives: some TextCommon widgets carry link-like fields.
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') {
const o = option as Record<string, unknown>;
if (
'videoUrl' in o ||
'videoSrc' in o ||
'playUrl' in o ||
'srcUrl' in o ||
'sourceUrl' in o ||
'liveUrl' in o ||
'streamUrl' in o ||
'mp4' in o ||
'm3u8' in o ||
'flv' in o ||
'hls' in o ||
'rtsp' in o ||
hasAnyKeyDeep(
option,
[
'videoUrl',
'videoSrc',
'playUrl',
'srcUrl',
'sourceUrl',
'liveUrl',
'streamUrl',
'mp4',
'm3u8',
'flv',
'hls',
'rtsp',
'rtmp',
// list-ish shapes
'sources' in o ||
'sourceList' in o ||
'urlList' in o ||
'autoplay' in o ||
'autoPlay' in o ||
'isAutoPlay' in o ||
'poster' in o ||
'posterUrl' in o
'sources',
'sourceList',
'urlList',
'srcList',
'playlist',
'playList',
// playback flags
'autoplay',
'autoPlay',
'isAutoPlay',
// common UI fields
'poster',
'posterUrl',
],
2,
)
) {
return true;
}