refine goview import heuristics + context menu parity
This commit is contained in:
parent
06868fed42
commit
858097bfba
@ -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?.();
|
||||||
|
|||||||
@ -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 })}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user