mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-21 13:23:14 +01:00
Create a rotate 90 degrees clockwise button for each image (#666)
* Create a rotate 90 degrees clockwise button for each image * Added more error handling and turned rotate function to async as recommended by coderabbitai
This commit is contained in:
@@ -75,6 +75,26 @@
|
|||||||
<p>Delete photo</p>
|
<p>Delete photo</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
@click.prevent="
|
||||||
|
async () => {
|
||||||
|
await rotateBase64Image90Deg(photo.fileBase64, index);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<MdiRotateClockwise />
|
||||||
|
<div class="sr-only">Rotate photo</div>
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Rotate photo</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<!-- TODO: re-enable when we have a way to set primary photos -->
|
<!-- TODO: re-enable when we have a way to set primary photos -->
|
||||||
<!-- <Tooltip>
|
<!-- <Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
@@ -114,6 +134,7 @@
|
|||||||
import MdiPackageVariant from "~icons/mdi/package-variant";
|
import MdiPackageVariant from "~icons/mdi/package-variant";
|
||||||
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
import MdiPackageVariantClosed from "~icons/mdi/package-variant-closed";
|
||||||
import MdiDelete from "~icons/mdi/delete";
|
import MdiDelete from "~icons/mdi/delete";
|
||||||
|
import MdiRotateClockwise from "~icons/mdi/rotate-clockwise";
|
||||||
// import MdiStarOutline from "~icons/mdi/star-outline";
|
// import MdiStarOutline from "~icons/mdi/star-outline";
|
||||||
// import MdiStar from "~icons/mdi/star";
|
// import MdiStar from "~icons/mdi/star";
|
||||||
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
|
import { AttachmentTypes } from "~~/lib/api/types/non-generated";
|
||||||
@@ -294,4 +315,79 @@
|
|||||||
navigateTo(`/item/${data.id}`);
|
navigateTo(`/item/${data.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dataURLtoFile(dataURL: string, fileName: string) {
|
||||||
|
try {
|
||||||
|
const arr = dataURL.split(",");
|
||||||
|
const mimeMatch = arr[0].match(/:(.*?);/);
|
||||||
|
if (!mimeMatch || !mimeMatch[1]) {
|
||||||
|
throw new Error("Invalid data URL format");
|
||||||
|
}
|
||||||
|
const mime = mimeMatch[1];
|
||||||
|
|
||||||
|
// Validate mime type is an image
|
||||||
|
if (!mime.startsWith("image/")) {
|
||||||
|
throw new Error("Invalid mime type, expected image");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bstr = atob(arr[arr.length - 1]);
|
||||||
|
let n = bstr.length;
|
||||||
|
const u8arr = new Uint8Array(n);
|
||||||
|
while (n--) {
|
||||||
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
|
}
|
||||||
|
return new File([u8arr], fileName, { type: mime });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error converting data URL to file:", error);
|
||||||
|
// Return a fallback or rethrow based on your error handling strategy
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rotateBase64Image90Deg(base64Image: string, index: number) {
|
||||||
|
// Create an off-screen canvas
|
||||||
|
const offScreenCanvas = document.createElement("canvas");
|
||||||
|
const offScreenCanvasCtx = offScreenCanvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!offScreenCanvasCtx) {
|
||||||
|
toast.error("Your browser doesn't support canvas operations");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an image
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
// Create a promise to handle the image loading
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
img.onload = () => resolve();
|
||||||
|
img.onerror = () => reject(new Error("Failed to load image"));
|
||||||
|
img.src = base64Image;
|
||||||
|
}).catch(error => {
|
||||||
|
toast.error("Failed to rotate image: " + error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set its dimensions to rotated size
|
||||||
|
offScreenCanvas.height = img.width;
|
||||||
|
offScreenCanvas.width = img.height;
|
||||||
|
|
||||||
|
// Rotate and draw source image into the off-screen canvas
|
||||||
|
offScreenCanvasCtx.rotate((90 * Math.PI) / 180);
|
||||||
|
offScreenCanvasCtx.translate(0, -offScreenCanvas.width);
|
||||||
|
offScreenCanvasCtx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const imageType = base64Image.match(/^data:(.+);base64/)?.[1] || "image/jpeg";
|
||||||
|
|
||||||
|
// Encode image to data-uri with base64
|
||||||
|
try {
|
||||||
|
form.photos[index].fileBase64 = offScreenCanvas.toDataURL(imageType, 100);
|
||||||
|
form.photos[index].file = dataURLtoFile(form.photos[index].fileBase64, form.photos[index].photoName);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to process rotated image");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
// Clean up resources
|
||||||
|
offScreenCanvas.width = 0;
|
||||||
|
offScreenCanvas.height = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user