mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
feat: profile picture plugin #256
This commit is contained in:
@@ -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}
|
||||
>×</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;
|
||||
}
|
||||
Reference in New Issue
Block a user