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:
@@ -13,7 +13,7 @@
|
|||||||
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent"
|
||||||
|
|
||||||
import { getAllPluginSettings } from "@/plugins"
|
import { getAllPluginSettings } from "@/plugins"
|
||||||
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting } from "@/plugins/core/types"
|
import type { BooleanSetting, StringSetting, NumberSetting, SelectSetting, ButtonSetting, HotkeySetting, ComponentSetting } from "@/plugins/core/types"
|
||||||
|
|
||||||
// Union type representing all possible settings
|
// Union type representing all possible settings
|
||||||
type SettingType =
|
type SettingType =
|
||||||
@@ -32,6 +32,11 @@
|
|||||||
(Omit<HotkeySetting, 'type'> & {
|
(Omit<HotkeySetting, 'type'> & {
|
||||||
type: 'hotkey',
|
type: 'hotkey',
|
||||||
id: string
|
id: string
|
||||||
|
}) |
|
||||||
|
(Omit<ComponentSetting, 'type'> & {
|
||||||
|
type: 'component',
|
||||||
|
id: string,
|
||||||
|
component: any
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Plugin {
|
interface Plugin {
|
||||||
@@ -55,7 +60,11 @@
|
|||||||
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {};
|
||||||
|
|
||||||
for (const [key, setting] of Object.entries(plugin.settings)) {
|
for (const [key, setting] of Object.entries(plugin.settings)) {
|
||||||
if (pluginSettingsValues[plugin.pluginId][key] === undefined && setting.type !== 'button') {
|
if (
|
||||||
|
pluginSettingsValues[plugin.pluginId][key] === undefined &&
|
||||||
|
setting.type !== 'button' &&
|
||||||
|
setting.type !== 'component'
|
||||||
|
) {
|
||||||
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
pluginSettingsValues[plugin.pluginId][key] = setting.default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,6 +277,8 @@
|
|||||||
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default}
|
||||||
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)}
|
||||||
/>
|
/>
|
||||||
|
{:else if setting.type === 'component'}
|
||||||
|
<svelte:component this={setting.component} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
StringSetting,
|
StringSetting,
|
||||||
ButtonSetting,
|
ButtonSetting,
|
||||||
HotkeySetting,
|
HotkeySetting,
|
||||||
|
ComponentSetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { createPluginAPI } from "./createAPI";
|
import { createPluginAPI } from "./createAPI";
|
||||||
import browser from "webextension-polyfill";
|
import browser from "webextension-polyfill";
|
||||||
@@ -195,7 +196,8 @@ export class PluginManager {
|
|||||||
options: Array<{ value: string; label: string }>;
|
options: Array<{ value: string; label: string }>;
|
||||||
})
|
})
|
||||||
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
|
| (Omit<ButtonSetting, "type"> & { type: "button"; id: string; trigger?: () => void | Promise<void> })
|
||||||
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string });
|
| (Omit<HotkeySetting, "type"> & { type: "hotkey"; id: string })
|
||||||
|
| (Omit<ComponentSetting, "type"> & { type: "component"; id: string; component: any });
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
return Array.from(this.plugins.entries()).map(([id, plugin]) => {
|
||||||
@@ -203,8 +205,8 @@ export class PluginManager {
|
|||||||
([key, setting]) => {
|
([key, setting]) => {
|
||||||
const settingObj = setting as any;
|
const settingObj = setting as any;
|
||||||
let result: any;
|
let result: any;
|
||||||
if (settingObj.type === "button") {
|
if (settingObj.type === "button" || settingObj.type === "component") {
|
||||||
// For button, keep the trigger function
|
// For button or component, keep the functions
|
||||||
result = { ...settingObj };
|
result = { ...settingObj };
|
||||||
} else {
|
} else {
|
||||||
// For others, strip functions
|
// For others, strip functions
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
SelectSetting,
|
SelectSetting,
|
||||||
StringSetting,
|
StringSetting,
|
||||||
HotkeySetting,
|
HotkeySetting,
|
||||||
|
ComponentSetting,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export function numberSetting(
|
export function numberSetting(
|
||||||
@@ -52,6 +53,15 @@ export function buttonSetting(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function componentSetting(
|
||||||
|
options: Omit<ComponentSetting, "type">,
|
||||||
|
): ComponentSetting {
|
||||||
|
return {
|
||||||
|
type: "component",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function hotkeySetting(
|
export function hotkeySetting(
|
||||||
options: Omit<HotkeySetting, "type">,
|
options: Omit<HotkeySetting, "type">,
|
||||||
): HotkeySetting {
|
): HotkeySetting {
|
||||||
|
|||||||
@@ -48,13 +48,21 @@ export interface HotkeySetting {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ComponentSetting {
|
||||||
|
type: "component";
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
component: any;
|
||||||
|
}
|
||||||
|
|
||||||
export type PluginSetting =
|
export type PluginSetting =
|
||||||
| BooleanSetting
|
| BooleanSetting
|
||||||
| StringSetting
|
| StringSetting
|
||||||
| NumberSetting
|
| NumberSetting
|
||||||
| SelectSetting<string>
|
| SelectSetting<string>
|
||||||
| ButtonSetting
|
| ButtonSetting
|
||||||
| HotkeySetting;
|
| HotkeySetting
|
||||||
|
| ComponentSetting;
|
||||||
|
|
||||||
export type PluginSettings = {
|
export type PluginSettings = {
|
||||||
[key: string]: PluginSetting;
|
[key: string]: PluginSetting;
|
||||||
@@ -71,6 +79,8 @@ export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
|
|||||||
? O
|
? O
|
||||||
: T extends HotkeySetting
|
: T extends HotkeySetting
|
||||||
? string
|
? string
|
||||||
|
: T extends ComponentSetting
|
||||||
|
? never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
export type SettingsAPI<T extends PluginSettings> = {
|
export type SettingsAPI<T extends PluginSettings> = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import themesPlugin from "./built-in/themes";
|
|||||||
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
import animatedBackgroundPlugin from "./built-in/animatedBackground";
|
||||||
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
import assessmentsAveragePlugin from "./built-in/assessmentsAverage";
|
||||||
import globalSearchPlugin from "./built-in/globalSearch/src/core";
|
import globalSearchPlugin from "./built-in/globalSearch/src/core";
|
||||||
|
import profilePicturePlugin from "./built-in/profilePicture";
|
||||||
//import testPlugin from './built-in/test';
|
//import testPlugin from './built-in/test';
|
||||||
|
|
||||||
// Initialize plugin manager
|
// Initialize plugin manager
|
||||||
@@ -19,6 +20,7 @@ pluginManager.registerPlugin(assessmentsAveragePlugin);
|
|||||||
pluginManager.registerPlugin(notificationCollectorPlugin);
|
pluginManager.registerPlugin(notificationCollectorPlugin);
|
||||||
pluginManager.registerPlugin(timetablePlugin);
|
pluginManager.registerPlugin(timetablePlugin);
|
||||||
pluginManager.registerPlugin(globalSearchPlugin);
|
pluginManager.registerPlugin(globalSearchPlugin);
|
||||||
|
pluginManager.registerPlugin(profilePicturePlugin);
|
||||||
//pluginManager.registerPlugin(testPlugin);
|
//pluginManager.registerPlugin(testPlugin);
|
||||||
|
|
||||||
export { init as Monofile } from "./monofile";
|
export { init as Monofile } from "./monofile";
|
||||||
|
|||||||
Reference in New Issue
Block a user