diff --git a/README.md b/README.md index d6e82240..810ad95d 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ Don't worry- if you get stuck feel free to ask around in the [discord](https://d ## Getting started -1. Clone the repository +    **1. Clone the repository** ``` git clone https://github.com/BetterSEQTA/BetterSEQTA-Plus ``` -1. Install dependencies +    **2. Install dependencies** You may install the dependencies like below: @@ -80,27 +80,27 @@ npm install --legacy-peer-deps # Only NPM supported ### Running Development -2. Run the dev script (it updates as you save files) +    **3. Run the dev script (it updates as you save files)** ``` -npm run dev # or use your perferred package manager +npm run dev # or use your preferred package manager ``` ### Building for production -2. Run the build script +    **4. Run the build script** ``` -npm run build # or use your perferred package manager +npm run build # or use your preferred package manager ``` -2.1. Package it up (optional) +    **4.1. Package it up (optional)** ``` -npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your perferred package manager +npm run zip # This REQUIRES 7-Zip to be installed in order to work. You can also use your preferred package manager ``` -3. Load the extension into chrome +    **5. Load the extension into chrome** - Go to `chrome://extensions` - Enable developer mode diff --git a/src/css/injected.scss b/src/css/injected.scss index 299c23e7..1bb63a6b 100644 --- a/src/css/injected.scss +++ b/src/css/injected.scss @@ -1997,6 +1997,15 @@ div.entry.class[style*="width: 46.5%"] { min-width: 0; width: auto !important; } + +div.entry.tutorial { + border-radius: 4px; +} + +div.entry.event { + border-radius: 4px; +} + .uiFileHandler .uiButton { border-radius: 32px !important; color: var(--text-primary) !important; 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 6ba06f6d..86243862 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"; @@ -299,7 +300,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 }); }; // 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; 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 diff --git a/src/plugins/core/settingsHelpers.ts b/src/plugins/core/settingsHelpers.ts index 56da40c8..40651138 100644 --- a/src/plugins/core/settingsHelpers.ts +++ b/src/plugins/core/settingsHelpers.ts @@ -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 { + 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"; diff --git a/src/plugins/monofile.ts b/src/plugins/monofile.ts index aefcae2e..55e6e71c 100644 --- a/src/plugins/monofile.ts +++ b/src/plugins/monofile.ts @@ -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 { 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) { diff --git a/src/seqta/utils/Openers/OpenAboutPage.ts b/src/seqta/utils/Openers/OpenAboutPage.ts index a953714a..ed08d1a7 100644 --- a/src/seqta/utils/Openers/OpenAboutPage.ts +++ b/src/seqta/utils/Openers/OpenAboutPage.ts @@ -23,31 +23,39 @@ export function OpenAboutPage() { let text = stringToHTML(/* html */ `
-

BetterSEQTA+ is a fork of BetterSEQTA (originally developed by Nulkem), which was discontinued. BetterSEQTA+ continued development of BetterSEQTA, while incorporating a plethora of features.

We are currently working on fixing bugs and adding useful features. If you want to make a feature request or report a bug, you can do so on GitHub (find icon below). We are always looking for more contributors!

Credits:

-

Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph, SethBurkart123, and other contributors.

-

Full contributors list here

+

+ Nulkem created the original extension, was ported to Manifest V3 by MEGA-Dawg68, and is under active development by Crazypersonalph, SethBurkart123, and other contributors. +

+

+ All Contributors: +

+
+ +
`).firstChild; let footer = stringToHTML(/* html */ `
- Report bugs and feedback: - - + Report bugs and feedback: + + - - + + - - + + diff --git a/src/seqta/utils/Whatsnew.ts b/src/seqta/utils/Whatsnew.ts index 1f9a20e9..fa96e6a1 100644 --- a/src/seqta/utils/Whatsnew.ts +++ b/src/seqta/utils/Whatsnew.ts @@ -257,26 +257,25 @@ export function OpenWhatsNewPopup() { let footer = stringToHTML(/* html */ `
- diff --git a/src/seqta/utils/convertTo12HourFormat.ts b/src/seqta/utils/convertTo12HourFormat.ts index 8dd6670e..691bea0c 100644 --- a/src/seqta/utils/convertTo12HourFormat.ts +++ b/src/seqta/utils/convertTo12HourFormat.ts @@ -3,10 +3,10 @@ export function convertTo12HourFormat( noMinutes: boolean = false, ): string { let [hours, minutes] = time.split(":").map(Number); - let period = "AM"; + let period = "am"; if (hours >= 12) { - period = "PM"; + period = "pm"; if (hours > 12) hours -= 12; } else if (hours === 0) { hours = 12; @@ -17,5 +17,5 @@ export function convertTo12HourFormat( hoursStr = hoursStr.substring(1); } - return `${hoursStr}${noMinutes ? "" : `:${minutes.toString().padStart(2, "0")}`} ${period}`; + return `${hoursStr}${noMinutes ? "" : `:${minutes.toString().padStart(2, "0")}`}${period}`; } diff --git a/src/seqta/utils/updateTimetableTimes.ts b/src/seqta/utils/updateTimetableTimes.ts new file mode 100644 index 00000000..3e9a3d5c --- /dev/null +++ b/src/seqta/utils/updateTimetableTimes.ts @@ -0,0 +1,51 @@ +import { settingsState } from "@/seqta/utils/listeners/SettingsState"; +import { convertTo12HourFormat } from "./convertTo12HourFormat"; +import { waitForElm } from "./waitForElm"; + + +export async function updateTimetableTimes(): Promise { + if (!settingsState.timeFormat) return; + + const timetablePage = document.querySelector(".timetablepage"); + if (!timetablePage) return; + + // Wait for time elements to exist if page is still loading + try { + await waitForElm(".timetablepage .time", true, 10); + } catch { + return; + } + + const times = timetablePage.querySelectorAll(".times .time"); + times.forEach((el) => { + if (!el.dataset.original) el.dataset.original = el.textContent || ""; + const original = el.dataset.original; + if (!original) return; + + if (settingsState.timeFormat === "12") { + el.textContent = convertTo12HourFormat(original, true) + .toLowerCase() + .replace(" ", ""); + } else { + el.textContent = original; + } + }); + + const entryTimes = timetablePage.querySelectorAll(".entry .times"); + entryTimes.forEach((el) => { + if (!el.dataset.original) el.dataset.original = el.textContent || ""; + const original = el.dataset.original || ""; + if (!original.includes("–") && !original.includes("-")) return; + + const [start, end] = original.split(/[-–]/).map((p) => p.trim()); + if (!start || !end) return; + + if (settingsState.timeFormat === "12") { + const start12 = convertTo12HourFormat(start).toLowerCase().replace(" ", ""); + const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", ""); + el.textContent = `${start12}–${end12}`; + } else { + el.textContent = original; + } + }); +} diff --git a/vite.config.ts b/vite.config.ts index b9b32943..229d002d 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -78,6 +78,7 @@ export default defineConfig(({ command }) => ({ emptyOutDir: false, minify: true, //sourcemap: sourcemap, + chunkSizeWarningLimit: 4000, rollupOptions: { input: { settings: join(__dirname, "src", "interface", "index.html"),