diff --git a/src/interface/pages/settings/general.svelte b/src/interface/pages/settings/general.svelte index c34a81fb..29dc85e8 100644 --- a/src/interface/pages/settings/general.svelte +++ b/src/interface/pages/settings/general.svelte @@ -13,25 +13,30 @@ import hideSensitiveContent from "@/seqta/ui/dev/hideSensitiveContent" 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 - type SettingType = + type SettingType = (Omit & { type: 'boolean', id: string }) | (Omit & { type: 'string', id: string }) | (Omit & { type: 'number', id: string }) | - (Omit, 'type'> & { - type: 'select', - id: string, + (Omit, 'type'> & { + type: 'select', + id: string, options: string[] }) | - (Omit & { - type: 'button', - id: string + (Omit & { + type: 'button', + id: string }) | - (Omit & { - type: 'hotkey', - id: string + (Omit & { + type: 'hotkey', + id: string + }) | + (Omit & { + type: 'component', + id: string, + component: any }); interface Plugin { @@ -55,7 +60,11 @@ pluginSettingsValues[plugin.pluginId] = stored[storageKey] || {}; 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; } } @@ -268,6 +277,8 @@ value={pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default} onChange={(value) => updatePluginSetting(plugin.pluginId, key, value)} /> + {:else if setting.type === 'component'} + {/if} diff --git a/src/plugins/built-in/profilePicture/ProfilePictureSetting.svelte b/src/plugins/built-in/profilePicture/ProfilePictureSetting.svelte new file mode 100644 index 00000000..1da2b571 --- /dev/null +++ b/src/plugins/built-in/profilePicture/ProfilePictureSetting.svelte @@ -0,0 +1,99 @@ + + +
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} + Profile + + {:else} +
+ {'\ued47'} + Upload +
+ {/if} + + {#if dragging} +
+ {/if} +
diff --git a/src/plugins/built-in/profilePicture/index.ts b/src/plugins/built-in/profilePicture/index.ts new file mode 100644 index 00000000..446dc21b --- /dev/null +++ b/src/plugins/built-in/profilePicture/index.ts @@ -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 = { + 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("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; + diff --git a/src/plugins/built-in/profilePicture/styles.css b/src/plugins/built-in/profilePicture/styles.css new file mode 100644 index 00000000..11030720 --- /dev/null +++ b/src/plugins/built-in/profilePicture/styles.css @@ -0,0 +1,10 @@ +.userInfoImg { + width: 80%; + height: 80%; + position: absolute; + top: 10%; + left: 10%; + border-radius: 50%; + object-fit: cover; + z-index: 4; +} diff --git a/src/plugins/core/manager.ts b/src/plugins/core/manager.ts index ff6621db..7e62b829 100644 --- a/src/plugins/core/manager.ts +++ b/src/plugins/core/manager.ts @@ -7,6 +7,7 @@ import type { StringSetting, ButtonSetting, HotkeySetting, + ComponentSetting, } from "./types"; import { createPluginAPI } from "./createAPI"; import browser from "webextension-polyfill"; @@ -195,7 +196,8 @@ export class PluginManager { options: Array<{ value: string; label: string }>; }) | (Omit & { type: "button"; id: string; trigger?: () => void | Promise }) - | (Omit & { type: "hotkey"; id: string }); + | (Omit & { type: "hotkey"; id: string }) + | (Omit & { type: "component"; id: string; component: any }); }; }> { return Array.from(this.plugins.entries()).map(([id, plugin]) => { @@ -203,8 +205,8 @@ export class PluginManager { ([key, setting]) => { const settingObj = setting as any; let result: any; - if (settingObj.type === "button") { - // For button, keep the trigger function + if (settingObj.type === "button" || settingObj.type === "component") { + // For button or component, keep the functions result = { ...settingObj }; } else { // For others, strip functions diff --git a/src/plugins/core/settingsHelpers.ts b/src/plugins/core/settingsHelpers.ts index e37bb2e6..7e34937a 100644 --- a/src/plugins/core/settingsHelpers.ts +++ b/src/plugins/core/settingsHelpers.ts @@ -5,6 +5,7 @@ import type { SelectSetting, StringSetting, HotkeySetting, + ComponentSetting, } from "./types"; export function numberSetting( @@ -52,6 +53,15 @@ export function buttonSetting( }; } +export function componentSetting( + options: Omit, +): ComponentSetting { + return { + type: "component", + ...options, + }; +} + export function hotkeySetting( options: Omit, ): HotkeySetting { diff --git a/src/plugins/core/types.ts b/src/plugins/core/types.ts index 1e06b0ea..c3616f94 100644 --- a/src/plugins/core/types.ts +++ b/src/plugins/core/types.ts @@ -48,13 +48,21 @@ export interface HotkeySetting { description?: string; } +export interface ComponentSetting { + type: "component"; + title: string; + description?: string; + component: any; +} + export type PluginSetting = | BooleanSetting | StringSetting | NumberSetting | SelectSetting | ButtonSetting - | HotkeySetting; + | HotkeySetting + | ComponentSetting; export type PluginSettings = { [key: string]: PluginSetting; @@ -71,7 +79,9 @@ export type SettingValue = T extends BooleanSetting ? O : T extends HotkeySetting ? string - : never; + : T extends ComponentSetting + ? never + : never; export type SettingsAPI = { [K in keyof T]: SettingValue; diff --git a/src/plugins/index.ts b/src/plugins/index.ts index e47dfa84..00b692af 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -7,6 +7,7 @@ import themesPlugin from "./built-in/themes"; import animatedBackgroundPlugin from "./built-in/animatedBackground"; import assessmentsAveragePlugin from "./built-in/assessmentsAverage"; import globalSearchPlugin from "./built-in/globalSearch/src/core"; +import profilePicturePlugin from "./built-in/profilePicture"; //import testPlugin from './built-in/test'; // Initialize plugin manager @@ -19,6 +20,7 @@ pluginManager.registerPlugin(assessmentsAveragePlugin); pluginManager.registerPlugin(notificationCollectorPlugin); pluginManager.registerPlugin(timetablePlugin); pluginManager.registerPlugin(globalSearchPlugin); +pluginManager.registerPlugin(profilePicturePlugin); //pluginManager.registerPlugin(testPlugin); export { init as Monofile } from "./monofile";