feat: profile picture plugin #256

This commit is contained in:
SethBurkart123
2025-06-04 16:08:01 +10:00
parent 6c12f5cf00
commit 9b13e7571a
8 changed files with 246 additions and 17 deletions
@@ -0,0 +1,99 @@
<script lang="ts">
import localforage from 'localforage'
let value = $state<string | undefined>(undefined)
let fileInput = $state<HTMLInputElement | undefined>(undefined)
let dragging = $state(false)
let blobUrl = $state<string | undefined>(undefined)
// Setup localforage instance
const store = localforage.createInstance({
name: 'profile-picture-store',
storeName: 'profilePicture',
})
async function load() {
const blob = await store.getItem<Blob>('profile-picture')
if (blob && blob instanceof Blob) {
// Revoke old blobUrl if any
if (blobUrl) URL.revokeObjectURL(blobUrl)
blobUrl = URL.createObjectURL(blob)
value = blobUrl
} else {
value = undefined
}
}
load()
function triggerSelect() {
fileInput?.click()
}
async function handleFiles(files: FileList | null) {
const file = files?.[0]
if (!file) return
// Revoke old blob URL if it exists
if (blobUrl) {
URL.revokeObjectURL(blobUrl)
}
// Store the blob in localforage
await store.setItem('profile-picture', file)
const newBlobUrl = URL.createObjectURL(file)
value = newBlobUrl
blobUrl = newBlobUrl
}
function onFileChange() {
handleFiles(fileInput?.files || null)
}
function onDrop(event: DragEvent) {
event.preventDefault()
dragging = false
handleFiles(event.dataTransfer?.files || null)
}
async function removeImage() {
if (blobUrl) {
URL.revokeObjectURL(blobUrl)
blobUrl = undefined
}
value = undefined
await store.removeItem('profile-picture')
}
</script>
<div
class="flex relative justify-center items-center rounded-lg cursor-pointer select-none border-zinc-300 dark:border-zinc-600 bg-white/20 dark:bg-zinc-800/30"
onclick={() => value ? null : triggerSelect()}
ondragover={(e) => { e.stopPropagation(); dragging = true }}
ondragleave={() => dragging = false}
ondrop={onDrop}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
triggerSelect()
}
}}
role="button"
tabindex="0"
>
{#if value}
<img src={value} alt="Profile" class="object-cover rounded-full size-10" />
<button
class="flex justify-center items-center m-1 text-lg dark:text-white size-7"
onclick={removeImage}
>&#215;</button>
{:else}
<div class="flex gap-2 items-center px-3 py-1 text-xs rounded-lg border border-dashed transition border-zinc-300 dark:border-zinc-600 text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300">
<span class="text-lg font-IconFamily">{'\ued47'}</span>
<span>Upload</span>
</div>
{/if}
<input type="file" accept="image/*" class="hidden" bind:this={fileInput} onchange={onFileChange} />
{#if dragging}
<div class="absolute inset-0 rounded-full bg-zinc-200/40 dark:bg-zinc-700/40"></div>
{/if}
</div>
@@ -0,0 +1,85 @@
import type { Plugin } from "@/plugins/core/types";
import { defineSettings, componentSetting } from "@/plugins/core/settingsHelpers";
import ProfilePictureSetting from "./ProfilePictureSetting.svelte";
import { waitForElm } from "@/seqta/utils/waitForElm";
import styles from "./styles.css?inline";
import localforage from "localforage";
const settings = defineSettings({
picture: componentSetting({
title: "Profile Picture",
description: "Upload or remove your custom profile image",
component: ProfilePictureSetting,
}),
});
const profilePicturePlugin: Plugin<typeof settings> = {
id: "profile-picture",
name: "Custom Profile Picture",
description: "Use your own image in place of the profile icon",
version: "1.1.0",
settings: settings,
disableToggle: true,
defaultEnabled: false,
styles,
run: async (api) => {
await api.storage.loaded;
let container: Element;
try {
container = await waitForElm(".userInfosvgdiv", true, 100, 60);
} catch {
return () => {};
}
const svg = container.querySelector(".userInfosvg") as HTMLElement | null;
let img: HTMLImageElement | null = null;
let currentBlobUrl: string | undefined;
// Setup localforage instance
const store = localforage.createInstance({
name: "profile-picture-store",
storeName: "profilePicture",
});
async function updateImageFromStore() {
// Remove old image if present
if (img) {
img.remove();
img = null;
}
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = undefined;
}
const blob = await store.getItem<Blob>("profile-picture");
if (blob && blob instanceof Blob) {
currentBlobUrl = URL.createObjectURL(blob);
img = document.createElement("img");
img.className = "userInfoImg";
img.src = currentBlobUrl;
if (svg) svg.style.display = "none";
container.appendChild(img);
} else {
if (svg) svg.style.display = "";
}
}
// Initial load
await updateImageFromStore();
// Listen for storage changes (in case user updates from settings)
const interval = setInterval(updateImageFromStore, 1000);
return () => {
clearInterval(interval);
if (img) img.remove();
if (svg) svg.style.display = "";
if (currentBlobUrl) URL.revokeObjectURL(currentBlobUrl);
};
},
};
export default profilePicturePlugin;
@@ -0,0 +1,10 @@
.userInfoImg {
width: 80%;
height: 80%;
position: absolute;
top: 10%;
left: 10%;
border-radius: 50%;
object-fit: cover;
z-index: 4;
}