mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: apply our exisitng icons to engage sidebar
This commit is contained in:
+2
-75
@@ -3,10 +3,6 @@ import browser from "webextension-polyfill";
|
|||||||
import { animate, stagger } from "motion";
|
import { animate, stagger } from "motion";
|
||||||
|
|
||||||
// Internal utilities and functions
|
// Internal utilities and functions
|
||||||
import {
|
|
||||||
ChangeMenuItemPositions,
|
|
||||||
MenuOptionsOpen,
|
|
||||||
} from "@/seqta/utils/Openers/OpenMenuOptions";
|
|
||||||
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
||||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
import { delay } from "@/seqta/utils/delay";
|
import { delay } from "@/seqta/utils/delay";
|
||||||
@@ -34,7 +30,7 @@ import { runStartupPopupQueue } from "@/seqta/utils/Openers/StartupPopupQueue";
|
|||||||
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
|
import { updateTimetableTimes } from "@/seqta/utils/updateTimetableTimes";
|
||||||
|
|
||||||
// JSON content
|
// JSON content
|
||||||
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
import { observeMenuItemPosition } from "@/seqta/utils/sidebarMenuIcons";
|
||||||
|
|
||||||
// Icons and fonts
|
// Icons and fonts
|
||||||
import IconFamily from "@/resources/fonts/IconFamily.woff";
|
import IconFamily from "@/resources/fonts/IconFamily.woff";
|
||||||
@@ -612,75 +608,6 @@ export function tryLoad() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ReplaceMenuSVG(element: HTMLElement, svg: string) {
|
|
||||||
let item = element.firstChild as HTMLElement;
|
|
||||||
item!.firstChild!.remove();
|
|
||||||
|
|
||||||
item.innerHTML = `<span>${item.innerHTML}</span>`;
|
|
||||||
|
|
||||||
let newsvg = stringToHTML(svg).firstChild;
|
|
||||||
item.insertBefore(newsvg as Node, item.firstChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
const processedSymbol = Symbol("processed");
|
|
||||||
|
|
||||||
export async function ObserveMenuItemPosition() {
|
|
||||||
if (isSeqtaEngageExperience()) return;
|
|
||||||
await waitForElm("#menu > ul > li");
|
|
||||||
|
|
||||||
eventManager.register(
|
|
||||||
"menuList",
|
|
||||||
{
|
|
||||||
parentElement: document.querySelector("#menu")!.firstChild as Element,
|
|
||||||
},
|
|
||||||
(element: Element) => {
|
|
||||||
const node = element as HTMLElement;
|
|
||||||
|
|
||||||
// Only process top-level menu items and skip everything else
|
|
||||||
if (
|
|
||||||
!node.classList.contains("item") ||
|
|
||||||
node.nodeName !== "LI" ||
|
|
||||||
node.parentElement?.parentElement?.id !== "menu"
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early exit if already processed
|
|
||||||
if ((element as any)[processedSymbol]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MenuOptionsOpen) {
|
|
||||||
const key =
|
|
||||||
MenuitemSVGKey[node?.dataset?.key! as keyof typeof MenuitemSVGKey];
|
|
||||||
if (key) {
|
|
||||||
ReplaceMenuSVG(
|
|
||||||
node,
|
|
||||||
MenuitemSVGKey[node.dataset.key as keyof typeof MenuitemSVGKey],
|
|
||||||
);
|
|
||||||
} else if (node?.firstChild?.nodeName === "LABEL") {
|
|
||||||
const label = node.firstChild as HTMLElement;
|
|
||||||
let textNode = label.lastChild as HTMLElement;
|
|
||||||
|
|
||||||
if (
|
|
||||||
textNode.nodeType === 3 &&
|
|
||||||
textNode.parentNode &&
|
|
||||||
textNode.parentNode.nodeName !== "SPAN"
|
|
||||||
) {
|
|
||||||
const span = document.createElement("span");
|
|
||||||
span.textContent = textNode.nodeValue;
|
|
||||||
|
|
||||||
label.replaceChild(span, textNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ChangeMenuItemPositions(settingsState.menuorder);
|
|
||||||
|
|
||||||
(element as any)[processedSymbol] = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function showConflictPopup() {
|
export function showConflictPopup() {
|
||||||
if (document.getElementById("conflict-popup")) return;
|
if (document.getElementById("conflict-popup")) return;
|
||||||
document.body.classList.remove("hidden");
|
document.body.classList.remove("hidden");
|
||||||
@@ -760,7 +687,7 @@ export function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector(".legacy-root")?.classList.add("hidden");
|
document.querySelector(".legacy-root")?.classList.add("hidden");
|
||||||
ObserveMenuItemPosition();
|
void observeMenuItemPosition();
|
||||||
|
|
||||||
new StorageChangeHandler();
|
new StorageChangeHandler();
|
||||||
new MessageHandler();
|
new MessageHandler();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { waitForEngageMenuList } from "@/seqta/utils/waitForEngageMenuList";
|
||||||
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
|
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
|
||||||
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
import { loadEngageHomePage } from "@/seqta/utils/Loaders/LoadEngageHomePage";
|
import { loadEngageHomePage } from "@/seqta/utils/Loaders/LoadEngageHomePage";
|
||||||
@@ -287,44 +288,6 @@ async function createSettingsButton(parent?: Element) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Engage mounts the sidebar inside batched React trees; EventManager-based waitForElm can miss `#menu`. Polling `waitForElm` matches the real DOM reliably. */
|
|
||||||
async function waitForEngageMenuList(): Promise<HTMLElement | null> {
|
|
||||||
const poll = true as const;
|
|
||||||
const interval = 100;
|
|
||||||
const trySelectors: { selector: string; maxIterations: number }[] = [
|
|
||||||
{ selector: "#menu > ul > li", maxIterations: 500 },
|
|
||||||
{ selector: "#menu ul", maxIterations: 350 },
|
|
||||||
{ selector: "#menu", maxIterations: 350 },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const { selector, maxIterations } of trySelectors) {
|
|
||||||
try {
|
|
||||||
await waitForElm(selector, poll, interval, maxIterations);
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selector === "#menu > ul > li") {
|
|
||||||
const ul = document.querySelector("#menu > ul") as HTMLElement | null;
|
|
||||||
if (ul) return ul;
|
|
||||||
} else if (selector === "#menu ul") {
|
|
||||||
const ul = document.querySelector("#menu ul") as HTMLElement | null;
|
|
||||||
if (ul) return ul;
|
|
||||||
} else {
|
|
||||||
const menu = document.getElementById("menu");
|
|
||||||
const ul =
|
|
||||||
(menu?.querySelector("ul") as HTMLElement | null) ??
|
|
||||||
(menu?.firstElementChild as HTMLElement | null);
|
|
||||||
if (ul) return ul;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(
|
|
||||||
"[BetterSEQTA+] Engage: could not find a menu list to inject the home button",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function injectEngageHomeButton() {
|
async function injectEngageHomeButton() {
|
||||||
if (document.getElementById("homebutton")) return;
|
if (document.getElementById("homebutton")) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import MenuitemSVGKey from "@/seqta/content/MenuItemSVGKey.json";
|
||||||
|
import {
|
||||||
|
ChangeMenuItemPositions,
|
||||||
|
MenuOptionsOpen,
|
||||||
|
} from "@/seqta/utils/Openers/OpenMenuOptions";
|
||||||
|
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
|
import { waitForEngageMenuList } from "@/seqta/utils/waitForEngageMenuList";
|
||||||
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
import { eventManager } from "@/seqta/utils/listeners/EventManager";
|
||||||
|
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||||
|
|
||||||
|
const BETTERSEQTA_ICON_ATTR = "data-betterseqta-icon";
|
||||||
|
|
||||||
|
function getMenuLabel(element: HTMLElement): HTMLElement | null {
|
||||||
|
const label = element.querySelector(":scope > label");
|
||||||
|
return label instanceof HTMLElement ? label : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTopLevelMenuList(menu = document.getElementById("menu")): HTMLElement | null {
|
||||||
|
if (!menu) return null;
|
||||||
|
return (
|
||||||
|
(menu.querySelector(":scope > ul") as HTMLElement | null) ??
|
||||||
|
(menu.querySelector("ul") as HTMLElement | null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTopLevelSidebarItem(node: HTMLElement): boolean {
|
||||||
|
if (!node.classList.contains("item")) return false;
|
||||||
|
if (node.nodeName !== "LI" && node.nodeName !== "SECTION") return false;
|
||||||
|
|
||||||
|
const topList = getTopLevelMenuList();
|
||||||
|
return !!topList && node.parentElement === topList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapMenuLabelText(label: HTMLElement) {
|
||||||
|
const textNode = label.lastChild;
|
||||||
|
if (
|
||||||
|
textNode?.nodeType === 3 &&
|
||||||
|
textNode.parentNode &&
|
||||||
|
textNode.parentNode.nodeName !== "SPAN"
|
||||||
|
) {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
span.textContent = textNode.nodeValue;
|
||||||
|
label.replaceChild(span, textNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceMenuSVG(element: HTMLElement, svg: string) {
|
||||||
|
const label = getMenuLabel(element);
|
||||||
|
if (!label?.firstChild) return;
|
||||||
|
|
||||||
|
if (label.firstElementChild?.getAttribute(BETTERSEQTA_ICON_ATTR) === "true") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.firstChild.remove();
|
||||||
|
label.innerHTML = `<span>${label.innerHTML}</span>`;
|
||||||
|
|
||||||
|
const newSvg = stringToHTML(svg).firstChild;
|
||||||
|
if (!(newSvg instanceof Element)) return;
|
||||||
|
|
||||||
|
newSvg.setAttribute(BETTERSEQTA_ICON_ATTR, "true");
|
||||||
|
label.insertBefore(newSvg, label.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processMenuItemNode(node: HTMLElement) {
|
||||||
|
if (!isTopLevelSidebarItem(node) || MenuOptionsOpen) return;
|
||||||
|
|
||||||
|
const key = node.dataset.key as keyof typeof MenuitemSVGKey | undefined;
|
||||||
|
if (key && MenuitemSVGKey[key]) {
|
||||||
|
replaceMenuSVG(node, MenuitemSVGKey[key]);
|
||||||
|
} else {
|
||||||
|
const label = getMenuLabel(node);
|
||||||
|
if (label) wrapMenuLabelText(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTopLevelMenuItems(reorder = !isSeqtaEngageExperience()) {
|
||||||
|
if (MenuOptionsOpen) return;
|
||||||
|
|
||||||
|
const topList = getTopLevelMenuList();
|
||||||
|
if (!topList) return;
|
||||||
|
|
||||||
|
for (const child of topList.children) {
|
||||||
|
if (child instanceof HTMLElement) {
|
||||||
|
processMenuItemNode(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reorder) {
|
||||||
|
ChangeMenuItemPositions(settingsState.menuorder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let engageMenuIconObserver: MutationObserver | null = null;
|
||||||
|
let engageMenuIconFrame: number | null = null;
|
||||||
|
|
||||||
|
function scheduleEngageMenuIconPass() {
|
||||||
|
if (engageMenuIconFrame !== null) return;
|
||||||
|
|
||||||
|
engageMenuIconFrame = window.requestAnimationFrame(() => {
|
||||||
|
engageMenuIconFrame = null;
|
||||||
|
processTopLevelMenuItems(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function observeEngageMenuIcons() {
|
||||||
|
const menuList = await waitForEngageMenuList();
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
if (!menu || !menuList) return;
|
||||||
|
|
||||||
|
processTopLevelMenuItems(false);
|
||||||
|
|
||||||
|
engageMenuIconObserver?.disconnect();
|
||||||
|
engageMenuIconObserver = new MutationObserver(() => {
|
||||||
|
scheduleEngageMenuIconPass();
|
||||||
|
});
|
||||||
|
engageMenuIconObserver.observe(menu, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedSymbol = Symbol("processed");
|
||||||
|
|
||||||
|
export async function observeMenuItemPosition() {
|
||||||
|
if (isSeqtaEngageExperience()) {
|
||||||
|
await observeEngageMenuIcons();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForElm("#menu > ul > li");
|
||||||
|
|
||||||
|
eventManager.register(
|
||||||
|
"menuList",
|
||||||
|
{
|
||||||
|
parentElement: document.querySelector("#menu")!.firstChild as Element,
|
||||||
|
},
|
||||||
|
(element: Element) => {
|
||||||
|
const node = element as HTMLElement;
|
||||||
|
|
||||||
|
if (!isTopLevelSidebarItem(node)) return;
|
||||||
|
if ((element as any)[processedSymbol]) return;
|
||||||
|
|
||||||
|
if (!MenuOptionsOpen) {
|
||||||
|
processMenuItemNode(node);
|
||||||
|
ChangeMenuItemPositions(settingsState.menuorder);
|
||||||
|
(element as any)[processedSymbol] = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||||
|
|
||||||
|
/** Engage mounts the sidebar inside batched React trees; polling `waitForElm` matches the real DOM reliably. */
|
||||||
|
export async function waitForEngageMenuList(): Promise<HTMLElement | null> {
|
||||||
|
const poll = true as const;
|
||||||
|
const interval = 100;
|
||||||
|
const trySelectors: { selector: string; maxIterations: number }[] = [
|
||||||
|
{ selector: "#menu > ul > li", maxIterations: 500 },
|
||||||
|
{ selector: "#menu ul", maxIterations: 350 },
|
||||||
|
{ selector: "#menu", maxIterations: 350 },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { selector, maxIterations } of trySelectors) {
|
||||||
|
try {
|
||||||
|
await waitForElm(selector, poll, interval, maxIterations);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selector === "#menu > ul > li") {
|
||||||
|
const ul = document.querySelector("#menu > ul") as HTMLElement | null;
|
||||||
|
if (ul) return ul;
|
||||||
|
} else if (selector === "#menu ul") {
|
||||||
|
const ul = document.querySelector("#menu ul") as HTMLElement | null;
|
||||||
|
if (ul) return ul;
|
||||||
|
} else {
|
||||||
|
const menu = document.getElementById("menu");
|
||||||
|
const ul =
|
||||||
|
(menu?.querySelector("ul") as HTMLElement | null) ??
|
||||||
|
(menu?.firstElementChild as HTMLElement | null);
|
||||||
|
if (ul) return ul;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"[BetterSEQTA+] Engage: could not find a menu list to inject the home button",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user