1
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import type { ChangeEvent, CSSProperties, PointerEvent } from 'react';
|
||||
import {
|
||||
Children,
|
||||
type ReactNode,
|
||||
@@ -954,18 +953,123 @@ function loadImageDimensionsFromDataUrl(source: string) {
|
||||
});
|
||||
}
|
||||
|
||||
const COVER_CROP_RATIO = 16 / 9;
|
||||
|
||||
type CoverCropDragHandle =
|
||||
| 'move'
|
||||
| 'north'
|
||||
| 'northEast'
|
||||
| 'east'
|
||||
| 'southEast'
|
||||
| 'south'
|
||||
| 'southWest'
|
||||
| 'west'
|
||||
| 'northWest';
|
||||
|
||||
type CoverCropDragSnapshot = {
|
||||
pointerId: number;
|
||||
handle: CoverCropDragHandle;
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
cropRect: CustomWorldCoverCropRect;
|
||||
previewWidth: number;
|
||||
previewHeight: number;
|
||||
};
|
||||
|
||||
const COVER_CROP_RESIZE_HANDLES: Array<{
|
||||
handle: Exclude<CoverCropDragHandle, 'move'>;
|
||||
label: string;
|
||||
className: string;
|
||||
dotClassName: string;
|
||||
}> = [
|
||||
{
|
||||
handle: 'northWest',
|
||||
label: '拖拽左上角裁剪边界',
|
||||
className: 'left-0 top-0 -translate-x-1/2 -translate-y-1/2 cursor-nwse-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'north',
|
||||
label: '拖拽上边裁剪边界',
|
||||
className: 'left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 cursor-ns-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'northEast',
|
||||
label: '拖拽右上角裁剪边界',
|
||||
className: 'right-0 top-0 translate-x-1/2 -translate-y-1/2 cursor-nesw-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'east',
|
||||
label: '拖拽右边裁剪边界',
|
||||
className: 'right-0 top-1/2 -translate-y-1/2 translate-x-1/2 cursor-ew-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'southEast',
|
||||
label: '拖拽右下角裁剪边界',
|
||||
className: 'bottom-0 right-0 translate-x-1/2 translate-y-1/2 cursor-nwse-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'south',
|
||||
label: '拖拽下边裁剪边界',
|
||||
className: 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 cursor-ns-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'southWest',
|
||||
label: '拖拽左下角裁剪边界',
|
||||
className: 'bottom-0 left-0 -translate-x-1/2 translate-y-1/2 cursor-nesw-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
{
|
||||
handle: 'west',
|
||||
label: '拖拽左边裁剪边界',
|
||||
className: 'left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 cursor-ew-resize',
|
||||
dotClassName: 'left-1/2 top-1/2',
|
||||
},
|
||||
];
|
||||
|
||||
function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function getCoverCropSizeBounds(imageSize: { width: number; height: number }) {
|
||||
const maxWidth = Math.max(
|
||||
1,
|
||||
Math.min(imageSize.width, imageSize.height * COVER_CROP_RATIO),
|
||||
);
|
||||
const minWidth = Math.min(maxWidth, Math.max(48, maxWidth * 0.16));
|
||||
|
||||
return { minWidth, maxWidth };
|
||||
}
|
||||
|
||||
function normalizeCoverCropRect(
|
||||
cropRect: CustomWorldCoverCropRect,
|
||||
imageSize: { width: number; height: number },
|
||||
): CustomWorldCoverCropRect {
|
||||
const { minWidth, maxWidth } = getCoverCropSizeBounds(imageSize);
|
||||
const width = clampNumber(cropRect.width, minWidth, maxWidth);
|
||||
const height = width / COVER_CROP_RATIO;
|
||||
const x = clampNumber(cropRect.x, 0, Math.max(0, imageSize.width - width));
|
||||
const y = clampNumber(cropRect.y, 0, Math.max(0, imageSize.height - height));
|
||||
|
||||
return { x, y, width, height };
|
||||
}
|
||||
|
||||
function buildCenteredCoverCropRect(
|
||||
width: number,
|
||||
height: number,
|
||||
): CustomWorldCoverCropRect {
|
||||
const targetRatio = 16 / 9;
|
||||
if (width <= 0 || height <= 0) {
|
||||
return { x: 0, y: 0, width: 1, height: 1 };
|
||||
}
|
||||
|
||||
if (width / height >= targetRatio) {
|
||||
if (width / height >= COVER_CROP_RATIO) {
|
||||
const cropHeight = height;
|
||||
const cropWidth = cropHeight * targetRatio;
|
||||
const cropWidth = cropHeight * COVER_CROP_RATIO;
|
||||
return {
|
||||
x: (width - cropWidth) / 2,
|
||||
y: 0,
|
||||
@@ -975,7 +1079,7 @@ function buildCenteredCoverCropRect(
|
||||
}
|
||||
|
||||
const cropWidth = width;
|
||||
const cropHeight = cropWidth / targetRatio;
|
||||
const cropHeight = cropWidth / COVER_CROP_RATIO;
|
||||
return {
|
||||
x: 0,
|
||||
y: (height - cropHeight) / 2,
|
||||
@@ -984,16 +1088,111 @@ function buildCenteredCoverCropRect(
|
||||
};
|
||||
}
|
||||
|
||||
function clampCoverCropRect(
|
||||
cropRect: CustomWorldCoverCropRect,
|
||||
function resizeCoverCropRectFromHandle(
|
||||
snapshot: CoverCropDragSnapshot,
|
||||
deltaX: number,
|
||||
deltaY: number,
|
||||
imageSize: { width: number; height: number },
|
||||
) {
|
||||
const width = Math.max(1, Math.min(imageSize.width, cropRect.width));
|
||||
const height = Math.max(1, Math.min(imageSize.height, cropRect.height));
|
||||
const x = Math.max(0, Math.min(imageSize.width - width, cropRect.x));
|
||||
const y = Math.max(0, Math.min(imageSize.height - height, cropRect.y));
|
||||
): CustomWorldCoverCropRect {
|
||||
const start = snapshot.cropRect;
|
||||
const startRight = start.x + start.width;
|
||||
const startBottom = start.y + start.height;
|
||||
const startCenterX = start.x + start.width / 2;
|
||||
const startCenterY = start.y + start.height / 2;
|
||||
const { minWidth, maxWidth } = getCoverCropSizeBounds(imageSize);
|
||||
const chooseWidth = (widthFromX: number, widthFromY: number) => {
|
||||
const xDistance = Math.abs(widthFromX - start.width);
|
||||
const yDistance = Math.abs(widthFromY - start.width);
|
||||
|
||||
return { x, y, width, height };
|
||||
return xDistance >= yDistance ? widthFromX : widthFromY;
|
||||
};
|
||||
const clampWidth = (width: number, maxByAnchor = maxWidth) =>
|
||||
clampNumber(width, minWidth, Math.max(minWidth, Math.min(maxWidth, maxByAnchor)));
|
||||
|
||||
if (snapshot.handle === 'move') {
|
||||
return normalizeCoverCropRect(
|
||||
{
|
||||
...start,
|
||||
x: start.x + deltaX,
|
||||
y: start.y + deltaY,
|
||||
},
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.handle === 'east' || snapshot.handle === 'west') {
|
||||
const isEast = snapshot.handle === 'east';
|
||||
const anchorX = isEast ? start.x : startRight;
|
||||
const maxByAnchorX = isEast ? imageSize.width - anchorX : anchorX;
|
||||
const maxByCenterY =
|
||||
2 * Math.min(startCenterY, imageSize.height - startCenterY) *
|
||||
COVER_CROP_RATIO;
|
||||
const width = clampWidth(
|
||||
start.width + (isEast ? deltaX : -deltaX),
|
||||
Math.min(maxByAnchorX, maxByCenterY),
|
||||
);
|
||||
const height = width / COVER_CROP_RATIO;
|
||||
|
||||
return normalizeCoverCropRect(
|
||||
{
|
||||
x: isEast ? anchorX : anchorX - width,
|
||||
y: startCenterY - height / 2,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.handle === 'north' || snapshot.handle === 'south') {
|
||||
const isSouth = snapshot.handle === 'south';
|
||||
const anchorY = isSouth ? start.y : startBottom;
|
||||
const maxByAnchorY =
|
||||
(isSouth ? imageSize.height - anchorY : anchorY) * COVER_CROP_RATIO;
|
||||
const maxByCenterX =
|
||||
2 * Math.min(startCenterX, imageSize.width - startCenterX);
|
||||
const width = clampWidth(
|
||||
(start.height + (isSouth ? deltaY : -deltaY)) * COVER_CROP_RATIO,
|
||||
Math.min(maxByAnchorY, maxByCenterX),
|
||||
);
|
||||
const height = width / COVER_CROP_RATIO;
|
||||
|
||||
return normalizeCoverCropRect(
|
||||
{
|
||||
x: startCenterX - width / 2,
|
||||
y: isSouth ? anchorY : anchorY - height,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
|
||||
const isEast = snapshot.handle === 'northEast' || snapshot.handle === 'southEast';
|
||||
const isSouth = snapshot.handle === 'southEast' || snapshot.handle === 'southWest';
|
||||
const anchorX = isEast ? start.x : startRight;
|
||||
const anchorY = isSouth ? start.y : startBottom;
|
||||
const maxByAnchorX = isEast ? imageSize.width - anchorX : anchorX;
|
||||
const maxByAnchorY =
|
||||
(isSouth ? imageSize.height - anchorY : anchorY) * COVER_CROP_RATIO;
|
||||
const widthFromX = start.width + (isEast ? deltaX : -deltaX);
|
||||
const widthFromY =
|
||||
(start.height + (isSouth ? deltaY : -deltaY)) * COVER_CROP_RATIO;
|
||||
const width = clampWidth(
|
||||
chooseWidth(widthFromX, widthFromY),
|
||||
Math.min(maxByAnchorX, maxByAnchorY),
|
||||
);
|
||||
const height = width / COVER_CROP_RATIO;
|
||||
|
||||
return normalizeCoverCropRect(
|
||||
{
|
||||
x: isEast ? anchorX : anchorX - width,
|
||||
y: isSouth ? anchorY : anchorY - height,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
imageSize,
|
||||
);
|
||||
}
|
||||
|
||||
function buildCoverCropPreviewStyle(
|
||||
@@ -3316,51 +3515,116 @@ function buildGeneratedCoverProfile(
|
||||
function CoverUploadCropModal({
|
||||
imageDataUrl,
|
||||
imageSize,
|
||||
worldName,
|
||||
isSubmitting,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: {
|
||||
imageDataUrl: string;
|
||||
imageSize: { width: number; height: number };
|
||||
worldName: string;
|
||||
isSubmitting: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: (cropRect: CustomWorldCoverCropRect) => void;
|
||||
}) {
|
||||
const [zoomPercent, setZoomPercent] = useState(100);
|
||||
const baseCropRect = useMemo(
|
||||
() => buildCenteredCoverCropRect(imageSize.width, imageSize.height),
|
||||
[imageSize],
|
||||
const previewRef = useRef<HTMLDivElement | null>(null);
|
||||
const dragSnapshotRef = useRef<CoverCropDragSnapshot | null>(null);
|
||||
const [activeDragHandle, setActiveDragHandle] =
|
||||
useState<CoverCropDragHandle | null>(null);
|
||||
const [cropRect, setCropRect] = useState(() =>
|
||||
normalizeCoverCropRect(
|
||||
buildCenteredCoverCropRect(imageSize.width, imageSize.height),
|
||||
imageSize,
|
||||
),
|
||||
);
|
||||
const [offsetX, setOffsetX] = useState(0);
|
||||
const [offsetY, setOffsetY] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setZoomPercent(100);
|
||||
setOffsetX(0);
|
||||
setOffsetY(0);
|
||||
}, [imageDataUrl]);
|
||||
|
||||
const cropRect = useMemo(() => {
|
||||
const scale = Math.max(1, zoomPercent / 100);
|
||||
const nextCropRect = {
|
||||
width: baseCropRect.width / scale,
|
||||
height: baseCropRect.height / scale,
|
||||
x: baseCropRect.x + offsetX,
|
||||
y: baseCropRect.y + offsetY,
|
||||
};
|
||||
|
||||
return clampCoverCropRect(nextCropRect, imageSize);
|
||||
}, [baseCropRect, imageSize, offsetX, offsetY, zoomPercent]);
|
||||
setActiveDragHandle(null);
|
||||
dragSnapshotRef.current = null;
|
||||
setCropRect(
|
||||
normalizeCoverCropRect(
|
||||
buildCenteredCoverCropRect(imageSize.width, imageSize.height),
|
||||
imageSize,
|
||||
),
|
||||
);
|
||||
}, [imageDataUrl, imageSize]);
|
||||
|
||||
const previewStyle = useMemo(
|
||||
() => buildCoverCropPreviewStyle(cropRect, imageSize),
|
||||
[cropRect, imageSize],
|
||||
);
|
||||
const editorPreviewStyle = useMemo(
|
||||
() =>
|
||||
({
|
||||
aspectRatio: `${imageSize.width} / ${imageSize.height}`,
|
||||
width: `min(100%, calc(min(58vh, 34rem) * ${
|
||||
imageSize.width / Math.max(1, imageSize.height)
|
||||
}))`,
|
||||
}) satisfies CSSProperties,
|
||||
[imageSize],
|
||||
);
|
||||
const outputPreviewStyle = useMemo(
|
||||
() =>
|
||||
({
|
||||
left: `${-(cropRect.x / cropRect.width) * 100}%`,
|
||||
top: `${-(cropRect.y / cropRect.height) * 100}%`,
|
||||
width: `${(imageSize.width / cropRect.width) * 100}%`,
|
||||
height: `${(imageSize.height / cropRect.height) * 100}%`,
|
||||
}) satisfies CSSProperties,
|
||||
[cropRect, imageSize],
|
||||
);
|
||||
|
||||
const maxOffsetX = Math.max(0, imageSize.width - cropRect.width);
|
||||
const maxOffsetY = Math.max(0, imageSize.height - cropRect.height);
|
||||
const beginCropDrag = (
|
||||
handle: CoverCropDragHandle,
|
||||
event: PointerEvent<HTMLElement>,
|
||||
) => {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = previewRef.current;
|
||||
if (!preview) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = preview.getBoundingClientRect();
|
||||
dragSnapshotRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
handle,
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
cropRect,
|
||||
previewWidth: rect.width,
|
||||
previewHeight: rect.height,
|
||||
};
|
||||
setActiveDragHandle(handle);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const updateCropDrag = (event: PointerEvent<HTMLElement>) => {
|
||||
const snapshot = dragSnapshotRef.current;
|
||||
if (!snapshot || snapshot.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX =
|
||||
((event.clientX - snapshot.clientX) * imageSize.width) /
|
||||
Math.max(1, snapshot.previewWidth);
|
||||
const deltaY =
|
||||
((event.clientY - snapshot.clientY) * imageSize.height) /
|
||||
Math.max(1, snapshot.previewHeight);
|
||||
setCropRect(resizeCoverCropRectFromHandle(snapshot, deltaX, deltaY, imageSize));
|
||||
};
|
||||
|
||||
const stopCropDrag = (event: PointerEvent<HTMLElement>) => {
|
||||
if (dragSnapshotRef.current?.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
dragSnapshotRef.current = null;
|
||||
setActiveDragHandle(null);
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalShell
|
||||
@@ -3371,77 +3635,75 @@ function CoverUploadCropModal({
|
||||
disableClose={isSubmitting}
|
||||
>
|
||||
<div className="grid gap-5 lg:grid-cols-[minmax(0,1.05fr)_20rem]">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-3">
|
||||
<ImagePreview
|
||||
src={imageDataUrl}
|
||||
alt="上传封面裁剪预览"
|
||||
fallbackLabel={worldName.slice(0, 4) || '封面'}
|
||||
tone="landscape"
|
||||
overlayInteractive
|
||||
previewOverlay={
|
||||
<>
|
||||
<div className="absolute inset-0 bg-black/45" />
|
||||
<div
|
||||
className="absolute border border-sky-300/90 bg-white/8 shadow-[0_0_0_9999px_rgba(0,0,0,0.35)]"
|
||||
style={previewStyle}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Field label="缩放">
|
||||
<input
|
||||
type="range"
|
||||
min={100}
|
||||
max={220}
|
||||
step={1}
|
||||
value={zoomPercent}
|
||||
onChange={(event) => setZoomPercent(Number(event.target.value))}
|
||||
disabled={isSubmitting}
|
||||
className="w-full accent-sky-400"
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/18 p-2 sm:p-3">
|
||||
<div
|
||||
ref={previewRef}
|
||||
aria-label="封面裁剪操作区"
|
||||
className="relative mx-auto overflow-hidden rounded-xl border border-white/10 bg-black/40 select-none touch-none"
|
||||
style={editorPreviewStyle}
|
||||
>
|
||||
<ResolvedAssetImage
|
||||
src={imageDataUrl}
|
||||
alt="上传封面裁剪预览"
|
||||
className="h-full w-full object-fill"
|
||||
draggable={false}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="左右位置">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={Math.max(0, Math.floor(maxOffsetX))}
|
||||
step={1}
|
||||
value={Math.max(0, Math.floor(offsetX + baseCropRect.x))}
|
||||
onChange={(event) =>
|
||||
setOffsetX(Number(event.target.value) - baseCropRect.x)
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
className="w-full accent-sky-400"
|
||||
<div
|
||||
className={`absolute border-2 border-sky-200/95 shadow-[0_0_0_9999px_rgba(0,0,0,0.38)] outline outline-1 outline-black/35 ${
|
||||
activeDragHandle === 'move' ? 'cursor-grabbing' : 'cursor-grab'
|
||||
}`}
|
||||
style={previewStyle}
|
||||
onPointerDown={(event) => beginCropDrag('move', event)}
|
||||
onPointerMove={updateCropDrag}
|
||||
onPointerUp={stopCropDrag}
|
||||
onPointerCancel={stopCropDrag}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="上下位置">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={Math.max(0, Math.floor(maxOffsetY))}
|
||||
step={1}
|
||||
value={Math.max(0, Math.floor(offsetY + baseCropRect.y))}
|
||||
onChange={(event) =>
|
||||
setOffsetY(Number(event.target.value) - baseCropRect.y)
|
||||
}
|
||||
disabled={isSubmitting}
|
||||
className="w-full accent-sky-400"
|
||||
/>
|
||||
</Field>
|
||||
<div
|
||||
className="pointer-events-none absolute border border-white/70"
|
||||
style={previewStyle}
|
||||
>
|
||||
<div className="absolute inset-x-0 top-1/3 border-t border-white/35" />
|
||||
<div className="absolute inset-x-0 top-2/3 border-t border-white/35" />
|
||||
<div className="absolute inset-y-0 left-1/3 border-l border-white/35" />
|
||||
<div className="absolute inset-y-0 left-2/3 border-l border-white/35" />
|
||||
</div>
|
||||
<div className="pointer-events-none absolute" style={previewStyle}>
|
||||
{COVER_CROP_RESIZE_HANDLES.map((handleConfig) => (
|
||||
<button
|
||||
key={handleConfig.handle}
|
||||
type="button"
|
||||
aria-label={handleConfig.label}
|
||||
disabled={isSubmitting}
|
||||
className={`pointer-events-auto absolute z-10 h-10 w-10 rounded-full border border-white/15 bg-black/5 p-0 disabled:cursor-not-allowed disabled:opacity-45 sm:h-9 sm:w-9 ${handleConfig.className}`}
|
||||
onPointerDown={(event) =>
|
||||
beginCropDrag(handleConfig.handle, event)
|
||||
}
|
||||
onPointerMove={updateCropDrag}
|
||||
onPointerUp={stopCropDrag}
|
||||
onPointerCancel={stopCropDrag}
|
||||
>
|
||||
<span
|
||||
className={`absolute h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white bg-sky-300 shadow-[0_0_0_3px_rgba(2,132,199,0.32)] ${handleConfig.dotClassName}`}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4 text-sm leading-6 text-zinc-200">
|
||||
成品会固定保存为 16:9,并由后端统一压缩到 1600 × 900。
|
||||
</div>
|
||||
<div className="rounded-2xl border border-white/8 bg-black/20 px-4 py-4 text-xs leading-6 text-zinc-400">
|
||||
当前裁剪区域:
|
||||
<br />
|
||||
{`x ${Math.round(cropRect.x)} / y ${Math.round(cropRect.y)} / w ${Math.round(cropRect.width)} / h ${Math.round(cropRect.height)}`}
|
||||
<div className="overflow-hidden rounded-2xl border border-white/8 bg-black/20 p-2">
|
||||
<div className="relative aspect-[16/9] overflow-hidden rounded-xl bg-black/30">
|
||||
<ResolvedAssetImage
|
||||
src={imageDataUrl}
|
||||
alt="上传封面裁剪结果预览"
|
||||
className="absolute max-w-none object-fill"
|
||||
draggable={false}
|
||||
style={outputPreviewStyle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
|
||||
<ActionButton
|
||||
@@ -3901,7 +4163,6 @@ export function WorldCoverEditor({
|
||||
<CoverUploadCropModal
|
||||
imageDataUrl={pendingUploadImageDataUrl}
|
||||
imageSize={pendingUploadImageSize}
|
||||
worldName={profile.name}
|
||||
isSubmitting={isUploading}
|
||||
onCancel={() => {
|
||||
if (isUploading) {
|
||||
|
||||
Reference in New Issue
Block a user