| | <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> |
| |
|