| <script lang="ts"> |
| import { toast } from 'svelte-sonner'; |
| import { getContext } from 'svelte'; |
|
|
| const i18n = getContext('i18n'); |
|
|
| import { getGravatarUrl } from '$lib/apis/utils'; |
| import { canvasPixelTest, generateInitialsImage } from '$lib/utils'; |
|
|
| import { WEBUI_BASE_URL } from '$lib/constants'; |
|
|
| export let profileImageUrl; |
| export let user = null; |
|
|
| export let imageClassName = 'size-14 md:size-18'; |
|
|
| let profileImageInputElement; |
| </script> |
|
|
| <input |
| id="profile-image-input" |
| bind:this={profileImageInputElement} |
| type="file" |
| hidden |
| accept="image/*" |
| on:change={(e) => { |
| const files = profileImageInputElement.files ?? []; |
| let reader = new FileReader(); |
| reader.onload = (event) => { |
| let originalImageUrl = `${event.target.result}`; |
|
|
| const img = new Image(); |
| img.src = originalImageUrl; |
|
|
| img.onload = function () { |
| const canvas = document.createElement('canvas'); |
| const ctx = canvas.getContext('2d'); |
|
|
| |
| const aspectRatio = img.width / img.height; |
|
|
| |
| let newWidth, newHeight; |
| if (aspectRatio > 1) { |
| newWidth = 250 * aspectRatio; |
| newHeight = 250; |
| } else { |
| newWidth = 250; |
| newHeight = 250 / aspectRatio; |
| } |
|
|
| |
| canvas.width = 250; |
| canvas.height = 250; |
|
|
| |
| const offsetX = (250 - newWidth) / 2; |
| const offsetY = (250 - newHeight) / 2; |
|
|
| |
| ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); |
|
|
| |
| const compressedSrc = canvas.toDataURL('image/webp', 0.8); |
|
|
| |
| profileImageUrl = compressedSrc; |
|
|
| profileImageInputElement.files = null; |
| }; |
| }; |
|
|
| if ( |
| files.length > 0 && |
| ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type']) |
| ) { |
| reader.readAsDataURL(files[0]); |
| } |
| }} |
| /> |
|
|
| <div class="flex flex-col self-start group"> |
| <div class="self-center flex"> |
| <button |
| class="relative rounded-full dark:bg-gray-700" |
| type="button" |
| on:click={() => { |
| profileImageInputElement.click(); |
| }} |
| > |
| <img |
| src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(user?.name)} |
| alt="profile" |
| class=" rounded-full {imageClassName} object-cover" |
| /> |
|
|
| <div class="absolute bottom-0 right-0 opacity-0 group-hover:opacity-100 transition"> |
| <div class="p-1 rounded-full bg-white text-black border-gray-100 shadow"> |
| <svg |
| xmlns="http://www.w3.org/2000/svg" |
| viewBox="0 0 20 20" |
| fill="currentColor" |
| class="size-3" |
| > |
| <path |
| d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" |
| /> |
| </svg> |
| </div> |
| </div> |
| </button> |
| </div> |
| <div class="flex flex-col w-full justify-center mt-2"> |
| <button |
| class=" text-xs text-center text-gray-500 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all" |
| type="button" |
| on:click={async () => { |
| profileImageUrl = `${WEBUI_BASE_URL}/user.png`; |
| }}>{$i18n.t('Remove')}</button |
| > |
|
|
| <button |
| class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all" |
| type="button" |
| on:click={async () => { |
| if (canvasPixelTest()) { |
| profileImageUrl = generateInitialsImage(user?.name); |
| } else { |
| toast.info( |
| $i18n.t( |
| 'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.' |
| ), |
| { |
| duration: 1000 * 10 |
| } |
| ); |
| } |
| }}>{$i18n.t('Initials')}</button |
| > |
|
|
| <button |
| class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all" |
| type="button" |
| on:click={async () => { |
| const url = await getGravatarUrl(localStorage.token, user?.email); |
|
|
| profileImageUrl = url; |
| }}>{$i18n.t('Gravatar')}</button |
| > |
| </div> |
| </div> |
|
|