mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
Merge branch 'main' into main
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;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
StringSetting,
|
||||
ButtonSetting,
|
||||
HotkeySetting,
|
||||
ComponentSetting,
|
||||
} from "./types";
|
||||
import { createPluginAPI } from "./createAPI";
|
||||
import browser from "webextension-polyfill";
|
||||
@@ -299,7 +300,8 @@ export class PluginManager {
|
||||
options: Array<{ value: string; label: string }>;
|
||||
})
|
||||
| (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 });
|
||||
};
|
||||
// Actual type is more complex, see original code, but this gives the gist for the JSDoc.
|
||||
// Array<{ pluginId: string; name: string; description: string; beta?: boolean; settings: Record<string, ProcessedSetting>; disableToggle?: boolean; }>
|
||||
@@ -309,8 +311,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
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
StringSetting,
|
||||
HotkeySetting,
|
||||
PluginSettings,
|
||||
ComponentSetting,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
@@ -114,6 +115,16 @@ export function buttonSetting(
|
||||
* excluding the `type` property (e.g., `title`, `default` hotkey string).
|
||||
* @returns {HotkeySetting} A complete hotkey setting object with `type: "hotkey"`.
|
||||
*/
|
||||
|
||||
export function componentSetting(
|
||||
options: Omit<ComponentSetting, "type">,
|
||||
): ComponentSetting {
|
||||
return {
|
||||
type: "component",
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
export function hotkeySetting(
|
||||
options: Omit<HotkeySetting, "type">,
|
||||
): HotkeySetting {
|
||||
|
||||
@@ -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<string>
|
||||
| ButtonSetting
|
||||
| HotkeySetting;
|
||||
| HotkeySetting
|
||||
| ComponentSetting;
|
||||
|
||||
export type PluginSettings = {
|
||||
[key: string]: PluginSetting;
|
||||
@@ -71,7 +79,9 @@ export type SettingValue<T extends PluginSetting> = T extends BooleanSetting
|
||||
? O
|
||||
: T extends HotkeySetting
|
||||
? string
|
||||
: never;
|
||||
: T extends ComponentSetting
|
||||
? never
|
||||
: never;
|
||||
|
||||
export type SettingsAPI<T extends PluginSettings> = {
|
||||
[K in keyof T]: SettingValue<T[K]>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -24,6 +24,9 @@ import loading from "@/seqta/ui/Loading";
|
||||
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
||||
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
||||
import { OpenWhatsNewPopup } from "@/seqta/utils/Whatsnew";
|
||||
import {
|
||||
updateTimetableTimes,
|
||||
} from "@/seqta/utils/updateTimetableTimes";
|
||||
|
||||
// JSON content
|
||||
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
||||
@@ -241,14 +244,16 @@ async function LoadPageElements(): Promise<void> {
|
||||
handleReports,
|
||||
);
|
||||
|
||||
/* eventManager.register(
|
||||
eventManager.register(
|
||||
"timetableAdded",
|
||||
{
|
||||
elementType: "div",
|
||||
className: "timetablepage",
|
||||
},
|
||||
handleTimetable,
|
||||
) */
|
||||
async () => {
|
||||
await updateTimetableTimes();
|
||||
},
|
||||
);
|
||||
|
||||
eventManager.register(
|
||||
"noticesAdded",
|
||||
@@ -503,7 +508,7 @@ export async function ObserveMenuItemPosition() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!node?.dataset?.checked && !MenuOptionsOpen) {
|
||||
if (!MenuOptionsOpen) {
|
||||
const key =
|
||||
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
|
||||
if (key) {
|
||||
|
||||
Reference in New Issue
Block a user