9bb4942ae7
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/95 Co-authored-by: 王德宇 <kvtodev@outlook.com> Co-committed-by: 王德宇 <kvtodev@outlook.com>
234 lines
6.5 KiB
TypeScript
234 lines
6.5 KiB
TypeScript
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import {
|
|
contextAssetMediaSrc,
|
|
type EditorAgentContextAsset,
|
|
EditorAgentRightClickAction,
|
|
type RightClickMenuTarget,
|
|
} from './common.ts';
|
|
|
|
type MessageBubbleRightClickMenuProps = {
|
|
x: number;
|
|
y: number;
|
|
target: RightClickMenuTarget;
|
|
pendingAction: EditorAgentRightClickAction | null;
|
|
resultAction: EditorAgentRightClickAction | null;
|
|
result: 'success' | 'error' | null;
|
|
onAction: (action: EditorAgentRightClickAction) => void;
|
|
onClose: () => void;
|
|
};
|
|
|
|
const VIEWPORT_MARGIN = 8;
|
|
|
|
function actionLabel({
|
|
action,
|
|
idleLabel,
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
}: {
|
|
action: EditorAgentRightClickAction;
|
|
idleLabel: string;
|
|
pendingAction: EditorAgentRightClickAction | null;
|
|
resultAction: EditorAgentRightClickAction | null;
|
|
result: 'success' | 'error' | null;
|
|
}) {
|
|
if (pendingAction === action) {
|
|
return action === EditorAgentRightClickAction.DownloadAsset
|
|
? '下载中'
|
|
: action === EditorAgentRightClickAction.ReferenceImage
|
|
? '引用中'
|
|
: '复制中';
|
|
}
|
|
if (resultAction !== action) {
|
|
return idleLabel;
|
|
}
|
|
if (result === 'success') {
|
|
return action === EditorAgentRightClickAction.DownloadAsset
|
|
? '已下载'
|
|
: action === EditorAgentRightClickAction.ReferenceImage
|
|
? '已引用'
|
|
: '已复制';
|
|
}
|
|
if (result === 'error') {
|
|
return action === EditorAgentRightClickAction.DownloadAsset
|
|
? '下载失败'
|
|
: action === EditorAgentRightClickAction.ReferenceImage
|
|
? '引用失败'
|
|
: '复制失败';
|
|
}
|
|
return idleLabel;
|
|
}
|
|
|
|
function assetDownloadLabel(asset: EditorAgentContextAsset) {
|
|
return `下载${
|
|
asset.mediaType === 'image'
|
|
? '图片'
|
|
: asset.mediaType === 'video'
|
|
? '视频'
|
|
: '音频'
|
|
}`;
|
|
}
|
|
|
|
export function MessageBubbleRightClickMenu({
|
|
x,
|
|
y,
|
|
target,
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
onAction,
|
|
onClose,
|
|
}: MessageBubbleRightClickMenuProps) {
|
|
const menuRef = useRef<HTMLDivElement | null>(null);
|
|
const [position, setPosition] = useState<{ x: number; y: number } | null>(
|
|
null,
|
|
);
|
|
|
|
useLayoutEffect(() => {
|
|
const menu = menuRef.current;
|
|
if (!menu || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
const rect = menu.getBoundingClientRect();
|
|
setPosition({
|
|
x: Math.min(
|
|
Math.max(x, VIEWPORT_MARGIN),
|
|
Math.max(
|
|
VIEWPORT_MARGIN,
|
|
window.innerWidth - rect.width - VIEWPORT_MARGIN,
|
|
),
|
|
),
|
|
y: Math.min(
|
|
Math.max(y, VIEWPORT_MARGIN),
|
|
Math.max(
|
|
VIEWPORT_MARGIN,
|
|
window.innerHeight - rect.height - VIEWPORT_MARGIN,
|
|
),
|
|
),
|
|
});
|
|
}, [target.kind, x, y]);
|
|
|
|
useEffect(() => {
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
if (!menuRef.current?.contains(event.target as Node)) {
|
|
onClose();
|
|
}
|
|
};
|
|
const handleContextMenu = (event: MouseEvent) => {
|
|
if (menuRef.current?.contains(event.target as Node)) {
|
|
event.preventDefault();
|
|
}
|
|
onClose();
|
|
};
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('pointerdown', handlePointerDown);
|
|
window.addEventListener('contextmenu', handleContextMenu, true);
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
window.addEventListener('scroll', onClose, true);
|
|
window.addEventListener('resize', onClose);
|
|
return () => {
|
|
window.removeEventListener('pointerdown', handlePointerDown);
|
|
window.removeEventListener('contextmenu', handleContextMenu, true);
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
window.removeEventListener('scroll', onClose, true);
|
|
window.removeEventListener('resize', onClose);
|
|
};
|
|
}, [onClose]);
|
|
|
|
if (typeof document === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
return createPortal(
|
|
<div
|
|
ref={menuRef}
|
|
className="image-canvas-editor__context-menu"
|
|
role="menu"
|
|
aria-label={target.kind === 'text' ? '消息右键菜单' : '消息素材右键菜单'}
|
|
style={{
|
|
left: position?.x ?? x,
|
|
top: position?.y ?? y,
|
|
zIndex: 60,
|
|
}}
|
|
onContextMenu={(event) => event.preventDefault()}
|
|
>
|
|
{target.kind === 'text' ? (
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() => onAction(EditorAgentRightClickAction.CopyText)}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.CopyText,
|
|
idleLabel: '复制文本',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
) : (
|
|
<>
|
|
{target.asset.mediaType === 'image' ? (
|
|
<>
|
|
{contextAssetMediaSrc(target.asset).trim() ? (
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() =>
|
|
onAction(EditorAgentRightClickAction.ReferenceImage)
|
|
}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.ReferenceImage,
|
|
idleLabel: '引用',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() => onAction(EditorAgentRightClickAction.CopyImage)}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.CopyImage,
|
|
idleLabel: '复制图片',
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
</>
|
|
) : null}
|
|
<button
|
|
type="button"
|
|
role="menuitem"
|
|
disabled={pendingAction !== null}
|
|
onClick={() => onAction(EditorAgentRightClickAction.DownloadAsset)}
|
|
>
|
|
{actionLabel({
|
|
action: EditorAgentRightClickAction.DownloadAsset,
|
|
idleLabel: assetDownloadLabel(target.asset),
|
|
pendingAction,
|
|
resultAction,
|
|
result,
|
|
})}
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|