refactor: improve context menu selection sync and goview embed detection

This commit is contained in:
clawdbot 2026-01-28 20:18:29 +08:00
parent b1ca80d5fe
commit 947946edc6
2 changed files with 29 additions and 10 deletions

View File

@ -1,6 +1,12 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Typography } from 'antd'; import { Typography } from 'antd';
function selectionKeyOf(ids: string[]): string {
// Keep stable regardless of selection order.
// This avoids false mismatches between context-menu state and reducer state.
return [...ids].sort().join('|');
}
export type ContextMenuState = export type ContextMenuState =
| { | {
kind: 'canvas'; kind: 'canvas';
@ -101,11 +107,18 @@ export function ContextMenu(props: {
if (!ctx || !position) return null; if (!ctx || !position) return null;
const hasSelection = props.selectionIds.length > 0; const currentSelectionKey = selectionKeyOf(props.selectionIds);
const canModifySelection = hasSelection && props.selectionHasUnlocked; const selectionInSync = currentSelectionKey === ctx.selectionKey;
// Use captured selection while waiting for store selection to catch up.
// This prevents transient "disabled" menu items right after right-click selecting.
const selectionIds = selectionInSync ? props.selectionIds : ctx.selectionIds;
const hasSelection = selectionIds.length > 0;
const canModifySelection = selectionInSync && hasSelection && props.selectionHasUnlocked;
const hasTarget = ctx.kind === 'node'; const hasTarget = ctx.kind === 'node';
const targetId = hasTarget ? ctx.targetId : undefined; const targetId = hasTarget ? ctx.targetId : undefined;
const targetInSelection = !!targetId && props.selectionIds.includes(targetId); const targetInSelection = !!targetId && selectionIds.includes(targetId);
const canSelectSingle = !!props.onSelectSingle; const canSelectSingle = !!props.onSelectSingle;
return ( return (
@ -136,7 +149,7 @@ export function ContextMenu(props: {
}} }}
/> />
{canSelectSingle && hasTarget && targetInSelection && props.selectionIds.length > 1 ? ( {canSelectSingle && hasTarget && targetInSelection && selectionIds.length > 1 ? (
<MenuItem <MenuItem
label="Select Only" label="Select Only"
onClick={() => { onClick={() => {
@ -185,7 +198,7 @@ export function ContextMenu(props: {
? 'Toggle Lock' ? 'Toggle Lock'
: 'Lock' : 'Lock'
} }
disabled={!hasSelection || !props.onToggleLockSelected} disabled={!selectionInSync || !hasSelection || !props.onToggleLockSelected}
onClick={() => { onClick={() => {
props.onToggleLockSelected?.(); props.onToggleLockSelected?.();
onClose(); onClose();
@ -199,7 +212,7 @@ export function ContextMenu(props: {
? 'Toggle Visibility' ? 'Toggle Visibility'
: 'Hide' : 'Hide'
} }
disabled={!hasSelection || !props.onToggleHideSelected} disabled={!selectionInSync || !hasSelection || !props.onToggleHideSelected}
onClick={() => { onClick={() => {
props.onToggleHideSelected?.(); props.onToggleHideSelected?.();
onClose(); onClose();
@ -210,7 +223,7 @@ export function ContextMenu(props: {
<MenuItem <MenuItem
label="Bring To Front" label="Bring To Front"
disabled={!hasSelection || !props.onBringToFrontSelected} disabled={!selectionInSync || !hasSelection || !props.onBringToFrontSelected}
onClick={() => { onClick={() => {
props.onBringToFrontSelected?.(); props.onBringToFrontSelected?.();
onClose(); onClose();
@ -218,7 +231,7 @@ export function ContextMenu(props: {
/> />
<MenuItem <MenuItem
label="Send To Back" label="Send To Back"
disabled={!hasSelection || !props.onSendToBackSelected} disabled={!selectionInSync || !hasSelection || !props.onSendToBackSelected}
onClick={() => { onClick={() => {
props.onSendToBackSelected?.(); props.onSendToBackSelected?.();
onClose(); onClose();
@ -235,7 +248,13 @@ export function ContextMenu(props: {
}} }}
/> />
{!selectionInSync ? (
<Typography.Text style={{ display: 'block', marginTop: 6, color: 'rgba(255,255,255,0.45)', fontSize: 11 }}> <Typography.Text style={{ display: 'block', marginTop: 6, color: 'rgba(255,255,255,0.45)', fontSize: 11 }}>
(updating selection)
</Typography.Text>
) : null}
<Typography.Text style={{ display: 'block', marginTop: 2, color: 'rgba(255,255,255,0.45)', fontSize: 11 }}>
({Math.round(ctx.worldX)}, {Math.round(ctx.worldY)}) ({Math.round(ctx.worldX)}, {Math.round(ctx.worldY)})
</Typography.Text> </Typography.Text>
</div> </div>

View File

@ -678,7 +678,7 @@ export function convertGoViewProjectToScreen(input: GoViewProjectLike | GoViewSt
const urlLike = pickUrlLike(option); const urlLike = pickUrlLike(option);
const urlLooksLikeEmbedPage = const urlLooksLikeEmbedPage =
!!urlLike && !!urlLike &&
/(youtube\.com|youtu\.be|vimeo\.com|twitch\.tv|tiktok\.com|douyin\.com|kuaishou\.com|ixigua\.com|huya\.com|douyu\.com|bilibili\.com|live\.bilibili\.com|player\.bilibili\.com|youku\.com|iqiyi\.com|mgtv\.com|tencent|qq\.com|music\.163\.com)/i.test( /(youtube\.com|youtu\.be|vimeo\.com|twitch\.tv|tiktok\.com|douyin\.com|kuaishou\.com|ixigua\.com|huya\.com|douyu\.com|bilibili\.com|live\.bilibili\.com|player\.bilibili\.com|youku\.com|iqiyi\.com|mgtv\.com|tencent|qq\.com|music\.163\.com|spotify\.com|soundcloud\.com|figma\.com|google\.com\/maps|amap\.com|gaode\.com|map\.baidu\.com)/i.test(
urlLike, urlLike,
) && ) &&
// Keep actual media URLs as video. // Keep actual media URLs as video.