fix: duplacation and formatting

This commit is contained in:
Moyasee
2025-11-04 22:10:07 +02:00
parent 6733a3e5b0
commit e04a94d10d
5 changed files with 214 additions and 227 deletions

View File

@@ -681,6 +681,11 @@
"crop_background_image": "Crop Background Image",
"crop": "Crop",
"cropping": "Cropping…",
"crop_area": "Crop area",
"resize_handle_nw": "Resize handle - northwest corner",
"resize_handle_ne": "Resize handle - northeast corner",
"resize_handle_sw": "Resize handle - southwest corner",
"resize_handle_se": "Resize handle - southeast corner",
"zoom_in": "Zoom In",
"zoom_out": "Zoom Out",
"image_crop_failure": "Failed to crop image. Please try again.",

View File

@@ -105,4 +105,3 @@
justify-content: flex-end;
}
}

View File

@@ -51,37 +51,59 @@ export function ImageCropper({
return imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
};
const calculateInitialCropArea = useCallback(() => {
const calculateContainerBounds = useCallback(() => {
const container = containerRef.current;
if (!container) return;
if (!container) return null;
const containerRect = container.getBoundingClientRect();
if (containerRect.width === 0 || containerRect.height === 0) return;
if (containerRect.width === 0 || containerRect.height === 0) return null;
const maxWidth = containerRect.width - 40;
const maxHeight = containerRect.height - 200;
return {
maxWidth: containerRect.width - 40,
maxHeight: containerRect.height - 200,
};
}, []);
if (maxWidth <= 0 || maxHeight <= 0) return;
const calculateDisplayDimensions = useCallback(
(bounds: { maxWidth: number; maxHeight: number } | null) => {
if (!imageLoaded) return { width: 0, height: 0 };
const imageAspect = imageSize.width / imageSize.height;
let displayWidth = imageSize.width * zoom;
let displayHeight = imageSize.height * zoom;
if (!bounds) {
return {
width: imageSize.width * zoom,
height: imageSize.height * zoom,
};
}
if (displayWidth > maxWidth) {
displayWidth = maxWidth;
displayHeight = displayWidth / imageAspect;
}
if (displayHeight > maxHeight) {
displayHeight = maxHeight;
displayWidth = displayHeight * imageAspect;
}
const imageAspect = imageSize.width / imageSize.height;
let displayWidth = imageSize.width * zoom;
let displayHeight = imageSize.height * zoom;
if (displayWidth > bounds.maxWidth) {
displayWidth = bounds.maxWidth;
displayHeight = displayWidth / imageAspect;
}
if (displayHeight > bounds.maxHeight) {
displayHeight = bounds.maxHeight;
displayWidth = displayHeight * imageAspect;
}
return { width: displayWidth, height: displayHeight };
},
[imageLoaded, imageSize]
);
const calculateInitialCropArea = useCallback(() => {
const bounds = calculateContainerBounds();
if (!bounds || bounds.maxWidth <= 0 || bounds.maxHeight <= 0) return;
const { width: displayWidth, height: displayHeight } =
calculateDisplayDimensions(bounds);
const effectiveAspectRatio = circular ? 1 : aspectRatio;
let cropWidth: number;
let cropHeight: number;
if (effectiveAspectRatio) {
const displayAspect = displayWidth / displayHeight;
if (displayAspect > effectiveAspectRatio) {
@@ -103,7 +125,17 @@ export function ImageCropper({
width: cropWidth,
height: cropHeight,
});
}, [imageSize, aspectRatio, circular]);
}, [
calculateContainerBounds,
calculateDisplayDimensions,
aspectRatio,
circular,
]);
const getDisplaySize = useCallback(() => {
const bounds = calculateContainerBounds();
return calculateDisplayDimensions(bounds);
}, [calculateContainerBounds, calculateDisplayDimensions]);
useEffect(() => {
const img = new Image();
@@ -159,40 +191,6 @@ export function ImageCropper({
};
}, [imageLoaded, imageSize, calculateInitialCropArea]);
const getDisplaySize = useCallback(() => {
if (!imageLoaded) return { width: 0, height: 0 };
const container = containerRef.current;
if (!container) {
return {
width: imageSize.width * zoom,
height: imageSize.height * zoom,
};
}
const containerRect = container.getBoundingClientRect();
const maxWidth = containerRect.width - 40;
const maxHeight = containerRect.height - 200;
const imageAspect = imageSize.width / imageSize.height;
let displayWidth = imageSize.width * zoom;
let displayHeight = imageSize.height * zoom;
if (displayWidth > maxWidth) {
displayWidth = maxWidth;
displayHeight = displayWidth / imageAspect;
}
if (displayHeight > maxHeight) {
displayHeight = maxHeight;
displayWidth = displayHeight * imageAspect;
}
return {
width: displayWidth,
height: displayHeight,
};
}, [imageLoaded, imageSize]);
const getRealCropArea = (): CropArea => {
if (!imageLoaded || imageSize.width === 0 || imageSize.height === 0) {
return cropArea;
@@ -214,6 +212,27 @@ export function ImageCropper({
};
};
const adjustAspectRatio = useCallback(
(width: number, height: number, ratio: number) => {
const currentRatio = width / height;
if (currentRatio > ratio) {
return { width, height: width / ratio };
}
return { width: height * ratio, height };
},
[]
);
const calculateMinSize = useCallback(
(effectiveAspectRatio: number | undefined) => {
if (!effectiveAspectRatio) return minCropSize;
return effectiveAspectRatio > 1
? minCropSize * effectiveAspectRatio
: minCropSize / effectiveAspectRatio;
},
[minCropSize]
);
const constrainCropArea = useCallback(
(area: CropArea): CropArea => {
const displaySize = getDisplaySize();
@@ -222,32 +241,23 @@ export function ImageCropper({
const effectiveAspectRatio = circular ? 1 : aspectRatio;
if (effectiveAspectRatio) {
const currentRatio = width / height;
if (currentRatio > effectiveAspectRatio) {
height = width / effectiveAspectRatio;
} else {
width = height * effectiveAspectRatio;
}
}
let minSize = minCropSize;
if (effectiveAspectRatio) {
if (effectiveAspectRatio > 1) {
minSize = minCropSize * effectiveAspectRatio;
} else {
minSize = minCropSize / effectiveAspectRatio;
}
({ width, height } = adjustAspectRatio(
width,
height,
effectiveAspectRatio
));
}
const minSize = calculateMinSize(effectiveAspectRatio);
width = Math.max(minSize, Math.min(width, displaySize.width));
height = Math.max(minSize, Math.min(height, displaySize.height));
if (effectiveAspectRatio) {
if (width / height > effectiveAspectRatio) {
width = height * effectiveAspectRatio;
} else {
height = width / effectiveAspectRatio;
}
({ width, height } = adjustAspectRatio(
width,
height,
effectiveAspectRatio
));
}
x = Math.max(0, Math.min(x, displaySize.width - width));
@@ -255,7 +265,7 @@ export function ImageCropper({
return { x, y, width, height };
},
[getDisplaySize, circular, aspectRatio, minCropSize]
[getDisplaySize, circular, aspectRatio, adjustAspectRatio, calculateMinSize]
);
const getRelativeCoordinates = (
@@ -275,9 +285,7 @@ export function ImageCropper({
(coords: { x: number; y: number }) => {
const newX = coords.x - dragStart.x;
const newY = coords.y - dragStart.y;
setCropArea((prev) =>
constrainCropArea({ ...prev, x: newX, y: newY })
);
setCropArea((prev) => constrainCropArea({ ...prev, x: newX, y: newY }));
},
[dragStart, constrainCropArea]
);
@@ -287,38 +295,49 @@ export function ImageCropper({
coords: { x: number; y: number },
cropStart: CropArea
) => {
if (resizeHandle === "resize-se") {
return {
deltaX: coords.x - (cropStart.x + cropStart.width),
deltaY: coords.y - (cropStart.y + cropStart.height),
newX: cropStart.x,
newY: cropStart.y,
};
}
if (resizeHandle === "resize-sw") {
return {
deltaX: cropStart.x - coords.x,
deltaY: coords.y - (cropStart.y + cropStart.height),
newX: coords.x,
newY: cropStart.y,
};
}
if (resizeHandle === "resize-ne") {
return {
deltaX: coords.x - (cropStart.x + cropStart.width),
deltaY: cropStart.y - coords.y,
newX: cropStart.x,
newY: coords.y,
};
}
if (resizeHandle === "resize-nw") {
return {
deltaX: cropStart.x - coords.x,
deltaY: cropStart.y - coords.y,
newX: coords.x,
newY: coords.y,
};
const handleMap: Record<
string,
(
coords: { x: number; y: number },
cropStart: CropArea
) => {
deltaX: number;
deltaY: number;
newX: number;
newY: number;
}
> = {
"resize-se": (c, cs) => ({
deltaX: c.x - (cs.x + cs.width),
deltaY: c.y - (cs.y + cs.height),
newX: cs.x,
newY: cs.y,
}),
"resize-sw": (c, cs) => ({
deltaX: cs.x - c.x,
deltaY: c.y - (cs.y + cs.height),
newX: c.x,
newY: cs.y,
}),
"resize-ne": (c, cs) => ({
deltaX: c.x - (cs.x + cs.width),
deltaY: cs.y - c.y,
newX: cs.x,
newY: c.y,
}),
"resize-nw": (c, cs) => ({
deltaX: cs.x - c.x,
deltaY: cs.y - c.y,
newX: c.x,
newY: c.y,
}),
};
const handler = handleMap[resizeHandle];
if (handler) {
return handler(coords, cropStart);
}
return {
deltaX: 0,
deltaY: 0,
@@ -327,6 +346,25 @@ export function ImageCropper({
};
};
const adjustPositionForHandle = (
resizeHandle: string,
cropStart: CropArea,
adjustedWidth: number,
adjustedHeight: number
) => {
let adjustedX = cropStart.x;
let adjustedY = cropStart.y;
if (resizeHandle === "resize-nw" || resizeHandle === "resize-sw") {
adjustedX = cropStart.x + cropStart.width - adjustedWidth;
}
if (resizeHandle === "resize-nw" || resizeHandle === "resize-ne") {
adjustedY = cropStart.y + cropStart.height - adjustedHeight;
}
return { x: adjustedX, y: adjustedY };
};
const applyCircularConstraint = (
newWidth: number,
newHeight: number,
@@ -337,22 +375,15 @@ export function ImageCropper({
const deltaSize = size - Math.min(cropStart.width, cropStart.height);
const adjustedWidth = cropStart.width + deltaSize;
const adjustedHeight = cropStart.height + deltaSize;
let adjustedX = cropStart.x;
let adjustedY = cropStart.y;
if (resizeHandle === "resize-nw" || resizeHandle === "resize-sw") {
adjustedX = cropStart.x + cropStart.width - adjustedWidth;
}
if (resizeHandle === "resize-nw" || resizeHandle === "resize-ne") {
adjustedY = cropStart.y + cropStart.height - adjustedHeight;
}
const { x, y } = adjustPositionForHandle(
resizeHandle,
cropStart,
adjustedWidth,
adjustedHeight
);
return {
width: adjustedWidth,
height: adjustedHeight,
x: adjustedX,
y: adjustedY,
};
return { width: adjustedWidth, height: adjustedHeight, x, y };
};
const applyAspectRatioConstraint = (
@@ -362,32 +393,20 @@ export function ImageCropper({
resizeHandle: string,
aspectRatio: number
) => {
const currentRatio = newWidth / newHeight;
let adjustedWidth = newWidth;
let adjustedHeight = newHeight;
const { width: adjustedWidth, height: adjustedHeight } = adjustAspectRatio(
newWidth,
newHeight,
aspectRatio
);
if (currentRatio > aspectRatio) {
adjustedHeight = newWidth / aspectRatio;
} else {
adjustedWidth = newHeight * aspectRatio;
}
const { x, y } = adjustPositionForHandle(
resizeHandle,
cropStart,
adjustedWidth,
adjustedHeight
);
let adjustedX = cropStart.x;
let adjustedY = cropStart.y;
if (resizeHandle === "resize-nw" || resizeHandle === "resize-sw") {
adjustedX = cropStart.x + cropStart.width - adjustedWidth;
}
if (resizeHandle === "resize-nw" || resizeHandle === "resize-ne") {
adjustedY = cropStart.y + cropStart.height - adjustedHeight;
}
return {
width: adjustedWidth,
height: adjustedHeight,
x: adjustedX,
y: adjustedY,
};
return { width: adjustedWidth, height: adjustedHeight, x, y };
};
const handleResize = useCallback(
@@ -481,7 +500,15 @@ export function ImageCropper({
handleResize(coords);
}
},
[imageLoaded, isDragging, isResizing, cropStart, resizeHandle, handleDrag, handleResize]
[
imageLoaded,
isDragging,
isResizing,
cropStart,
resizeHandle,
handleDrag,
handleResize,
]
);
const handleMouseUp = useCallback(() => {
@@ -491,37 +518,30 @@ export function ImageCropper({
setCropStart(null);
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
}
},
[]
);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
}
}, []);
const handleOverlayKeyDown = useCallback(
(e: React.KeyboardEvent) => {
const step = e.shiftKey ? 10 : 1;
let newX = cropArea.x;
let newY = cropArea.y;
const keyMap: Record<
string,
(area: CropArea) => { x: number; y: number }
> = {
ArrowLeft: (area) => ({ x: area.x - step, y: area.y }),
ArrowRight: (area) => ({ x: area.x + step, y: area.y }),
ArrowUp: (area) => ({ x: area.x, y: area.y - step }),
ArrowDown: (area) => ({ x: area.x, y: area.y + step }),
};
if (e.key === "ArrowLeft") {
e.preventDefault();
newX = cropArea.x - step;
} else if (e.key === "ArrowRight") {
e.preventDefault();
newX = cropArea.x + step;
} else if (e.key === "ArrowUp") {
e.preventDefault();
newY = cropArea.y - step;
} else if (e.key === "ArrowDown") {
e.preventDefault();
newY = cropArea.y + step;
} else {
return;
}
const handler = keyMap[e.key];
if (!handler) return;
e.preventDefault();
const { x: newX, y: newY } = handler(cropArea);
setCropArea((prev) => constrainCropArea({ ...prev, x: newX, y: newY }));
},
[cropArea, constrainCropArea]
@@ -540,7 +560,6 @@ export function ImageCropper({
return cleanup;
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
const handleCrop = async () => {
setIsCropping(true);
try {
@@ -555,10 +574,7 @@ export function ImageCropper({
return (
<div className="image-cropper">
<div
className="image-cropper__container"
ref={containerRef}
>
<div className="image-cropper__container" ref={containerRef}>
<div className="image-cropper__image-wrapper">
{imageLoaded && (
<img
@@ -588,38 +604,17 @@ export function ImageCropper({
onMouseDown={handleMouseDown}
onKeyDown={handleOverlayKeyDown}
>
<button
type="button"
className="image-cropper__crop-handle image-cropper__crop-handle--nw"
data-handle="resize-nw"
aria-label={t("resize_handle_nw")}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
/>
<button
type="button"
className="image-cropper__crop-handle image-cropper__crop-handle--ne"
data-handle="resize-ne"
aria-label={t("resize_handle_ne")}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
/>
<button
type="button"
className="image-cropper__crop-handle image-cropper__crop-handle--sw"
data-handle="resize-sw"
aria-label={t("resize_handle_sw")}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
/>
<button
type="button"
className="image-cropper__crop-handle image-cropper__crop-handle--se"
data-handle="resize-se"
aria-label={t("resize_handle_se")}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
/>
{(["nw", "ne", "sw", "se"] as const).map((position) => (
<button
key={position}
type="button"
className={`image-cropper__crop-handle image-cropper__crop-handle--${position}`}
data-handle={`resize-${position}`}
aria-label={t(`resize_handle_${position}`)}
onMouseDown={handleMouseDown}
onKeyDown={handleKeyDown}
/>
))}
</section>
)}
</div>
@@ -642,4 +637,3 @@ export function ImageCropper({
</div>
);
}

View File

@@ -103,17 +103,7 @@ export async function cropImageToCircle(
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
ctx.clip();
ctx.drawImage(
img,
cropArea.x,
cropArea.y,
size,
size,
0,
0,
size,
size
);
ctx.drawImage(img, cropArea.x, cropArea.y, size, size, 0, 0, size, size);
canvas.toBlob(
(blob) => {
@@ -138,4 +128,3 @@ export async function cropImageToCircle(
img.src = imagePath.startsWith("local:") ? imagePath : `local:${imagePath}`;
});
}

View File

@@ -1046,9 +1046,9 @@
optionalDependencies:
global-agent "^3.0.0"
"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2":
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
version "10.2.0-electron.1"
resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2"
resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
dependencies:
env-paths "^2.2.0"
exponential-backoff "^3.1.1"