feat: configure selected image toolbar density
This commit is contained in:
parent
2090dbd582
commit
2b82d57f73
@ -192,6 +192,8 @@ import {
|
||||
type CanvasToolbarConfig,
|
||||
type CanvasToolbarItemKey,
|
||||
DEFAULT_CANVAS_TOOLBAR_ITEMS,
|
||||
MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
defaultCanvasToolbarConfig,
|
||||
getDefaultCanvasToolbarItem,
|
||||
normalizeCanvasToolbarConfig,
|
||||
@ -3691,6 +3693,7 @@ export function AdminSettingsClient() {
|
||||
return {
|
||||
...prev,
|
||||
canvasToolbarConfig: {
|
||||
...normalized,
|
||||
items: {
|
||||
...normalized.items,
|
||||
[key]: {
|
||||
@ -12468,6 +12471,54 @@ export function AdminSettingsClient() {
|
||||
恢复全部默认
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-background/60 p-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<Label
|
||||
className="font-medium text-sm"
|
||||
htmlFor="selectedImageInlineActionCount"
|
||||
>
|
||||
选中图片一级按钮数量
|
||||
</Label>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
控制画布中选中图片后,快捷弹窗主条直接显示几个功能按钮;超出的功能自动收进“更多”菜单。
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
className="w-28"
|
||||
id="selectedImageInlineActionCount"
|
||||
max={MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT}
|
||||
min={MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT}
|
||||
onChange={(event) => {
|
||||
const normalized =
|
||||
normalizeCanvasToolbarConfig(
|
||||
siteSettings.canvasToolbarConfig
|
||||
);
|
||||
const parsed = Number(event.target.value);
|
||||
const nextValue = Number.isFinite(parsed)
|
||||
? Math.min(
|
||||
MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
Math.max(
|
||||
MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
Math.round(parsed)
|
||||
)
|
||||
)
|
||||
: normalized.selectedImageInlineActionCount;
|
||||
setSiteSettings({
|
||||
...siteSettings,
|
||||
canvasToolbarConfig: {
|
||||
...normalized,
|
||||
selectedImageInlineActionCount: nextValue,
|
||||
},
|
||||
});
|
||||
}}
|
||||
type="number"
|
||||
value={
|
||||
normalizeCanvasToolbarConfig(
|
||||
siteSettings.canvasToolbarConfig
|
||||
).selectedImageInlineActionCount
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{Array.from(
|
||||
new Set(
|
||||
DEFAULT_CANVAS_TOOLBAR_ITEMS.map(
|
||||
|
||||
@ -1894,11 +1894,13 @@ const updateSchema = z.object({
|
||||
.optional(),
|
||||
canvasToolbarConfig: z
|
||||
.object({
|
||||
selectedImageInlineActionCount: z.number().int().min(3).max(12).optional(),
|
||||
items: z
|
||||
.record(
|
||||
z.object({
|
||||
label: z.string().trim().max(40).optional(),
|
||||
iconUrl: z.string().trim().max(2000).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
|
||||
@ -170,6 +170,7 @@ import {
|
||||
getScaledNodeContentStyle,
|
||||
} from "@/lib/canvas/node-content-scale";
|
||||
import {
|
||||
getSelectedImageInlineActionCount,
|
||||
getCanvasToolbarItem,
|
||||
isCanvasToolbarItemEnabled,
|
||||
type CanvasToolbarConfig,
|
||||
@ -1180,6 +1181,47 @@ export function ImageNode({
|
||||
);
|
||||
const imageLayerSplitEnabled =
|
||||
isLayerSplitEnabled && imageLayerSplitEnabledByToolbar;
|
||||
const selectedImageInlineActionCount =
|
||||
getSelectedImageInlineActionCount(canvasToolbarConfig);
|
||||
const selectedImageToolbarActionCandidates = [
|
||||
{ key: "pickObject", enabled: imagePickObjectEnabled },
|
||||
{ key: "enhance", enabled: imageEnhanceEnabled },
|
||||
{ key: "removeBackground", enabled: imageRemoveBackgroundEnabled },
|
||||
{ key: "aiWashQuick", enabled: imageAiWashQuickEnabled },
|
||||
{ key: "textEdit", enabled: imageTextEditEnabled },
|
||||
{ key: "boxCutout", enabled: imageBoxCutoutEnabled },
|
||||
{ key: "localEdit", enabled: imageLocalEditEnabled },
|
||||
{ key: "removeWatermark", enabled: imageRemoveWatermarkEnabled },
|
||||
{ key: "resize", enabled: imageResizeEnabled },
|
||||
{ key: "multiAngle", enabled: imageMultiAngleEnabled },
|
||||
{ key: "generate3dModel", enabled: imageGenerate3dModelEnabled },
|
||||
{ key: "generate3dWorld", enabled: imageGenerate3dWorldEnabled },
|
||||
{ key: "layerSplit", enabled: imageLayerSplitEnabled },
|
||||
{ key: "extend", enabled: imageExtendEnabled },
|
||||
{ key: "crop", enabled: imageCropEnabled },
|
||||
{ key: "adjust", enabled: imageAdjustEnabled },
|
||||
{ key: "flipRotate", enabled: imageFlipRotateEnabled },
|
||||
{ key: "promptRegenerate", enabled: imagePromptRegenerateEnabled },
|
||||
{ key: "note", enabled: imageNoteEnabled },
|
||||
] as const;
|
||||
type SelectedImageToolbarActionKey =
|
||||
(typeof selectedImageToolbarActionCandidates)[number]["key"];
|
||||
const enabledSelectedImageToolbarActions =
|
||||
selectedImageToolbarActionCandidates.filter((action) => action.enabled);
|
||||
const selectedImageInlineActionKeys = new Set<SelectedImageToolbarActionKey>(
|
||||
enabledSelectedImageToolbarActions
|
||||
.slice(0, selectedImageInlineActionCount)
|
||||
.map((action) => action.key)
|
||||
);
|
||||
const selectedImageMoreActionKeys = new Set<SelectedImageToolbarActionKey>(
|
||||
enabledSelectedImageToolbarActions
|
||||
.slice(selectedImageInlineActionCount)
|
||||
.map((action) => action.key)
|
||||
);
|
||||
const isSelectedImageActionInline = (key: SelectedImageToolbarActionKey) =>
|
||||
selectedImageInlineActionKeys.has(key);
|
||||
const isSelectedImageActionInMore = (key: SelectedImageToolbarActionKey) =>
|
||||
selectedImageMoreActionKeys.has(key);
|
||||
const hasHoverImageMorePrimaryActions =
|
||||
imageRemoveBackgroundEnabled ||
|
||||
imageRemoveWatermarkEnabled ||
|
||||
@ -1201,29 +1243,12 @@ export function ImageNode({
|
||||
imageNodeMoreEnabled &&
|
||||
(hasHoverImageMorePrimaryActions || hasHoverImageMoreSecondaryActions);
|
||||
const hasSelectedImageMorePrimaryActions =
|
||||
imageBoxCutoutEnabled ||
|
||||
imageLocalEditEnabled ||
|
||||
imageRemoveWatermarkEnabled ||
|
||||
imageResizeEnabled ||
|
||||
imageMultiAngleEnabled ||
|
||||
imageGenerate3dModelEnabled ||
|
||||
imageGenerate3dWorldEnabled ||
|
||||
imageLayerSplitEnabled ||
|
||||
imageExtendEnabled ||
|
||||
imageCropEnabled ||
|
||||
imageAdjustEnabled ||
|
||||
imageFlipRotateEnabled ||
|
||||
imagePromptRegenerateEnabled ||
|
||||
imageNoteEnabled;
|
||||
selectedImageMoreActionKeys.size > 0;
|
||||
const hasSelectedImageMoreMenu =
|
||||
hasSelectedImageMorePrimaryActions ||
|
||||
(imageNodeMoreEnabled && imageDeleteEnabled);
|
||||
const hasSelectedImagePrimaryToolbarActions =
|
||||
imagePickObjectEnabled ||
|
||||
imageEnhanceEnabled ||
|
||||
imageRemoveBackgroundEnabled ||
|
||||
imageTextEditEnabled ||
|
||||
imageAiWashQuickEnabled;
|
||||
selectedImageInlineActionKeys.size > 0;
|
||||
const hasSelectedImageSecondaryToolbarActions =
|
||||
hasSelectedImageMoreMenu || imageDownloadEnabled;
|
||||
const contentScale = useMemo(
|
||||
@ -8648,7 +8673,7 @@ export function ImageNode({
|
||||
typeof document !== "undefined" &&
|
||||
createPortal(
|
||||
<div
|
||||
className="fixed z-[10000] rounded-[20px] border border-slate-200 bg-white/96 p-2.5 text-slate-950 shadow-[0_14px_34px_rgba(15,23,42,0.15)] backdrop-blur-xl dark:border-white/12 dark:bg-slate-950/94 dark:text-white"
|
||||
className="fixed z-[10000] overflow-hidden rounded-[14px] border border-slate-200 bg-white text-slate-950 shadow-[0_18px_44px_rgba(15,23,42,0.18)] dark:border-white/12 dark:bg-slate-950 dark:text-white"
|
||||
data-canvas-no-drag="true"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
@ -8659,26 +8684,24 @@ export function ImageNode({
|
||||
width: aiWashQuickPanelPosition.width,
|
||||
maxHeight: aiWashQuickPanelPosition.maxHeight,
|
||||
overflowY: "auto",
|
||||
scrollbarWidth: "thin",
|
||||
scrollbarWidth: "none",
|
||||
}}
|
||||
>
|
||||
<div className="mb-2.5 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[13px] border border-slate-200 bg-white shadow-[0_6px_14px_rgba(15,23,42,0.06)] dark:border-white/12 dark:bg-white/8">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-slate-100 px-4 py-3 dark:border-white/10">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-slate-950 dark:text-white">
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
className="h-5 w-5"
|
||||
fallback={Sparkles}
|
||||
iconUrl={aiWashQuickConfig.modeIconUrl}
|
||||
/>
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate font-semibold text-[14px] tracking-[-0.02em]">
|
||||
{aiWashQuickModeLabel}
|
||||
</div>
|
||||
<div className="min-w-0 truncate text-[17px] font-semibold tracking-[-0.02em]">
|
||||
{aiWashQuickModeLabel}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition hover:border-slate-950 hover:text-slate-950 dark:border-white/12 dark:bg-white/8 dark:text-white/60 dark:hover:border-white dark:hover:text-white"
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-950 dark:text-white/54 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
onClick={() => setAiWashQuickPanelOpen(false)}
|
||||
title="关闭"
|
||||
type="button"
|
||||
@ -8688,20 +8711,20 @@ export function ImageNode({
|
||||
</div>
|
||||
|
||||
{aiWashConfigLoading ? (
|
||||
<div className="mb-2 flex items-center gap-2 rounded-[14px] border border-slate-200 bg-slate-50 px-2.5 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/58">
|
||||
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3 text-[13px] text-slate-500 dark:border-white/10 dark:text-white/58">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
正在加载 AI洗图 配置...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="rounded-[16px] border border-slate-200 bg-slate-50/70 p-2.5 dark:border-white/10 dark:bg-white/[0.04]">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5 text-[12px] font-semibold text-slate-600 dark:text-white/62">
|
||||
<SlidersHorizontal className="h-3.5 w-3.5" />
|
||||
<div className="divide-y divide-slate-100 dark:divide-white/10">
|
||||
<div className="px-4 py-3">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-[14px] font-semibold text-slate-950 dark:text-white">
|
||||
<SlidersHorizontal className="h-4 w-4" />
|
||||
目标
|
||||
</div>
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
|
||||
{getAiWashStatusLabel(aiWashQuickStatus)}
|
||||
</span>
|
||||
</div>
|
||||
@ -8712,7 +8735,7 @@ export function ImageNode({
|
||||
}}
|
||||
value={selectedAiWashQuickPreset?.id || aiWashQuickPresetId}
|
||||
>
|
||||
<SelectTrigger className="h-9 rounded-[13px] border-slate-200 bg-white px-2.5 text-[13px] shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:border-white/10 dark:bg-white/8">
|
||||
<SelectTrigger className="h-10 rounded-[10px] border-slate-200 bg-white px-3 text-[14px] shadow-none dark:border-white/10 dark:bg-white/8">
|
||||
<SelectValue placeholder="选择渲染目标" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
|
||||
@ -8724,26 +8747,26 @@ export function ImageNode({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedAiWashQuickPreset?.description ? (
|
||||
<div className="mt-2 line-clamp-2 text-[11px] leading-4 text-slate-500 dark:text-white/46">
|
||||
<div className="mt-2 line-clamp-2 text-[12px] leading-5 text-slate-500 dark:text-white/46">
|
||||
{selectedAiWashQuickPreset.description}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="rounded-[16px] border border-slate-200 bg-white p-2.5 dark:border-white/10 dark:bg-white/[0.04]">
|
||||
<div className="mb-2 text-[12px] font-semibold text-slate-600 dark:text-white/62">
|
||||
<div className="px-4 py-3">
|
||||
<div className="mb-2 text-[14px] font-semibold text-slate-950 dark:text-white">
|
||||
参数
|
||||
</div>
|
||||
{shouldShowAiWashQuickModelSelector ? (
|
||||
<div className="mb-2.5 space-y-1.5">
|
||||
<Label className="text-[12px] text-slate-500 dark:text-white/54">
|
||||
<div className="mb-3 space-y-1.5">
|
||||
<Label className="text-[12px] font-medium text-slate-500 dark:text-white/54">
|
||||
生图模型
|
||||
</Label>
|
||||
<Select
|
||||
onValueChange={setAiWashQuickModelId}
|
||||
value={selectedAiWashQuickModel?.id || aiWashQuickModelId}
|
||||
>
|
||||
<SelectTrigger className="h-9 rounded-[13px] border-slate-200 bg-white px-2.5 text-[13px] shadow-[0_1px_2px_rgba(15,23,42,0.04)] dark:border-white/10 dark:bg-white/8">
|
||||
<SelectTrigger className="h-10 rounded-[10px] border-slate-200 bg-white px-3 text-[14px] shadow-none dark:border-white/10 dark:bg-white/8">
|
||||
<SelectValue placeholder="选择生图模型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
|
||||
@ -8780,7 +8803,7 @@ export function ImageNode({
|
||||
</div>
|
||||
</div>
|
||||
) : selectedAiWashQuickModel ? (
|
||||
<div className="mb-2.5 rounded-[13px] border border-slate-200 bg-slate-50 px-2.5 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
|
||||
<div className="mb-3 rounded-[10px] border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
|
||||
{selectedAiWashQuickModel.displayName}
|
||||
{selectedAiWashQuickModel.unitCost > 0
|
||||
? ` · ${selectedAiWashQuickModel.unitCost} 积分/张`
|
||||
@ -8788,7 +8811,7 @@ export function ImageNode({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="-mx-4 divide-y divide-slate-100 dark:divide-white/10">
|
||||
{aiWashQuickControls.map((control) => {
|
||||
const selectedIndex =
|
||||
aiWashQuickLevels[control.id] ?? control.defaultLevel;
|
||||
@ -8798,14 +8821,14 @@ export function ImageNode({
|
||||
null;
|
||||
return (
|
||||
<div
|
||||
className="rounded-[13px] border border-slate-200 bg-slate-50/70 px-2.5 py-2 dark:border-white/10 dark:bg-white/[0.04]"
|
||||
className="px-4 py-3"
|
||||
key={control.id}
|
||||
>
|
||||
<div className="mb-1.5 flex items-center justify-between gap-2">
|
||||
<div className="text-[12px] font-semibold">
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<div className="text-[14px] font-semibold">
|
||||
{control.label}
|
||||
</div>
|
||||
<span className="min-w-8 rounded-full border border-slate-200 bg-white px-1.5 py-0.5 text-center text-[10px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
|
||||
<span className="min-w-8 rounded-full border border-slate-200 bg-white px-2 py-0.5 text-center text-[12px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
|
||||
{activeLevel?.label || selectedIndex + 1}
|
||||
</span>
|
||||
</div>
|
||||
@ -8821,8 +8844,8 @@ export function ImageNode({
|
||||
}
|
||||
rangeClassName="bg-slate-950 dark:bg-white"
|
||||
step={1}
|
||||
thumbClassName="h-3 w-3 border border-slate-300 bg-white shadow-[0_0_0_3px_rgba(15,23,42,0.06)]"
|
||||
trackClassName="h-1 bg-slate-200 dark:bg-white/14"
|
||||
thumbClassName="h-3.5 w-3.5 border border-slate-300 bg-white shadow-[0_0_0_4px_rgba(15,23,42,0.06)]"
|
||||
trackClassName="h-1.5 bg-slate-200 dark:bg-white/14"
|
||||
value={[selectedIndex]}
|
||||
/>
|
||||
</div>
|
||||
@ -8831,8 +8854,9 @@ export function ImageNode({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-3 py-3">
|
||||
<Button
|
||||
className="h-9 w-full rounded-[14px] bg-slate-950 text-[13px] font-semibold text-white shadow-[0_10px_22px_rgba(15,23,42,0.14)] hover:bg-slate-900 hover:text-white disabled:bg-slate-200 disabled:text-slate-400 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
|
||||
className="h-10 w-full rounded-[10px] bg-slate-950 text-[14px] font-semibold text-white shadow-none hover:bg-slate-900 hover:text-white disabled:bg-slate-100 disabled:text-slate-300 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
|
||||
disabled={
|
||||
isSubmittingAiWashQuick ||
|
||||
!selectedAiWashQuickPreset ||
|
||||
@ -8851,9 +8875,10 @@ export function ImageNode({
|
||||
: `${aiWashQuickModeLabel}${
|
||||
selectedAiWashQuickModel?.unitCost
|
||||
? ` · ${selectedAiWashQuickModel.unitCost} 积分`
|
||||
: ""
|
||||
: ""
|
||||
}`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
@ -8921,7 +8946,7 @@ export function ImageNode({
|
||||
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
|
||||
) : null}
|
||||
|
||||
{imagePickObjectEnabled ? (
|
||||
{isSelectedImageActionInline("pickObject") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing || isPickerLoading || isThisNodePicking}
|
||||
@ -8938,7 +8963,7 @@ export function ImageNode({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{imageEnhanceEnabled ? (
|
||||
{isSelectedImageActionInline("enhance") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
@ -8955,7 +8980,7 @@ export function ImageNode({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{imageRemoveBackgroundEnabled ? (
|
||||
{isSelectedImageActionInline("removeBackground") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
@ -8972,7 +8997,7 @@ export function ImageNode({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{imageAiWashQuickEnabled ? (
|
||||
{isSelectedImageActionInline("aiWashQuick") ? (
|
||||
<Button
|
||||
className={cn(
|
||||
"h-10 rounded-xl px-3 font-medium text-[13px] transition",
|
||||
@ -8999,7 +9024,7 @@ export function ImageNode({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{imageTextEditEnabled ? (
|
||||
{isSelectedImageActionInline("textEdit") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing || isDetectingText || isApplyingTextEdit}
|
||||
@ -9016,6 +9041,244 @@ export function ImageNode({
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("boxCutout") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("box-cutout")}
|
||||
title={imageBoxCutoutItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Scan}
|
||||
item={imageBoxCutoutItem}
|
||||
/>
|
||||
{imageBoxCutoutItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("localEdit") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing || isSubmittingLocalEdit}
|
||||
onClick={() => handleAction("local-edit")}
|
||||
title={imageLocalEditItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Wand2}
|
||||
item={imageLocalEditItem}
|
||||
/>
|
||||
{imageLocalEditItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("removeWatermark") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("watermark")}
|
||||
title={imageRemoveWatermarkItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Eraser}
|
||||
item={imageRemoveWatermarkItem}
|
||||
/>
|
||||
{imageRemoveWatermarkItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("resize") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("resize")}
|
||||
title={imageResizeItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Maximize2}
|
||||
item={imageResizeItem}
|
||||
/>
|
||||
{imageResizeItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("multiAngle") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("angle")}
|
||||
title={imageMultiAngleItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Box}
|
||||
item={imageMultiAngleItem}
|
||||
/>
|
||||
{imageMultiAngleItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("generate3dModel") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("generate-3d-model")}
|
||||
title={imageGenerate3dModelItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Box}
|
||||
item={imageGenerate3dModelItem}
|
||||
/>
|
||||
{imageGenerate3dModelItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("generate3dWorld") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("generate-3d-world")}
|
||||
title={imageGenerate3dWorldItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Globe2}
|
||||
item={imageGenerate3dWorldItem}
|
||||
/>
|
||||
{imageGenerate3dWorldItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("layerSplit") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("layer-split")}
|
||||
title={imageLayerSplitItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Layers}
|
||||
item={imageLayerSplitItem}
|
||||
/>
|
||||
{imageLayerSplitItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("extend") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("extend")}
|
||||
title={imageExtendItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Expand}
|
||||
item={imageExtendItem}
|
||||
/>
|
||||
{imageExtendItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("crop") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("crop")}
|
||||
title={imageCropItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Crop}
|
||||
item={imageCropItem}
|
||||
/>
|
||||
{imageCropItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("adjust") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("adjust")}
|
||||
title={imageAdjustItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={SlidersHorizontal}
|
||||
item={imageAdjustItem}
|
||||
/>
|
||||
{imageAdjustItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("flipRotate") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("flip-rotate")}
|
||||
title={imageFlipRotateItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={FlipHorizontal2}
|
||||
item={imageFlipRotateItem}
|
||||
/>
|
||||
{imageFlipRotateItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("promptRegenerate") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("prompt-regenerate")}
|
||||
title={imagePromptRegenerateItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={RefreshCw}
|
||||
item={imagePromptRegenerateItem}
|
||||
/>
|
||||
{imagePromptRegenerateItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{isSelectedImageActionInline("note") ? (
|
||||
<Button
|
||||
className="h-10 rounded-xl px-3 font-medium text-[13px] text-slate-700 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-100 dark:hover:bg-zinc-800"
|
||||
disabled={isProcessing}
|
||||
onClick={() => handleAction("note")}
|
||||
title={imageNoteItem.label}
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={StickyNote}
|
||||
item={imageNoteItem}
|
||||
/>
|
||||
{imageNoteItem.label}
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{hasSelectedImagePrimaryToolbarActions &&
|
||||
hasSelectedImageSecondaryToolbarActions ? (
|
||||
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
|
||||
@ -9043,7 +9306,7 @@ export function ImageNode({
|
||||
className="bg-white opacity-100 backdrop-blur-none dark:bg-slate-900 dark:text-slate-100"
|
||||
side="bottom"
|
||||
>
|
||||
{imageBoxCutoutEnabled ? (
|
||||
{isSelectedImageActionInMore("boxCutout") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("box-cutout")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9053,7 +9316,7 @@ export function ImageNode({
|
||||
{imageBoxCutoutItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageLocalEditEnabled ? (
|
||||
{isSelectedImageActionInMore("localEdit") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("local-edit")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9063,7 +9326,7 @@ export function ImageNode({
|
||||
{imageLocalEditItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageRemoveWatermarkEnabled ? (
|
||||
{isSelectedImageActionInMore("removeWatermark") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("watermark")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9073,7 +9336,7 @@ export function ImageNode({
|
||||
{imageRemoveWatermarkItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageResizeEnabled ? (
|
||||
{isSelectedImageActionInMore("resize") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("resize")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9083,7 +9346,7 @@ export function ImageNode({
|
||||
{imageResizeItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageMultiAngleEnabled ? (
|
||||
{isSelectedImageActionInMore("multiAngle") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("angle")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9093,7 +9356,7 @@ export function ImageNode({
|
||||
{imageMultiAngleItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageGenerate3dModelEnabled ? (
|
||||
{isSelectedImageActionInMore("generate3dModel") ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAction("generate-3d-model")}
|
||||
>
|
||||
@ -9105,7 +9368,7 @@ export function ImageNode({
|
||||
{imageGenerate3dModelItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageGenerate3dWorldEnabled ? (
|
||||
{isSelectedImageActionInMore("generate3dWorld") ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAction("generate-3d-world")}
|
||||
>
|
||||
@ -9117,7 +9380,7 @@ export function ImageNode({
|
||||
{imageGenerate3dWorldItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageLayerSplitEnabled ? (
|
||||
{isSelectedImageActionInMore("layerSplit") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("layer-split")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9127,7 +9390,7 @@ export function ImageNode({
|
||||
{imageLayerSplitItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageExtendEnabled ? (
|
||||
{isSelectedImageActionInMore("extend") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("extend")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9137,7 +9400,7 @@ export function ImageNode({
|
||||
{imageExtendItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageCropEnabled ? (
|
||||
{isSelectedImageActionInMore("crop") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("crop")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9147,7 +9410,7 @@ export function ImageNode({
|
||||
{imageCropItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageAdjustEnabled ? (
|
||||
{isSelectedImageActionInMore("adjust") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("adjust")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9157,7 +9420,7 @@ export function ImageNode({
|
||||
{imageAdjustItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageFlipRotateEnabled ? (
|
||||
{isSelectedImageActionInMore("flipRotate") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("flip-rotate")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
@ -9167,7 +9430,7 @@ export function ImageNode({
|
||||
{imageFlipRotateItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imagePromptRegenerateEnabled ? (
|
||||
{isSelectedImageActionInMore("promptRegenerate") ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleAction("prompt-regenerate")}
|
||||
>
|
||||
@ -9179,7 +9442,7 @@ export function ImageNode({
|
||||
{imagePromptRegenerateItem.label}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{imageNoteEnabled ? (
|
||||
{isSelectedImageActionInMore("note") ? (
|
||||
<DropdownMenuItem onClick={() => handleAction("note")}>
|
||||
<CanvasToolbarIcon
|
||||
className="mr-2 h-4 w-4"
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
export const CANVAS_TOOLBAR_CONFIG_SETTING_KEY = "canvas_toolbar_config";
|
||||
export const DEFAULT_SELECTED_IMAGE_INLINE_ACTION_COUNT = 7;
|
||||
export const MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT = 3;
|
||||
export const MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT = 12;
|
||||
|
||||
export const DEFAULT_CANVAS_TOOLBAR_ITEMS = [
|
||||
{
|
||||
@ -716,6 +719,7 @@ export type CanvasToolbarItemConfig = {
|
||||
|
||||
export type CanvasToolbarConfig = {
|
||||
items: Record<CanvasToolbarItemKey, CanvasToolbarItemConfig>;
|
||||
selectedImageInlineActionCount: number;
|
||||
};
|
||||
|
||||
const DEFAULT_ITEM_BY_KEY = DEFAULT_CANVAS_TOOLBAR_ITEMS.reduce(
|
||||
@ -732,12 +736,29 @@ const DEFAULT_ITEM_BY_KEY = DEFAULT_CANVAS_TOOLBAR_ITEMS.reduce(
|
||||
|
||||
export const defaultCanvasToolbarConfig: CanvasToolbarConfig = {
|
||||
items: DEFAULT_ITEM_BY_KEY,
|
||||
selectedImageInlineActionCount: DEFAULT_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
};
|
||||
|
||||
function normalizeString(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeInteger(
|
||||
value: unknown,
|
||||
fallback: number,
|
||||
min: number,
|
||||
max: number
|
||||
) {
|
||||
const parsed =
|
||||
typeof value === "number"
|
||||
? value
|
||||
: typeof value === "string"
|
||||
? Number(value)
|
||||
: Number.NaN;
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.min(max, Math.max(min, Math.round(parsed)));
|
||||
}
|
||||
|
||||
export function normalizeCanvasToolbarConfig(
|
||||
value: unknown
|
||||
): CanvasToolbarConfig {
|
||||
@ -769,7 +790,21 @@ export function normalizeCanvasToolbarConfig(
|
||||
};
|
||||
});
|
||||
|
||||
return { items };
|
||||
return {
|
||||
items,
|
||||
selectedImageInlineActionCount: normalizeInteger(
|
||||
raw.selectedImageInlineActionCount,
|
||||
DEFAULT_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT,
|
||||
MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function getSelectedImageInlineActionCount(
|
||||
config: CanvasToolbarConfig | null | undefined
|
||||
): number {
|
||||
return normalizeCanvasToolbarConfig(config).selectedImageInlineActionCount;
|
||||
}
|
||||
|
||||
export function getDefaultCanvasToolbarItem(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user