mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-13 15:14:40 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 690792fd62 | |||
| f6ac112329 | |||
| ec68cec0ca |
@@ -1653,6 +1653,13 @@ html.transparencyEffects
|
||||
box-shadow: 0px 10px 15px -3px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Smoothed by attachNotificationsPanelAnimation (matches #ExtensionPopup spring) */
|
||||
.bsplus-notifications-panel {
|
||||
transform-origin: top right;
|
||||
will-change: opacity, transform;
|
||||
filter: drop-shadow(0px 0px 20px rgba(0, 0, 0, 0.35));
|
||||
}
|
||||
|
||||
#menu li.active {
|
||||
color: #ffffff !important;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
|
||||
@@ -63,7 +63,12 @@ function resetTimetableStyles(): void {
|
||||
}
|
||||
|
||||
async function handleTimetable(): Promise<void> {
|
||||
await waitForElm(".time", true, 10);
|
||||
// SEQTA uses `.times` blocks on entries, not necessarily `.time`; avoid infinite polling on a missing selector.
|
||||
try {
|
||||
await waitForElm(".timetablepage .times, .timetablepage .entry.class", true, 50, 200);
|
||||
} catch {
|
||||
/* timetable body may render after the shell */
|
||||
}
|
||||
|
||||
// Convert time format if needed
|
||||
if (settingsState.timeFormat == "12") {
|
||||
|
||||
@@ -271,7 +271,9 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
};
|
||||
|
||||
const syncQuickbarFromDOM = () => {
|
||||
const quickbar = document.querySelector(".timetablepage .quickbar.visible");
|
||||
const quickbar = document.querySelector(
|
||||
".timetablepage .quickbar.below.visible, .timetablepage .quickbar.visible",
|
||||
);
|
||||
if (quickbar && quickbar.getAttribute("data-type") === "class") {
|
||||
const titleEl = quickbar.querySelector(".title");
|
||||
const roomEl = quickbar.querySelector(".meta .room");
|
||||
@@ -287,7 +289,9 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
if (!timetablePage || quickbarObserver) return;
|
||||
|
||||
quickbarObserver = new MutationObserver(() => {
|
||||
const quickbar = document.querySelector(".timetablepage .quickbar.visible");
|
||||
const quickbar = document.querySelector(
|
||||
".timetablepage .quickbar.below.visible, .timetablepage .quickbar.visible",
|
||||
);
|
||||
if (quickbar?.getAttribute("data-type") === "class") {
|
||||
addEditButtonToQuickbar(quickbar as HTMLElement);
|
||||
}
|
||||
@@ -302,7 +306,13 @@ const timetableEditPlugin: Plugin<{}, TimetableStorage> = {
|
||||
};
|
||||
|
||||
const handleTimetable = async () => {
|
||||
await waitForElm(".timetablepage .entry", true, 10, 100);
|
||||
// Class entries (`div.entry.class`) load after the page shell; don't fail the whole
|
||||
// setup if they are slow or briefly absent (e.g. navigation). Observers still catch them.
|
||||
try {
|
||||
await waitForElm(".timetablepage .entry.class", true, 50, 300);
|
||||
} catch {
|
||||
/* entries may appear later */
|
||||
}
|
||||
processAllEntries();
|
||||
setupQuickbarObserver();
|
||||
syncQuickbarFromDOM();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
|
||||
import { loadEngageHomePage } from "@/seqta/utils/Loaders/LoadEngageHomePage";
|
||||
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
|
||||
import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
|
||||
import { attachNotificationsPanelAnimation } from "@/seqta/utils/attachNotificationsPanelAnimation";
|
||||
import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
|
||||
@@ -89,6 +90,7 @@ export async function AddBetterSEQTAElements() {
|
||||
addExtensionSettings();
|
||||
await createSettingsButton();
|
||||
setupSettingsButton();
|
||||
attachNotificationsPanelAnimation();
|
||||
}
|
||||
|
||||
function createHomeButton(fragment: DocumentFragment, _: HTMLElement) {
|
||||
@@ -423,10 +425,12 @@ async function setupEngageSettingsButton() {
|
||||
await addDarkLightToggle(parent);
|
||||
await createSettingsButton(parent);
|
||||
setupSettingsButton();
|
||||
attachNotificationsPanelAnimation();
|
||||
} catch {
|
||||
await addDarkLightToggle();
|
||||
await createSettingsButton();
|
||||
setupSettingsButton();
|
||||
attachNotificationsPanelAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
|
||||
<li>Fixed today's lessons on the homepage misbehaving in developer mode.</li>
|
||||
<li>Reduced overlap between BetterSEQTA subject averages and SEQTA's built-in averages UI.</li>
|
||||
<li>Updated outdated in-app links and update some under the hood code (Vite 8).</li>
|
||||
|
||||
<li>Added a notifications panel animation to work like settings.</li>
|
||||
<li>Fix timetable edit plugin not working correctly.</li>
|
||||
<h1>3.5.3 - Adaptive theme updates</h1>
|
||||
<li>Fixed adaptive theming on current-year course and assessment pages.</li>
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { animate } from "motion";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import { waitForElm } from "@/seqta/utils/waitForElm";
|
||||
|
||||
/**
|
||||
* Finds the SEQTA notifications dropdown panel (the list container next to the bell).
|
||||
*/
|
||||
function findNotificationPanel(): HTMLElement | null {
|
||||
const wrapper = document.querySelector(".connectedNotificationsWrapper");
|
||||
if (!wrapper) return null;
|
||||
|
||||
const flat = wrapper.querySelector<HTMLElement>(":scope > div > button + div");
|
||||
if (flat) return flat;
|
||||
|
||||
const notifBlock = wrapper.querySelector("[class*='notifications__notifications___']");
|
||||
if (notifBlock?.nextElementSibling instanceof HTMLElement) {
|
||||
return notifBlock.nextElementSibling;
|
||||
}
|
||||
|
||||
const list = wrapper.querySelector<HTMLElement>("[class*='notifications__list___']");
|
||||
if (list) return list;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPanelVisible(el: HTMLElement): boolean {
|
||||
return (
|
||||
el.getClientRects().length > 0 && getComputedStyle(el).visibility !== "hidden"
|
||||
);
|
||||
}
|
||||
|
||||
let lastVisible = false;
|
||||
/** Invalidates in-flight open animations when the panel closes or reopens. */
|
||||
let motionGeneration = 0;
|
||||
|
||||
function runOpenAnimation(panel: HTMLElement) {
|
||||
const myGen = ++motionGeneration;
|
||||
panel.classList.add("bsplus-notifications-panel");
|
||||
|
||||
if (!settingsState.animations) {
|
||||
panel.style.opacity = "1";
|
||||
panel.style.transform = "scale(1)";
|
||||
return;
|
||||
}
|
||||
|
||||
panel.style.opacity = "0";
|
||||
panel.style.transform = "scale(0)";
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (myGen !== motionGeneration) return;
|
||||
animate(0, 1, {
|
||||
onUpdate: (progress) => {
|
||||
panel.style.opacity = String(progress);
|
||||
panel.style.transform = `scale(${progress})`;
|
||||
},
|
||||
type: "spring",
|
||||
stiffness: 280,
|
||||
damping: 20,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function clearPanelMotionStyles(panel: HTMLElement) {
|
||||
motionGeneration++;
|
||||
panel.style.opacity = "";
|
||||
panel.style.transform = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring open / fade close for the native SEQTA notifications dropdown, matching ExtensionPopup.
|
||||
*/
|
||||
export function attachNotificationsPanelAnimation() {
|
||||
void setupNotificationsPanelAnimation();
|
||||
}
|
||||
|
||||
async function setupNotificationsPanelAnimation() {
|
||||
try {
|
||||
await waitForElm(".connectedNotificationsWrapper", true, 100, 60);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = document.querySelector(".connectedNotificationsWrapper");
|
||||
if (!wrapper) return;
|
||||
|
||||
const sync = () => {
|
||||
const panel = findNotificationPanel();
|
||||
// When SEQTA removes the dropdown from the DOM on close, we must reset
|
||||
// lastVisible — otherwise the next open still looks "already visible" and skips animation.
|
||||
if (!panel) {
|
||||
if (lastVisible) {
|
||||
lastVisible = false;
|
||||
motionGeneration++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const visible = isPanelVisible(panel);
|
||||
if (visible === lastVisible) return;
|
||||
|
||||
if (visible) {
|
||||
runOpenAnimation(panel);
|
||||
} else {
|
||||
clearPanelMotionStyles(panel);
|
||||
}
|
||||
lastVisible = visible;
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
sync();
|
||||
});
|
||||
observer.observe(wrapper, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["style", "class"],
|
||||
});
|
||||
|
||||
document.addEventListener(
|
||||
"click",
|
||||
() => {
|
||||
requestAnimationFrame(() => requestAnimationFrame(sync));
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
sync();
|
||||
}
|
||||
Reference in New Issue
Block a user