mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 12:25:20 +00:00
refac: edit user modal
This commit is contained in:
parent
aee7aaae68
commit
51a138aec3
3 changed files with 258 additions and 235 deletions
|
|
@ -12,6 +12,7 @@
|
||||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
|
import UserProfileImage from '$lib/components/chat/Settings/Account/UserProfileImage.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
@ -83,120 +84,118 @@
|
||||||
submitHandler();
|
submitHandler();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class=" flex items-center rounded-md px-5 py-2 w-full">
|
<div class=" px-5 pt-3 pb-5 w-full">
|
||||||
<div class=" self-center mr-5">
|
<div class="flex self-center w-full">
|
||||||
<img
|
<div class=" self-start h-full mr-6">
|
||||||
src={selectedUser.profile_image_url}
|
<UserProfileImage bind:profileImageUrl={_user.profile_image_url} user={_user} />
|
||||||
class=" max-w-[55px] object-cover rounded-full"
|
|
||||||
alt="User profile"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="overflow-hidden w-full">
|
|
||||||
<div class=" self-center capitalize font-medium truncate">{selectedUser.name}</div>
|
|
||||||
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
{$i18n.t('Created at')}
|
|
||||||
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" px-5 pt-3 pb-5">
|
|
||||||
<div class=" flex flex-col space-y-1.5">
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<select
|
|
||||||
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
|
||||||
bind:value={_user.role}
|
|
||||||
disabled={_user.id == sessionUser.id}
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="admin">{$i18n.t('Admin')}</option>
|
|
||||||
<option value="user">{$i18n.t('User')}</option>
|
|
||||||
<option value="pending">{$i18n.t('Pending')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if userGroups}
|
<div class=" flex-1">
|
||||||
<div class="flex flex-col w-full text-sm">
|
<div class="overflow-hidden w-ful mb-2">
|
||||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('User Groups')}</div>
|
<div class=" self-center capitalize font-medium truncate">
|
||||||
|
{selectedUser.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if userGroups.length}
|
<div class="text-xs text-gray-500">
|
||||||
<div class="flex flex-wrap gap-1 my-0.5 -mx-1">
|
{$i18n.t('Created at')}
|
||||||
{#each userGroups as userGroup}
|
{dayjs(selectedUser.created_at * 1000).format('LL')}
|
||||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
|
|
||||||
<a
|
|
||||||
href={'/admin/users/groups?id=' + userGroup.id}
|
|
||||||
on:click|preventDefault={() =>
|
|
||||||
goto('/admin/users/groups?id=' + userGroup.id)}
|
|
||||||
>
|
|
||||||
{userGroup.name}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<span>-</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
|
||||||
type="text"
|
|
||||||
bind:value={_user.name}
|
|
||||||
placeholder={$i18n.t('Enter Your Name')}
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1">
|
|
||||||
<input
|
|
||||||
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
|
||||||
type="email"
|
|
||||||
bind:value={_user.email}
|
|
||||||
placeholder={$i18n.t('Enter Your Email')}
|
|
||||||
autocomplete="off"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if _user?.oauth_sub}
|
|
||||||
<div class="flex flex-col w-full">
|
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('OAuth ID')}</div>
|
|
||||||
|
|
||||||
<div class="flex-1 text-sm break-all mb-1">
|
|
||||||
{_user.oauth_sub ?? ''}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-col w-full">
|
<div class=" flex flex-col space-y-1.5">
|
||||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
{#if (userGroups ?? []).length > 0}
|
||||||
|
<div class="flex flex-col w-full text-sm">
|
||||||
|
<div class="mb-1 text-xs text-gray-500">{$i18n.t('User Groups')}</div>
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex flex-wrap gap-1 my-0.5 -mx-1">
|
||||||
<SensitiveInput
|
{#each userGroups as userGroup}
|
||||||
class="w-full text-sm bg-transparent outline-hidden"
|
<span
|
||||||
type="password"
|
class="px-1.5 py-0.5 rounded-xl bg-gray-100 dark:bg-gray-850 text-xs"
|
||||||
placeholder={$i18n.t('Enter New Password')}
|
>
|
||||||
bind:value={_user.password}
|
<a
|
||||||
autocomplete="new-password"
|
href={'/admin/users/groups?id=' + userGroup.id}
|
||||||
required={false}
|
on:click|preventDefault={() =>
|
||||||
/>
|
goto('/admin/users/groups?id=' + userGroup.id)}
|
||||||
|
>
|
||||||
|
{userGroup.name}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<select
|
||||||
|
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
|
bind:value={_user.role}
|
||||||
|
disabled={_user.id == sessionUser.id}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="admin">{$i18n.t('Admin')}</option>
|
||||||
|
<option value="user">{$i18n.t('User')}</option>
|
||||||
|
<option value="pending">{$i18n.t('Pending')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
|
type="text"
|
||||||
|
bind:value={_user.name}
|
||||||
|
placeholder={$i18n.t('Enter Your Name')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
|
||||||
|
type="email"
|
||||||
|
bind:value={_user.email}
|
||||||
|
placeholder={$i18n.t('Enter Your Email')}
|
||||||
|
autocomplete="off"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if _user?.oauth_sub}
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('OAuth ID')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1 text-sm break-all mb-1">
|
||||||
|
{_user.oauth_sub ?? ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col w-full">
|
||||||
|
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<SensitiveInput
|
||||||
|
class="w-full text-sm bg-transparent outline-hidden"
|
||||||
|
type="password"
|
||||||
|
placeholder={$i18n.t('Enter New Password')}
|
||||||
|
bind:value={_user.password}
|
||||||
|
autocomplete="new-password"
|
||||||
|
required={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@
|
||||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||||
import { getUserById } from '$lib/apis/users';
|
import { getUserById } from '$lib/apis/users';
|
||||||
|
import User from '$lib/components/icons/User.svelte';
|
||||||
|
import UserProfileImage from './Account/UserProfileImage.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
|
|
@ -118,68 +120,6 @@
|
||||||
|
|
||||||
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
||||||
<div class=" overflow-y-scroll max-h-[28rem] md:max-h-full">
|
<div class=" overflow-y-scroll max-h-[28rem] md:max-h-full">
|
||||||
<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');
|
|
||||||
|
|
||||||
// Calculate the aspect ratio of the image
|
|
||||||
const aspectRatio = img.width / img.height;
|
|
||||||
|
|
||||||
// Calculate the new width and height to fit within 250x250
|
|
||||||
let newWidth, newHeight;
|
|
||||||
if (aspectRatio > 1) {
|
|
||||||
newWidth = 250 * aspectRatio;
|
|
||||||
newHeight = 250;
|
|
||||||
} else {
|
|
||||||
newWidth = 250;
|
|
||||||
newHeight = 250 / aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the canvas size
|
|
||||||
canvas.width = 250;
|
|
||||||
canvas.height = 250;
|
|
||||||
|
|
||||||
// Calculate the position to center the image
|
|
||||||
const offsetX = (250 - newWidth) / 2;
|
|
||||||
const offsetY = (250 - newHeight) / 2;
|
|
||||||
|
|
||||||
// Draw the image on the canvas
|
|
||||||
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
|
|
||||||
|
|
||||||
// Get the base64 representation of the compressed image
|
|
||||||
const compressedSrc = canvas.toDataURL('image/jpeg');
|
|
||||||
|
|
||||||
// Display the compressed image
|
|
||||||
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="space-y-1">
|
<div class="space-y-1">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-base font-medium">{$i18n.t('Your Account')}</div>
|
<div class="text-base font-medium">{$i18n.t('Your Account')}</div>
|
||||||
|
|
@ -192,73 +132,8 @@
|
||||||
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
|
<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> -->
|
||||||
|
|
||||||
<div class="flex space-x-5 my-4">
|
<div class="flex space-x-5 my-4">
|
||||||
<div class="flex flex-col self-start group">
|
<UserProfileImage bind:profileImageUrl user={$user} />
|
||||||
<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(name)}
|
|
||||||
alt="profile"
|
|
||||||
class=" rounded-full size-14 md:size-18 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"
|
|
||||||
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"
|
|
||||||
on:click={async () => {
|
|
||||||
if (canvasPixelTest()) {
|
|
||||||
profileImageUrl = generateInitialsImage(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"
|
|
||||||
on:click={async () => {
|
|
||||||
const url = await getGravatarUrl(localStorage.token, $user?.email);
|
|
||||||
|
|
||||||
profileImageUrl = url;
|
|
||||||
}}>{$i18n.t('Gravatar')}</button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<div class=" flex-1">
|
<div class=" flex-1">
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
|
|
|
||||||
149
src/lib/components/chat/Settings/Account/UserProfileImage.svelte
Normal file
149
src/lib/components/chat/Settings/Account/UserProfileImage.svelte
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
<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;
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
// Calculate the aspect ratio of the image
|
||||||
|
const aspectRatio = img.width / img.height;
|
||||||
|
|
||||||
|
// Calculate the new width and height to fit within 250x250
|
||||||
|
let newWidth, newHeight;
|
||||||
|
if (aspectRatio > 1) {
|
||||||
|
newWidth = 250 * aspectRatio;
|
||||||
|
newHeight = 250;
|
||||||
|
} else {
|
||||||
|
newWidth = 250;
|
||||||
|
newHeight = 250 / aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the canvas size
|
||||||
|
canvas.width = 250;
|
||||||
|
canvas.height = 250;
|
||||||
|
|
||||||
|
// Calculate the position to center the image
|
||||||
|
const offsetX = (250 - newWidth) / 2;
|
||||||
|
const offsetY = (250 - newHeight) / 2;
|
||||||
|
|
||||||
|
// Draw the image on the canvas
|
||||||
|
ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
|
||||||
|
|
||||||
|
// Get the base64 representation of the compressed image
|
||||||
|
const compressedSrc = canvas.toDataURL('image/jpeg');
|
||||||
|
|
||||||
|
// Display the compressed image
|
||||||
|
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 size-14 md:size-18 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>
|
||||||
Loading…
Reference in a new issue