feat: apply our exisitng icons to engage sidebar

This commit is contained in:
2026-05-24 17:11:47 +09:30
parent 304ce2e128
commit 475b865000
4 changed files with 195 additions and 113 deletions
+2 -75
View File
@@ -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 -38
View File
@@ -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;
+153
View File
@@ -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;
}
},
);
}
+39
View File
@@ -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;
}