From 3b50bb587718a9f35ebcd96bdd2231f3bd7692e0 Mon Sep 17 00:00:00 2001 From: April Hall Date: Tue, 4 Mar 2025 12:10:45 -0500 Subject: [PATCH] feat: Avatar cropping --- TODO.md | 1 + bun.lockb | Bin 241719 -> 241794 bytes package.json | 9 +- src/lib/components/forms/updatePFP.svelte | 52 ++++-- .../ui/avatar/avatar-fallback.svelte | 14 ++ .../components/ui/avatar/avatar-image.svelte | 14 ++ src/lib/components/ui/avatar/avatar.svelte | 14 ++ src/lib/components/ui/avatar/index.ts | 19 +++ .../image-cropper/image-cropper-cancel.svelte | 21 +++ .../image-cropper-controls.svelte | 16 ++ .../image-cropper/image-cropper-crop.svelte | 21 +++ .../image-cropper-cropper.svelte | 19 +++ .../image-cropper/image-cropper-dialog.svelte | 24 +++ .../image-cropper-preview.svelte | 29 ++++ .../image-cropper-upload-trigger.svelte | 18 ++ .../ui/image-cropper/image-cropper.svelte | 41 +++++ .../ui/image-cropper/image-cropper.svelte.ts | 157 ++++++++++++++++++ src/lib/components/ui/image-cropper/index.ts | 17 ++ src/lib/components/ui/image-cropper/types.ts | 22 +++ src/lib/components/ui/image-cropper/utils.ts | 87 ++++++++++ src/lib/utils/box.ts | 17 ++ src/lib/utils/utils.ts | 12 ++ src/routes/(main)/account/+page.svelte | 2 +- 23 files changed, 606 insertions(+), 20 deletions(-) create mode 100644 src/lib/components/ui/avatar/avatar-fallback.svelte create mode 100644 src/lib/components/ui/avatar/avatar-image.svelte create mode 100644 src/lib/components/ui/avatar/avatar.svelte create mode 100644 src/lib/components/ui/avatar/index.ts create mode 100644 src/lib/components/ui/image-cropper/image-cropper-cancel.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-controls.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-crop.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-cropper.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-dialog.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-preview.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper-upload-trigger.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper.svelte create mode 100644 src/lib/components/ui/image-cropper/image-cropper.svelte.ts create mode 100644 src/lib/components/ui/image-cropper/index.ts create mode 100644 src/lib/components/ui/image-cropper/types.ts create mode 100644 src/lib/components/ui/image-cropper/utils.ts create mode 100644 src/lib/utils/box.ts create mode 100644 src/lib/utils/utils.ts diff --git a/TODO.md b/TODO.md index 2d0e27e..9042aa3 100644 --- a/TODO.md +++ b/TODO.md @@ -4,6 +4,7 @@ A list of all tasks that need to be completed in the app
A more complex version of this list is available [here](https://trello.com/b/kJw6Aapn/svchat). - [x] Account / Profile management +- [x] Avatar cropping - [ ] Channel context menus - [x] Containerization with docker and docker-compose - [ ] Editing messages diff --git a/bun.lockb b/bun.lockb index 4aba811dfba5dedd3f029e12d4aa474581589ca5..f7d3f0cc7f2a472cb396dbfcf4aab154f14737fc 100755 GIT binary patch delta 38159 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 diff --git a/package.json b/package.json index 83de4a3..92fe675 100644 --- a/package.json +++ b/package.json @@ -49,21 +49,24 @@ "autoprefixer": "^10.4.20", "better-auth": "^1.1.16", "better-sqlite3": "^11.8.1", - "bits-ui": "^1.3.5", + "bits-ui": "1.3.2", "cassandra-driver": "^4.7.2", "dotenv": "^16.4.7", "express": "^4.21.2", - "lucide-svelte": "^0.477.0", + "lucide-svelte": "^0.475.0", "markdown-it": "^14.1.0", "markdown-it-highlightjs": "^4.2.0", "markdown-it-link-attributes": "^4.0.1", "minio": "^8.0.4", "mode-watcher": "^0.5.1", + "runed": "^0.23.4", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", + "svelte-easy-crop": "^4.0.0", "svelte-radix": "^2.0.1", + "svelte-toolbelt": "^0.7.1", "sveltekit-superforms": "^2.23.1", - "tailwind-merge": "^3.0.2", + "tailwind-merge": "^2.6.0", "tailwind-variants": "^0.3.1", "tailwindcss": "^3.4.17", "tsm": "^2.3.0", diff --git a/src/lib/components/forms/updatePFP.svelte b/src/lib/components/forms/updatePFP.svelte index 0721fc0..9aeaed7 100644 --- a/src/lib/components/forms/updatePFP.svelte +++ b/src/lib/components/forms/updatePFP.svelte @@ -1,29 +1,49 @@ -
-
+ +
Upload Profile Image - - + { + const file = await getFileFromUrl(url); + submit(file); + }} + > +
+ + +
+ +
+
+
+ + + + + + + +
diff --git a/src/lib/components/ui/avatar/avatar-fallback.svelte b/src/lib/components/ui/avatar/avatar-fallback.svelte new file mode 100644 index 0000000..408bf49 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-fallback.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ui/avatar/avatar-image.svelte b/src/lib/components/ui/avatar/avatar-image.svelte new file mode 100644 index 0000000..d4905dd --- /dev/null +++ b/src/lib/components/ui/avatar/avatar-image.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ui/avatar/avatar.svelte b/src/lib/components/ui/avatar/avatar.svelte new file mode 100644 index 0000000..bb48088 --- /dev/null +++ b/src/lib/components/ui/avatar/avatar.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ui/avatar/index.ts b/src/lib/components/ui/avatar/index.ts new file mode 100644 index 0000000..62b5f28 --- /dev/null +++ b/src/lib/components/ui/avatar/index.ts @@ -0,0 +1,19 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import Root from './avatar.svelte'; +import Image from './avatar-image.svelte'; +import Fallback from './avatar-fallback.svelte'; + +export { + Root, + Image, + Fallback, + // + Root as Avatar, + Image as AvatarImage, + Fallback as AvatarFallback, +}; diff --git a/src/lib/components/ui/image-cropper/image-cropper-cancel.svelte b/src/lib/components/ui/image-cropper/image-cropper-cancel.svelte new file mode 100644 index 0000000..4d4d233 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-cancel.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/image-cropper/image-cropper-controls.svelte b/src/lib/components/ui/image-cropper/image-cropper-controls.svelte new file mode 100644 index 0000000..58d2f5e --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-controls.svelte @@ -0,0 +1,16 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/image-cropper/image-cropper-crop.svelte b/src/lib/components/ui/image-cropper/image-cropper-crop.svelte new file mode 100644 index 0000000..cd66895 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-crop.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/lib/components/ui/image-cropper/image-cropper-cropper.svelte b/src/lib/components/ui/image-cropper/image-cropper-cropper.svelte new file mode 100644 index 0000000..cd05d8c --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-cropper.svelte @@ -0,0 +1,19 @@ + + + + + +
+ +
diff --git a/src/lib/components/ui/image-cropper/image-cropper-dialog.svelte b/src/lib/components/ui/image-cropper/image-cropper-dialog.svelte new file mode 100644 index 0000000..1a07ce5 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-dialog.svelte @@ -0,0 +1,24 @@ + + + + + + +
+ {@render children?.()} +
+
+
diff --git a/src/lib/components/ui/image-cropper/image-cropper-preview.svelte b/src/lib/components/ui/image-cropper/image-cropper-preview.svelte new file mode 100644 index 0000000..e843cca --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-preview.svelte @@ -0,0 +1,29 @@ + + + + +{#if child} + {@render child({ src: previewState.rootState.src })} +{:else} + + + + + Upload image + + +{/if} diff --git a/src/lib/components/ui/image-cropper/image-cropper-upload-trigger.svelte b/src/lib/components/ui/image-cropper/image-cropper-upload-trigger.svelte new file mode 100644 index 0000000..69a72c8 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper-upload-trigger.svelte @@ -0,0 +1,18 @@ + + + + + diff --git a/src/lib/components/ui/image-cropper/image-cropper.svelte b/src/lib/components/ui/image-cropper/image-cropper.svelte new file mode 100644 index 0000000..320a658 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper.svelte @@ -0,0 +1,41 @@ + + + + +{@render children?.()} + { + const file = e.currentTarget.files?.[0]; + if (!file) return; + rootState.onUpload(file); + // reset so that we can reupload the same file + (e.target! as HTMLInputElement).value = ''; + }} + type="file" + {id} + style="display: none;" +/> diff --git a/src/lib/components/ui/image-cropper/image-cropper.svelte.ts b/src/lib/components/ui/image-cropper/image-cropper.svelte.ts new file mode 100644 index 0000000..1bd9587 --- /dev/null +++ b/src/lib/components/ui/image-cropper/image-cropper.svelte.ts @@ -0,0 +1,157 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import type { ReadableBoxedValues, WritableBoxedValues } from '$lib/utils/box'; +import { Context } from 'runed'; +import type { CropArea, DispatchEvents } from 'svelte-easy-crop'; +import { getCroppedImg } from './utils'; + +export type ImageCropperRootProps = WritableBoxedValues<{ + src: string; +}> & + ReadableBoxedValues<{ + id: string; + }> & { + onCropped: (url: string) => void; + }; + +class ImageCropperRootState { + #createdUrls = $state([]); + open = $state(false); + tempUrl = $state(); + pixelCrop = $state(); + + constructor(readonly opts: ImageCropperRootProps) { + this.onUpload = this.onUpload.bind(this); + this.onCancel = this.onCancel.bind(this); + this.onCrop = this.onCrop.bind(this); + this.dispose = this.dispose.bind(this); + } + + onUpload(file: File) { + this.tempUrl = URL.createObjectURL(file); + this.#createdUrls.push(this.tempUrl); + this.open = true; + } + + onCancel() { + this.tempUrl = undefined; + this.open = false; + this.pixelCrop = undefined; + } + + async onCrop() { + if (!this.pixelCrop || !this.tempUrl) return; + + this.opts.src.current = await getCroppedImg(this.tempUrl, this.pixelCrop); + + this.open = false; + + this.opts.onCropped(this.opts.src.current); + } + + get src() { + return this.opts.src.current; + } + + get id() { + return this.opts.id.current; + } + + dispose() { + for (const url of this.#createdUrls) { + URL.revokeObjectURL(url); + } + } +} + +export type ImageCropperTriggerProps = ReadableBoxedValues<{ + id?: string; +}>; + +class ImageCropperTriggerState { + constructor(readonly rootState: ImageCropperRootState) {} +} + +class ImageCropperPreviewState { + constructor(readonly rootState: ImageCropperRootState) {} +} + +class ImageCropperDialogState { + constructor(readonly rootState: ImageCropperRootState) {} +} + +class ImageCropperCropperState { + constructor(readonly rootState: ImageCropperRootState) { + this.onCropComplete = this.onCropComplete.bind(this); + } + + onCropComplete(e: DispatchEvents['cropcomplete']) { + this.rootState.pixelCrop = e.pixels; + } +} + +class ImageCropperCropState { + constructor(readonly rootState: ImageCropperRootState) { + this.onclick = this.onclick.bind(this); + } + + onclick() { + this.rootState.onCrop(); + } +} + +class ImageCropperCancelState { + constructor(readonly rootState: ImageCropperRootState) { + this.onclick = this.onclick.bind(this); + } + + onclick() { + this.rootState.onCancel(); + } +} + +const ImageCropperRootContext = new Context('ImageCropper.Root'); + +export const useImageCropperRoot = (props: ImageCropperRootProps) => { + return ImageCropperRootContext.set(new ImageCropperRootState(props)); +}; + +export const useImageCropperTrigger = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperTriggerState(rootState); +}; + +export const useImageCropperPreview = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperPreviewState(rootState); +}; + +export const useImageCropperDialog = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperDialogState(rootState); +}; + +export const useImageCropperCropper = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperCropperState(rootState); +}; + +export const useImageCropperCrop = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperCropState(rootState); +}; + +export const useImageCropperCancel = () => { + const rootState = ImageCropperRootContext.get(); + + return new ImageCropperCancelState(rootState); +}; diff --git a/src/lib/components/ui/image-cropper/index.ts b/src/lib/components/ui/image-cropper/index.ts new file mode 100644 index 0000000..f2bf559 --- /dev/null +++ b/src/lib/components/ui/image-cropper/index.ts @@ -0,0 +1,17 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import Root from './image-cropper.svelte'; +import UploadTrigger from './image-cropper-upload-trigger.svelte'; +import Preview from './image-cropper-preview.svelte'; +import Dialog from './image-cropper-dialog.svelte'; +import Cropper from './image-cropper-cropper.svelte'; +import Controls from './image-cropper-controls.svelte'; +import Crop from './image-cropper-crop.svelte'; +import Cancel from './image-cropper-cancel.svelte'; +import { getFileFromUrl } from './utils'; + +export { Root, UploadTrigger, Preview, Dialog, Cropper, Controls, Crop, Cancel, getFileFromUrl }; diff --git a/src/lib/components/ui/image-cropper/types.ts b/src/lib/components/ui/image-cropper/types.ts new file mode 100644 index 0000000..6059e96 --- /dev/null +++ b/src/lib/components/ui/image-cropper/types.ts @@ -0,0 +1,22 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import type { AvatarRootProps, DialogContentProps, WithChildren } from 'bits-ui'; +import type { Snippet } from 'svelte'; +import type { HTMLInputAttributes } from 'svelte/elements'; + +export type ImageCropperRootProps = HTMLInputAttributes & + WithChildren<{ + id?: string; + src?: string; + onCropped?: (url: string) => void; + }>; + +export type ImageCropperDialogProps = DialogContentProps; + +export type ImageCropperPreviewProps = Omit & { + child?: Snippet<[{ src: string }]>; +}; diff --git a/src/lib/components/ui/image-cropper/utils.ts b/src/lib/components/ui/image-cropper/utils.ts new file mode 100644 index 0000000..bff2c75 --- /dev/null +++ b/src/lib/components/ui/image-cropper/utils.ts @@ -0,0 +1,87 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import type { CropArea } from 'svelte-easy-crop'; + +export const getFileFromUrl = async (url: string, fileName = 'cropped.png'): Promise => { + // Fetch the file data from the URL + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch resource: ${response.status} ${response.statusText}`); + } + + // Convert the response into a Blob + const blob = await response.blob(); + + // Create and return a File. You can set a custom type if needed. + return new File([blob], fileName, { type: blob.type }); +}; + +const createImage = (url: string): Promise => { + return new Promise((resolve, reject) => { + const image = new Image(); + image.addEventListener('load', () => resolve(image)); + image.addEventListener('error', (error) => reject(error)); + image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox + image.src = url; + }); +}; + +const getRadianAngle = (degreeValue: number) => { + return (degreeValue * Math.PI) / 180; +}; + +/** Gets the cropped image from the src using the cropped area + * + * @param imageSrc + * @param pixelCrop + * @param rotation + * @returns + */ +export const getCroppedImg = async (imageSrc: string, pixelCrop: CropArea, rotation = 0): Promise => { + const image = await createImage(imageSrc); + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Error getting 2d rendering context'); + } + + const maxSize = Math.max(image.width, image.height); + const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); + + // set each dimensions to double largest dimension to allow for a safe area for the + // image to rotate in without being clipped by canvas context + canvas.width = safeArea; + canvas.height = safeArea; + + // translate canvas context to a central location on image to allow rotating around the center. + ctx.translate(safeArea / 2, safeArea / 2); + ctx.rotate(getRadianAngle(rotation)); + ctx.translate(-safeArea / 2, -safeArea / 2); + + // draw rotated image and store data. + ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5); + const data = ctx.getImageData(0, 0, safeArea, safeArea); + + // set canvas width to final desired crop size - this will clear existing context + canvas.width = pixelCrop.width; + canvas.height = pixelCrop.height; + + // paste generated rotate image with correct offsets for x,y crop values. + ctx.putImageData( + data, + Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), + Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y), + ); + + return new Promise((resolve) => { + canvas.toBlob((file) => { + resolve(URL.createObjectURL(file!)); + }, 'image/png'); + }); +}; diff --git a/src/lib/utils/box.ts b/src/lib/utils/box.ts new file mode 100644 index 0000000..f4aef79 --- /dev/null +++ b/src/lib/utils/box.ts @@ -0,0 +1,17 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import type { ReadableBox, WritableBox } from 'svelte-toolbelt'; + +export type Box = ReadableBox | WritableBox; + +export type WritableBoxedValues = { + [K in keyof T]: WritableBox; +}; + +export type ReadableBoxedValues = { + [K in keyof T]: ReadableBox; +}; diff --git a/src/lib/utils/utils.ts b/src/lib/utils/utils.ts new file mode 100644 index 0000000..fc28227 --- /dev/null +++ b/src/lib/utils/utils.ts @@ -0,0 +1,12 @@ +/* + jsrepo 1.41.3 + Installed from github/ieedan/shadcn-svelte-extras + 3-4-2025 +*/ + +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/src/routes/(main)/account/+page.svelte b/src/routes/(main)/account/+page.svelte index 4a9a070..1d695c4 100644 --- a/src/routes/(main)/account/+page.svelte +++ b/src/routes/(main)/account/+page.svelte @@ -20,7 +20,7 @@
- +