mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
refactor: further trim PR #458 debloat across patches, notices, Select, music, archive, search
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,21 +9,7 @@
|
|||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
let activeIndex = $state(0);
|
let activeIndex = $state(0);
|
||||||
let root: HTMLDivElement | undefined = $state();
|
let trigger = $state<HTMLButtonElement>();
|
||||||
let trigger: HTMLButtonElement | undefined = $state();
|
|
||||||
let listbox: HTMLDivElement | undefined = $state();
|
|
||||||
|
|
||||||
const selectedLabel = $derived(
|
|
||||||
options.find((option) => option.value === value)?.label ?? value,
|
|
||||||
);
|
|
||||||
|
|
||||||
const activeDescendantId = $derived(
|
|
||||||
isOpen && options[activeIndex] ? optionId(options[activeIndex].value) : undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
function optionId(optionValue: string): string {
|
|
||||||
return `${listboxId}-option-${optionValue}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openMenu(preferredIndex?: number) {
|
function openMenu(preferredIndex?: number) {
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
@@ -41,62 +27,59 @@
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveActive(delta: number) {
|
function onKeydown(event: KeyboardEvent, inListbox = false) {
|
||||||
if (!options.length) return;
|
const { key } = event;
|
||||||
activeIndex = (activeIndex + delta + options.length) % options.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(event: KeyboardEvent, inListbox = false) {
|
if (key === "ArrowDown" || key === "ArrowUp") {
|
||||||
switch (event.key) {
|
event.preventDefault();
|
||||||
case "ArrowDown":
|
const count = options.length;
|
||||||
case "ArrowUp": {
|
if (!count) return;
|
||||||
event.preventDefault();
|
if (isOpen || inListbox) {
|
||||||
const delta = event.key === "ArrowDown" ? 1 : -1;
|
activeIndex = (activeIndex + (key === "ArrowDown" ? 1 : -1) + count) % count;
|
||||||
if (isOpen || inListbox) moveActive(delta);
|
} else {
|
||||||
else openMenu();
|
openMenu();
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case "Enter":
|
return;
|
||||||
case " ":
|
}
|
||||||
event.preventDefault();
|
|
||||||
if (isOpen) {
|
if (key === "Enter" || key === " ") {
|
||||||
const option = options[activeIndex];
|
event.preventDefault();
|
||||||
if (option) selectValue(option.value);
|
if (isOpen) {
|
||||||
} else {
|
const option = options[activeIndex];
|
||||||
openMenu();
|
if (option) selectValue(option.value);
|
||||||
}
|
} else {
|
||||||
break;
|
openMenu();
|
||||||
case "Escape":
|
}
|
||||||
if (isOpen) {
|
return;
|
||||||
event.preventDefault();
|
}
|
||||||
closeMenu();
|
|
||||||
}
|
if (key === "Escape" && isOpen) {
|
||||||
break;
|
event.preventDefault();
|
||||||
case "Home":
|
closeMenu();
|
||||||
if (inListbox) {
|
return;
|
||||||
event.preventDefault();
|
}
|
||||||
activeIndex = 0;
|
|
||||||
}
|
if (!inListbox) return;
|
||||||
break;
|
|
||||||
case "End":
|
if (key === "Home") {
|
||||||
if (inListbox) {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
activeIndex = 0;
|
||||||
activeIndex = Math.max(0, options.length - 1);
|
} else if (key === "End") {
|
||||||
}
|
event.preventDefault();
|
||||||
break;
|
activeIndex = Math.max(0, options.length - 1);
|
||||||
case "Tab":
|
} else if (key === "Tab") {
|
||||||
if (inListbox) closeMenu(false);
|
closeMenu(false);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
queueMicrotask(() => listbox?.focus());
|
queueMicrotask(() => document.getElementById(listboxId)?.focus());
|
||||||
|
|
||||||
|
const wrapper = trigger?.parentElement;
|
||||||
const onPointerDown = (event: PointerEvent) => {
|
const onPointerDown = (event: PointerEvent) => {
|
||||||
if (root && event.composedPath().includes(root)) return;
|
if (wrapper && event.composedPath().includes(wrapper)) return;
|
||||||
closeMenu(false);
|
closeMenu(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,46 +88,47 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="select-wrapper" bind:this={root}>
|
<div class="select relative w-full">
|
||||||
<button
|
<button
|
||||||
bind:this={trigger}
|
bind:this={trigger}
|
||||||
type="button"
|
type="button"
|
||||||
class="select-trigger"
|
class="select-trigger flex w-full items-center justify-between gap-3 rounded-[18px] border px-4 py-2.5 text-sm font-medium leading-tight shadow-2xl transition-[background-color,border-color,box-shadow] duration-200 cursor-pointer"
|
||||||
aria-haspopup="listbox"
|
aria-haspopup="listbox"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-controls={listboxId}
|
aria-controls={listboxId}
|
||||||
onclick={() => (isOpen ? closeMenu() : openMenu())}
|
onclick={() => (isOpen ? closeMenu() : openMenu())}
|
||||||
onkeydown={(event) => handleKeydown(event)}
|
onkeydown={onKeydown}
|
||||||
>
|
>
|
||||||
<span class="select-label">{selectedLabel}</span>
|
<span class="truncate">
|
||||||
<span class="select-icon" aria-hidden="true">
|
{options.find((option) => option.value === value)?.label ?? value}
|
||||||
|
</span>
|
||||||
|
<span class="select-icon shrink-0" aria-hidden="true">
|
||||||
<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
|
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 11.168l3.71-3.938a.75.75 0 1 1 1.08 1.04l-4.25 4.5a.75.75 0 0 1-1.08 0l-4.25-4.5a.75.75 0 0 1 .02-1.06Z"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div
|
<div
|
||||||
bind:this={listbox}
|
|
||||||
id={listboxId}
|
id={listboxId}
|
||||||
class="select-menu"
|
class="select-menu absolute inset-x-0 top-[calc(100%+0.35rem)] z-50 flex max-h-72 flex-col gap-0.5 p-2"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
aria-activedescendant={activeDescendantId}
|
aria-activedescendant={options[activeIndex] ? `${listboxId}-opt-${activeIndex}` : undefined}
|
||||||
onkeydown={(event) => handleKeydown(event, true)}
|
onkeydown={(event) => onKeydown(event, true)}
|
||||||
>
|
>
|
||||||
{#each options as option, index (option.value)}
|
{#each options as option, index (option.value)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
id={optionId(option.value)}
|
id={`${listboxId}-opt-${index}`}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={option.value === value}
|
aria-selected={option.value === value}
|
||||||
class="select-option"
|
class="select-option block w-full rounded-[10px] border-none px-3.5 py-2.5 text-left text-sm font-medium leading-snug transition-colors duration-150 cursor-pointer"
|
||||||
class:is-selected={option.value === value}
|
class:is-selected={option.value === value}
|
||||||
class:is-active={index === activeIndex}
|
class:is-active={index === activeIndex}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@@ -159,99 +143,54 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.select-wrapper {
|
.select {
|
||||||
position: relative;
|
--sel-border: var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
|
||||||
width: 100%;
|
--sel-bg: var(--theme-primary, #ffffff);
|
||||||
|
--sel-surface: var(--theme-secondary, #e5e7eb);
|
||||||
|
--sel-focus-border: color-mix(in srgb, var(--text-primary) 22%, var(--theme-secondary, #e5e7eb) 78%);
|
||||||
|
--sel-ring: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
||||||
|
--sel-menu-shadow:
|
||||||
|
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
||||||
|
0 8px 10px -6px rgb(0 0 0 / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-trigger {
|
.select-trigger {
|
||||||
display: flex;
|
border-color: var(--sel-border);
|
||||||
align-items: center;
|
background: var(--sel-bg);
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
|
|
||||||
border-radius: 18px;
|
|
||||||
background: var(--theme-primary, #ffffff);
|
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0.625rem 1rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.25;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
|
||||||
transition:
|
|
||||||
background-color 180ms ease,
|
|
||||||
border-color 180ms ease,
|
|
||||||
box-shadow 180ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-trigger:hover,
|
.select-trigger:hover,
|
||||||
.select-trigger:focus-visible {
|
.select-trigger:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
background: var(--theme-secondary, #e5e7eb);
|
background: var(--sel-surface);
|
||||||
border-color: var(--theme-offset-bg, var(--theme-secondary, #d4d4d8));
|
border-color: var(--sel-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-trigger:focus-visible {
|
.select-trigger:focus-visible {
|
||||||
border-color: color-mix(in srgb, var(--text-primary) 22%, var(--theme-secondary, #e5e7eb) 78%);
|
border-color: var(--sel-focus-border);
|
||||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
box-shadow: var(--sel-ring);
|
||||||
}
|
|
||||||
|
|
||||||
.select-label {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-icon {
|
.select-icon {
|
||||||
flex-shrink: 0;
|
|
||||||
color: color-mix(in srgb, var(--text-primary) 60%, transparent);
|
color: color-mix(in srgb, var(--text-primary) 60%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-menu {
|
.select-menu {
|
||||||
position: absolute;
|
border: 1px solid var(--sel-border);
|
||||||
top: calc(100% + 0.35rem);
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 50;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.125rem;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border: 1px solid var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
|
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--theme-primary, #ffffff);
|
background: var(--sel-bg);
|
||||||
box-shadow:
|
box-shadow: var(--sel-menu-shadow);
|
||||||
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
|
||||||
0 8px 10px -6px rgb(0 0 0 / 0.2);
|
|
||||||
max-height: 18rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-menu:focus-visible {
|
.select-menu:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow:
|
box-shadow: var(--sel-menu-shadow), var(--sel-ring);
|
||||||
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
|
||||||
0 8px 10px -6px rgb(0 0 0 / 0.2),
|
|
||||||
0 0 0 1px color-mix(in srgb, var(--text-primary) 12%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0.625rem 0.875rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.35;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 150ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option:hover,
|
.select-option:hover,
|
||||||
@@ -259,6 +198,6 @@
|
|||||||
.select-option.is-active,
|
.select-option.is-active,
|
||||||
.select-option.is-selected {
|
.select-option.is-selected {
|
||||||
outline: none;
|
outline: none;
|
||||||
background: var(--theme-secondary, #e5e7eb);
|
background: var(--sel-surface);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ const LAYER_CLASSES = [
|
|||||||
["bg", "bg3", ANIMATED_BG_MARKER],
|
["bg", "bg3", ANIMATED_BG_MARKER],
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const layerSelector = `:scope > div.bg.${ANIMATED_BG_MARKER}`;
|
const bgSel = `.bg.${ANIMATED_BG_MARKER}`;
|
||||||
|
const scopeSel = `:scope > div${bgSel}`;
|
||||||
|
const BASE_SPEEDS = [3, 4, 5] as const;
|
||||||
|
|
||||||
export function updateAnimationSpeed(speed: number) {
|
export function updateAnimationSpeed(speed: number) {
|
||||||
document.querySelectorAll(`.bg.${ANIMATED_BG_MARKER}`).forEach((element, index) => {
|
document.querySelectorAll(bgSel).forEach((element, index) => {
|
||||||
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
|
const base = BASE_SPEEDS[index] ?? BASE_SPEEDS[2];
|
||||||
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
|
(element as HTMLElement).style.animationDuration = `${base / speed}s`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,12 +25,12 @@ export function ensureAnimatedBackgroundLayers(
|
|||||||
menu: HTMLElement,
|
menu: HTMLElement,
|
||||||
speed: number,
|
speed: number,
|
||||||
): void {
|
): void {
|
||||||
if (container.querySelectorAll(layerSelector).length >= 3) {
|
if (container.querySelectorAll(scopeSel).length >= 3) {
|
||||||
updateAnimationSpeed(speed);
|
updateAnimationSpeed(speed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.querySelectorAll(layerSelector).forEach((el) => el.remove());
|
container.querySelectorAll(scopeSel).forEach((el) => el.remove());
|
||||||
|
|
||||||
for (const classes of LAYER_CLASSES) {
|
for (const classes of LAYER_CLASSES) {
|
||||||
const bk = document.createElement("div");
|
const bk = document.createElement("div");
|
||||||
@@ -40,7 +42,7 @@ export function ensureAnimatedBackgroundLayers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeAnimatedBackgroundLayers(): void {
|
export function removeAnimatedBackgroundLayers(): void {
|
||||||
document.querySelectorAll(`div.bg.${ANIMATED_BG_MARKER}`).forEach((el) => el.remove());
|
document.querySelectorAll(`div${bgSel}`).forEach((el) => el.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function syncAnimatedBackground(
|
export async function syncAnimatedBackground(
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ class AnimatedBackgroundPluginClass extends BasePlugin<typeof settings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const instance = new AnimatedBackgroundPluginClass();
|
const instance = new AnimatedBackgroundPluginClass();
|
||||||
const resync = (api: PluginAPI<typeof settings>) => () => void syncAnimatedBackground(api);
|
|
||||||
|
|
||||||
const animatedBackgroundPlugin: Plugin<typeof settings> = {
|
const animatedBackgroundPlugin: Plugin<typeof settings> = {
|
||||||
id: "animated-background",
|
id: "animated-background",
|
||||||
@@ -42,22 +41,20 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
|
|||||||
|
|
||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
await syncAnimatedBackground(api);
|
await syncAnimatedBackground(api);
|
||||||
|
const resync = () => void syncAnimatedBackground(api);
|
||||||
|
|
||||||
const speedUnregister = api.settings.onChange("speed", updateAnimationSpeed);
|
const speedUnregister = api.settings.onChange("speed", updateAnimationSpeed);
|
||||||
const pageChangeUnregister = api.seqta.onPageChange(resync(api));
|
const pageChangeUnregister = api.seqta.onPageChange(resync);
|
||||||
const pageshowHandler = (event: PageTransitionEvent) => {
|
window.addEventListener("pageshow", resync);
|
||||||
if (event.persisted) void syncAnimatedBackground(api);
|
|
||||||
};
|
|
||||||
window.addEventListener("pageshow", pageshowHandler);
|
|
||||||
|
|
||||||
const containerObserver = new MutationObserver(resync(api));
|
const containerObserver = new MutationObserver(resync);
|
||||||
const container = document.getElementById("container");
|
const container = document.getElementById("container");
|
||||||
if (container) containerObserver.observe(container, { childList: true });
|
if (container) containerObserver.observe(container, { childList: true });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
speedUnregister.unregister();
|
speedUnregister.unregister();
|
||||||
pageChangeUnregister.unregister();
|
pageChangeUnregister.unregister();
|
||||||
window.removeEventListener("pageshow", pageshowHandler);
|
window.removeEventListener("pageshow", resync);
|
||||||
containerObserver.disconnect();
|
containerObserver.disconnect();
|
||||||
removeAnimatedBackgroundLayers();
|
removeAnimatedBackgroundLayers();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,15 +44,9 @@ let objectUrl: string | null = null;
|
|||||||
let gestureCleanup: (() => void) | null = null;
|
let gestureCleanup: (() => void) | null = null;
|
||||||
let resumeTimer: ReturnType<typeof setTimeout> | null = null;
|
let resumeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let hintEl: HTMLElement | null = null;
|
let hintEl: HTMLElement | null = null;
|
||||||
let playing = false;
|
|
||||||
|
|
||||||
const clamp = (v: number) => Math.max(0, Math.min(1, v));
|
const clamp = (v: number) => Math.max(0, Math.min(1, v));
|
||||||
|
|
||||||
async function loadBlob(): Promise<Blob | null> {
|
|
||||||
const blob = await store.getItem<Blob>("audio-blob");
|
|
||||||
return blob instanceof Blob ? blob : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearHint(): void {
|
function clearHint(): void {
|
||||||
hintEl?.remove();
|
hintEl?.remove();
|
||||||
hintEl = null;
|
hintEl = null;
|
||||||
@@ -63,40 +57,18 @@ function disarmGesture(): void {
|
|||||||
gestureCleanup = null;
|
gestureCleanup = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayStarted(): void {
|
|
||||||
playing = true;
|
|
||||||
clearHint();
|
|
||||||
disarmGesture();
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAudio(): void {
|
function stopAudio(): void {
|
||||||
audio?.pause();
|
audio?.pause();
|
||||||
audio?.remove();
|
audio?.remove();
|
||||||
audio = null;
|
audio = null;
|
||||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||||
objectUrl = null;
|
objectUrl = null;
|
||||||
playing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showHint(onActivate: () => void): void {
|
|
||||||
clearHint();
|
|
||||||
const hint = document.createElement("button");
|
|
||||||
hint.id = "bsplus-bg-music-hint";
|
|
||||||
hint.type = "button";
|
|
||||||
hint.className = "bsplus-bg-music-hint";
|
|
||||||
hint.textContent = "Tap to start background music";
|
|
||||||
hint.addEventListener("pointerdown", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
onActivate();
|
|
||||||
});
|
|
||||||
document.body.append(hint);
|
|
||||||
hintEl = hint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prepare <audio> so play() can run synchronously inside a user-gesture handler. */
|
/** Prepare <audio> so play() can run synchronously inside a user-gesture handler. */
|
||||||
async function prepareAudio(vol: number): Promise<boolean> {
|
async function prepareAudio(vol: number): Promise<boolean> {
|
||||||
const blob = await loadBlob();
|
const blob = await store.getItem<Blob>("audio-blob");
|
||||||
if (!blob) {
|
if (!(blob instanceof Blob)) {
|
||||||
stopAudio();
|
stopAudio();
|
||||||
clearHint();
|
clearHint();
|
||||||
return false;
|
return false;
|
||||||
@@ -114,25 +86,16 @@ async function prepareAudio(vol: number): Promise<boolean> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Call synchronously from a user-gesture handler (no await before this). */
|
function attemptPlay(vol: number): Promise<boolean> {
|
||||||
function playPrepared(vol: number): void {
|
if (!audio) return Promise.resolve(false);
|
||||||
if (!audio) return;
|
|
||||||
audio.volume = clamp(vol);
|
audio.volume = clamp(vol);
|
||||||
void audio.play().then(onPlayStarted).catch(() => {
|
return audio
|
||||||
playing = false;
|
.play()
|
||||||
});
|
.then(() => {
|
||||||
}
|
disarmGesture();
|
||||||
|
return true;
|
||||||
async function tryAutoplay(vol: number): Promise<boolean> {
|
})
|
||||||
if (!(await prepareAudio(vol)) || !audio) return false;
|
.catch(() => false);
|
||||||
try {
|
|
||||||
await audio.play();
|
|
||||||
onPlayStarted();
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
playing = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function armGesture(onGesture: () => void): void {
|
function armGesture(onGesture: () => void): void {
|
||||||
@@ -153,14 +116,19 @@ function armGesture(onGesture: () => void): void {
|
|||||||
}
|
}
|
||||||
clearHint();
|
clearHint();
|
||||||
};
|
};
|
||||||
showHint(onGesture);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearResumeTimer(): void {
|
clearHint();
|
||||||
if (resumeTimer !== null) {
|
const hint = document.createElement("button");
|
||||||
clearTimeout(resumeTimer);
|
hint.id = "bsplus-bg-music-hint";
|
||||||
resumeTimer = null;
|
hint.type = "button";
|
||||||
}
|
hint.className = "bsplus-bg-music-hint";
|
||||||
|
hint.textContent = "Tap to start background music";
|
||||||
|
hint.addEventListener("pointerdown", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onGesture();
|
||||||
|
});
|
||||||
|
document.body.append(hint);
|
||||||
|
hintEl = hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
const backgroundMusicPlugin: Plugin<typeof settings> = {
|
const backgroundMusicPlugin: Plugin<typeof settings> = {
|
||||||
@@ -177,22 +145,20 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
await api.storage.loaded;
|
await api.storage.loaded;
|
||||||
|
|
||||||
type BgSettings = { volume?: number; pauseOnHidden?: boolean };
|
type BgSettings = { volume?: number; pauseOnHidden?: boolean };
|
||||||
const s = () => api.settings as BgSettings;
|
const vol = () => (api.settings as BgSettings).volume ?? 0.5;
|
||||||
const vol = () => s().volume ?? 0.5;
|
const pauseOnHidden = () => (api.settings as BgSettings).pauseOnHidden ?? true;
|
||||||
const pauseOnHidden = () => s().pauseOnHidden ?? true;
|
|
||||||
|
|
||||||
const gestureStart = () => {
|
const gesturePlay = () => {
|
||||||
if (audio) playPrepared(vol());
|
void attemptPlay(vol());
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensurePlayback = async () => {
|
const ensurePlayback = async () => {
|
||||||
if (!(await prepareAudio(vol()))) return;
|
if (!(await prepareAudio(vol()))) return;
|
||||||
if (playing && audio && !audio.paused) {
|
if (audio && !audio.paused) {
|
||||||
clearHint();
|
|
||||||
disarmGesture();
|
disarmGesture();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!(await tryAutoplay(vol()))) armGesture(gestureStart);
|
if (!(await attemptPlay(vol()))) armGesture(gesturePlay);
|
||||||
};
|
};
|
||||||
|
|
||||||
api.settings.onChange("volume" as never, (value: unknown) => {
|
api.settings.onChange("volume" as never, (value: unknown) => {
|
||||||
@@ -214,9 +180,9 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
const onVisibility = () => {
|
const onVisibility = () => {
|
||||||
if (document.visibilityState === "hidden") {
|
if (document.visibilityState === "hidden") {
|
||||||
if (!pauseOnHidden() || !audio) return;
|
if (!pauseOnHidden() || !audio) return;
|
||||||
clearResumeTimer();
|
if (resumeTimer) clearTimeout(resumeTimer);
|
||||||
|
resumeTimer = null;
|
||||||
audio.pause();
|
audio.pause();
|
||||||
playing = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!audio) {
|
if (!audio) {
|
||||||
@@ -224,10 +190,10 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!pauseOnHidden()) return;
|
if (!pauseOnHidden()) return;
|
||||||
clearResumeTimer();
|
if (resumeTimer) clearTimeout(resumeTimer);
|
||||||
resumeTimer = setTimeout(() => {
|
resumeTimer = setTimeout(() => {
|
||||||
resumeTimer = null;
|
resumeTimer = null;
|
||||||
void tryAutoplay(vol());
|
void attemptPlay(vol());
|
||||||
}, 200);
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,16 +201,14 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
const onStop = () => {
|
const onStop = () => {
|
||||||
disarmGesture();
|
disarmGesture();
|
||||||
stopAudio();
|
stopAudio();
|
||||||
clearHint();
|
|
||||||
};
|
};
|
||||||
const teardown = () => {
|
const teardown = () => {
|
||||||
document.removeEventListener("visibilitychange", onVisibility);
|
document.removeEventListener("visibilitychange", onVisibility);
|
||||||
window.removeEventListener("pageshow", onUpdated);
|
window.removeEventListener("pageshow", onUpdated);
|
||||||
window.removeEventListener("betterseqta-background-music-updated", onUpdated);
|
window.removeEventListener("betterseqta-background-music-updated", onUpdated);
|
||||||
window.removeEventListener("betterseqta-background-music-stop", onStop);
|
window.removeEventListener("betterseqta-background-music-stop", onStop);
|
||||||
clearResumeTimer();
|
if (resumeTimer) clearTimeout(resumeTimer);
|
||||||
disarmGesture();
|
disarmGesture();
|
||||||
clearHint();
|
|
||||||
stopAudio();
|
stopAudio();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,4 @@
|
|||||||
font: 600 0.8125rem/1.25 system-ui, sans-serif;
|
font: 600 0.8125rem/1.25 system-ui, sans-serif;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
|
box-shadow: 0 8px 24px rgb(0 0 0 / 0.35);
|
||||||
animation: bsplus-bg-music-hint-in 220ms ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bsplus-bg-music-hint:hover {
|
|
||||||
background: color-mix(in srgb, var(--theme-secondary, #2a2a2a) 90%, var(--better-main, #22c55e) 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bsplus-bg-music-hint-in {
|
|
||||||
from { opacity: 0; transform: translateY(6px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,11 +260,12 @@ export async function applyStoreDiff(
|
|||||||
if (puts.length === 0 && removeKeys.length === 0) return;
|
if (puts.length === 0 && removeKeys.length === 0) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const db = await openDB();
|
let db = await openDB();
|
||||||
if (!db.objectStoreNames.contains(store)) {
|
if (!db.objectStoreNames.contains(store)) {
|
||||||
await upgradeDB(store);
|
await upgradeDB(store);
|
||||||
|
db = await openDB();
|
||||||
}
|
}
|
||||||
await runStoreDiffTransaction(await openDB(), store, puts, removeKeys);
|
await runStoreDiffTransaction(db, store, puts, removeKeys);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error in applyStoreDiff for store ${store}:`, error);
|
console.error(`Error in applyStoreDiff for store ${store}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { applyStoreDiff, get, getAll, put, remove } from "./db";
|
import { applyStoreDiff, get, getAll, put, remove } from "./db";
|
||||||
import { jobs } from "./jobs";
|
import { jobs } from "./jobs";
|
||||||
import { decorateIndexItems, publishDynamicItemsUpdate } from "./renderComponents";
|
import { decorateIndexItems } from "./renderComponents";
|
||||||
import type { IndexItem, Job, JobContext } from "./types";
|
import type { IndexItem, Job, JobContext } from "./types";
|
||||||
import { VectorWorkerManager } from "./worker/vectorWorkerManager";
|
import { VectorWorkerManager } from "./worker/vectorWorkerManager";
|
||||||
import { loadDynamicItems } from "../utils/dynamicItems";
|
import { loadDynamicItems } from "../utils/dynamicItems";
|
||||||
@@ -260,54 +260,25 @@ function dispatchVectorProgress(
|
|||||||
completedJobs: number,
|
completedJobs: number,
|
||||||
totalSteps: number,
|
totalSteps: number,
|
||||||
): number {
|
): number {
|
||||||
let detailMessage = progress.message || "";
|
const { status, total, processed, message = "" } = progress;
|
||||||
|
let detail = message;
|
||||||
let completed = completedJobs;
|
let completed = completedJobs;
|
||||||
|
|
||||||
if (
|
if (status === "processing" && total != null && processed != null) {
|
||||||
progress.status === "processing" &&
|
detail = `Vectorizing: ${processed} / ${total}`;
|
||||||
progress.total &&
|
} else if (status === "started") {
|
||||||
progress.processed !== undefined
|
detail = `Vectorization started for ${total} items`;
|
||||||
) {
|
} else if (status === "complete") {
|
||||||
detailMessage = `Vectorizing: ${progress.processed} / ${progress.total}`;
|
dispatchProgress(++completed, totalSteps, false, "Indexing finished", "Vectorization complete");
|
||||||
} else if (progress.status === "complete") {
|
|
||||||
detailMessage = "Vectorization complete";
|
|
||||||
completed++;
|
|
||||||
dispatchProgress(completed, totalSteps, false, "Indexing finished", detailMessage);
|
|
||||||
return completed;
|
return completed;
|
||||||
} else if (progress.status === "error") {
|
} else if (status === "error") {
|
||||||
dispatchProgress(
|
dispatchProgress(completed, totalSteps, false, "Vectorization failed", `Vectorization error: ${message}`);
|
||||||
completed,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Vectorization failed",
|
|
||||||
`Vectorization error: ${progress.message}`,
|
|
||||||
);
|
|
||||||
return completed;
|
return completed;
|
||||||
} else if (progress.status === "cancelled") {
|
} else if (status === "cancelled") {
|
||||||
dispatchProgress(
|
dispatchProgress(completed, totalSteps, false, "Vectorization cancelled", `Vectorization cancelled: ${message}`);
|
||||||
completed,
|
|
||||||
totalSteps,
|
|
||||||
false,
|
|
||||||
"Vectorization cancelled",
|
|
||||||
`Vectorization cancelled: ${progress.message}`,
|
|
||||||
);
|
|
||||||
return completed;
|
return completed;
|
||||||
} else if (progress.status === "started") {
|
} else {
|
||||||
detailMessage = `Vectorization started for ${progress.total} items`;
|
dispatchProgress(completed, totalSteps, true, "Vectorization in progress", detail);
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
progress.status !== "complete" &&
|
|
||||||
progress.status !== "error" &&
|
|
||||||
progress.status !== "cancelled"
|
|
||||||
) {
|
|
||||||
dispatchProgress(
|
|
||||||
completed,
|
|
||||||
totalSteps,
|
|
||||||
true,
|
|
||||||
"Vectorization in progress",
|
|
||||||
detailMessage,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return completed;
|
return completed;
|
||||||
@@ -322,10 +293,7 @@ export async function runIndexing(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ensureSchemaCurrent();
|
await ensureSchemaCurrent();
|
||||||
|
if (isIndexingPaused()) return;
|
||||||
if (isIndexingPaused()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await acquireLock())) {
|
if (!(await acquireLock())) {
|
||||||
verboseDebug(
|
verboseDebug(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { verboseDebug } from '@/utils/verboseLog';
|
|
||||||
|
|
||||||
const EMBEDDIA_DB = "embeddiaDB";
|
const EMBEDDIA_DB = "embeddiaDB";
|
||||||
const EMBEDDIA_STORE = "embeddiaObjectStore";
|
const EMBEDDIA_STORE = "embeddiaObjectStore";
|
||||||
|
|
||||||
@@ -13,13 +11,9 @@ function openEmbeddiaDb(): Promise<IDBDatabase | null> {
|
|||||||
|
|
||||||
export async function getVectorizedItemIds(): Promise<Set<string>> {
|
export async function getVectorizedItemIds(): Promise<Set<string>> {
|
||||||
const db = await openEmbeddiaDb();
|
const db = await openEmbeddiaDb();
|
||||||
if (!db) {
|
if (!db) return new Set();
|
||||||
verboseDebug("Could not open embeddiaDB, assuming no items are vectorized");
|
|
||||||
return new Set();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!db.objectStoreNames.contains(EMBEDDIA_STORE)) {
|
if (!db.objectStoreNames.contains(EMBEDDIA_STORE)) {
|
||||||
verboseDebug("embeddiaObjectStore not found, assuming no items are vectorized");
|
|
||||||
db.close();
|
db.close();
|
||||||
return new Set();
|
return new Set();
|
||||||
}
|
}
|
||||||
@@ -39,7 +33,6 @@ export async function getVectorizedItemIds(): Promise<Set<string>> {
|
|||||||
if (typeof key === "string") vectorizedIds.add(key);
|
if (typeof key === "string") vectorizedIds.add(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
verboseDebug(`Found ${vectorizedIds.size} already vectorized items in embeddia DB`);
|
|
||||||
db.close();
|
db.close();
|
||||||
return vectorizedIds;
|
return vectorizedIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -28,15 +28,23 @@ function shouldDedupeAsSameCourseSPA(item: IndexItem): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function programmeMetaclassIds(
|
||||||
|
item: IndexItem,
|
||||||
|
): { programme?: number; metaclass?: number } {
|
||||||
|
const md = item.metadata ?? {};
|
||||||
|
return {
|
||||||
|
programme: toFiniteNumber(
|
||||||
|
md.programme ?? md.programmeId ?? md.programmeID,
|
||||||
|
),
|
||||||
|
metaclass: toFiniteNumber(
|
||||||
|
md.metaclass ?? md.metaclassId ?? md.metaclassID ?? md.subjectId,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function courseDestinationKey(item: IndexItem): string | undefined {
|
export function courseDestinationKey(item: IndexItem): string | undefined {
|
||||||
if (!shouldDedupeAsSameCourseSPA(item)) return undefined;
|
if (!shouldDedupeAsSameCourseSPA(item)) return undefined;
|
||||||
const md = item.metadata ?? {};
|
const { programme, metaclass } = programmeMetaclassIds(item);
|
||||||
const programme = toFiniteNumber(
|
|
||||||
md.programme ?? md.programmeId ?? md.programmeID,
|
|
||||||
);
|
|
||||||
const metaclass = toFiniteNumber(
|
|
||||||
md.metaclass ?? md.metaclassId ?? md.metaclassID ?? md.subjectId,
|
|
||||||
);
|
|
||||||
if (programme === undefined || metaclass === undefined) return undefined;
|
if (programme === undefined || metaclass === undefined) return undefined;
|
||||||
return `course:${programme}:${metaclass}`;
|
return `course:${programme}:${metaclass}`;
|
||||||
}
|
}
|
||||||
@@ -74,13 +82,7 @@ function isPassiveLike(item: IndexItem): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hasProgrammeMetaclass(item: IndexItem): boolean {
|
function hasProgrammeMetaclass(item: IndexItem): boolean {
|
||||||
const md = item.metadata ?? {};
|
const { programme, metaclass } = programmeMetaclassIds(item);
|
||||||
const programme = toFiniteNumber(
|
|
||||||
md.programme ?? md.programmeId ?? md.programmeID,
|
|
||||||
);
|
|
||||||
const metaclass = toFiniteNumber(
|
|
||||||
md.metaclass ?? md.metaclassId ?? md.metaclassID ?? md.subjectId,
|
|
||||||
);
|
|
||||||
return programme !== undefined && metaclass !== undefined;
|
return programme !== undefined && metaclass !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,44 +168,29 @@ function dynamicSearchKey(row: CombinedResult): string | undefined {
|
|||||||
return searchDedupeKey(row.item as IndexItem);
|
return searchDedupeKey(row.item as IndexItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeCombinedDuplicates(
|
||||||
|
a: CombinedResult,
|
||||||
|
b: CombinedResult,
|
||||||
|
key: string,
|
||||||
|
): CombinedResult {
|
||||||
|
const aItem = a.item as IndexItem;
|
||||||
|
const bItem = b.item as IndexItem;
|
||||||
|
const winnerItem = pickBetterSearchDuplicate(aItem, bItem, key);
|
||||||
|
const envelope = winnerItem.id === aItem.id ? a : b;
|
||||||
|
return {
|
||||||
|
...envelope,
|
||||||
|
score: Math.max(a.score, b.score),
|
||||||
|
id: winnerItem.id,
|
||||||
|
item: winnerItem,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function dedupeCombinedResultsByCourseNav(
|
export function dedupeCombinedResultsByCourseNav(
|
||||||
results: CombinedResult[],
|
results: CombinedResult[],
|
||||||
): CombinedResult[] {
|
): CombinedResult[] {
|
||||||
const best = new Map<string, CombinedResult>();
|
return dedupeByCanonicalKey(
|
||||||
|
results,
|
||||||
for (const r of results) {
|
dynamicSearchKey,
|
||||||
const key = dynamicSearchKey(r);
|
mergeCombinedDuplicates,
|
||||||
if (!key) continue;
|
);
|
||||||
const prev = best.get(key);
|
|
||||||
if (!prev) {
|
|
||||||
best.set(key, r);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const aItem = prev.item as IndexItem;
|
|
||||||
const bItem = r.item as IndexItem;
|
|
||||||
const winnerItem = pickBetterSearchDuplicate(aItem, bItem, key);
|
|
||||||
const envelope = winnerItem.id === aItem.id ? prev : r;
|
|
||||||
best.set(key, {
|
|
||||||
...envelope,
|
|
||||||
score: Math.max(prev.score, r.score),
|
|
||||||
id: winnerItem.id,
|
|
||||||
item: winnerItem,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const seenCanon = new Set<string>();
|
|
||||||
const out: CombinedResult[] = [];
|
|
||||||
|
|
||||||
for (const r of results) {
|
|
||||||
const key = dynamicSearchKey(r);
|
|
||||||
if (!key) {
|
|
||||||
out.push(r);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (seenCanon.has(key)) continue;
|
|
||||||
seenCanon.add(key);
|
|
||||||
out.push(best.get(key)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
|
|||||||
});
|
});
|
||||||
menuObserver.observe(menuList, { childList: true });
|
menuObserver.observe(menuList, { childList: true });
|
||||||
|
|
||||||
const onClick = (e: Event) => {
|
analyticsItem.addEventListener("click", (e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (
|
if (
|
||||||
MenuOptionsOpen ||
|
MenuOptionsOpen ||
|
||||||
@@ -75,12 +75,10 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.history.pushState({}, "", "/#?page=/analytics");
|
window.history.pushState({}, "", "/#?page=/analytics");
|
||||||
void loadAnalyticsPage();
|
void loadAnalyticsPage();
|
||||||
};
|
});
|
||||||
analyticsItem.addEventListener("click", onClick);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
menuObserver.disconnect();
|
menuObserver.disconnect();
|
||||||
analyticsItem.removeEventListener("click", onClick);
|
|
||||||
analyticsItem.remove();
|
analyticsItem.remove();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,27 +1,17 @@
|
|||||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||||
|
|
||||||
|
type RawNotification = Record<string, unknown>;
|
||||||
|
|
||||||
export interface ArchivedNotification {
|
export interface ArchivedNotification {
|
||||||
notificationID: number;
|
notificationID: number;
|
||||||
type: string;
|
|
||||||
timestamp: string;
|
|
||||||
title: string;
|
|
||||||
subtitle: string;
|
|
||||||
messageID?: number;
|
|
||||||
assessmentID?: number;
|
|
||||||
programmeID?: number;
|
|
||||||
metaclassID?: number;
|
|
||||||
subjectCode?: string;
|
|
||||||
firstSavedAt: string;
|
firstSavedAt: string;
|
||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
raw: RawNotification;
|
raw: RawNotification;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ArchiveMap = Record<string, ArchivedNotification>;
|
export type ArchiveMap = Record<string, ArchivedNotification>;
|
||||||
|
|
||||||
export type ArchivesByUser = Record<string, ArchiveMap>;
|
export type ArchivesByUser = Record<string, ArchiveMap>;
|
||||||
|
|
||||||
type RawNotification = Record<string, unknown>;
|
|
||||||
|
|
||||||
export async function resolveNotificationUserKey(): Promise<string | null> {
|
export async function resolveNotificationUserKey(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const info = await getUserInfo();
|
const info = await getUserInfo();
|
||||||
@@ -45,165 +35,58 @@ export async function fetchAllNotifications(): Promise<RawNotification[]> {
|
|||||||
hash: "#?page=/notifications",
|
hash: "#?page=/notifications",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
|
|
||||||
const json = (await res.json()) as {
|
const json = (await res.json()) as {
|
||||||
notifications?: RawNotification[];
|
notifications?: RawNotification[];
|
||||||
payload?: { notifications?: RawNotification[] };
|
payload?: { notifications?: RawNotification[] };
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = json.notifications ?? json.payload?.notifications;
|
const list = json.notifications ?? json.payload?.notifications;
|
||||||
return Array.isArray(list) ? list : [];
|
return Array.isArray(list) ? list : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function readString(value: unknown): string {
|
function archiveTimestamp(
|
||||||
if (value == null) return "";
|
item: ArchivedNotification & { timestamp?: string },
|
||||||
return String(value).trim();
|
): number {
|
||||||
}
|
const ms = new Date(
|
||||||
|
String(item.raw?.timestamp ?? item.timestamp ?? 0),
|
||||||
export function normalizeArchivedNotification(
|
).getTime();
|
||||||
raw: RawNotification,
|
return Number.isNaN(ms) ? 0 : ms;
|
||||||
now = new Date().toISOString(),
|
|
||||||
): ArchivedNotification | null {
|
|
||||||
const notificationID = Number(raw.notificationID);
|
|
||||||
if (!notificationID || Number.isNaN(notificationID)) return null;
|
|
||||||
|
|
||||||
const type = readString(raw.type) || "unknown";
|
|
||||||
const timestamp = readString(raw.timestamp) || now;
|
|
||||||
|
|
||||||
if (type === "message" && raw.message && typeof raw.message === "object") {
|
|
||||||
const message = raw.message as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
notificationID,
|
|
||||||
type,
|
|
||||||
timestamp,
|
|
||||||
title: readString(message.title) || "Message",
|
|
||||||
subtitle: readString(message.subtitle),
|
|
||||||
messageID: Number(message.messageID) || undefined,
|
|
||||||
firstSavedAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
raw: { ...raw },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
type === "coneqtassessments" &&
|
|
||||||
raw.coneqtAssessments &&
|
|
||||||
typeof raw.coneqtAssessments === "object"
|
|
||||||
) {
|
|
||||||
const assessment = raw.coneqtAssessments as Record<string, unknown>;
|
|
||||||
return {
|
|
||||||
notificationID,
|
|
||||||
type,
|
|
||||||
timestamp,
|
|
||||||
title: readString(assessment.title) || "Assessment",
|
|
||||||
subtitle: readString(assessment.subtitle) || readString(assessment.subjectCode),
|
|
||||||
assessmentID: Number(assessment.assessmentID) || undefined,
|
|
||||||
programmeID: Number(assessment.programmeID) || undefined,
|
|
||||||
metaclassID: Number(assessment.metaclassID) || undefined,
|
|
||||||
subjectCode: readString(assessment.subjectCode) || undefined,
|
|
||||||
firstSavedAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
raw: { ...raw },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
notificationID,
|
|
||||||
type,
|
|
||||||
timestamp,
|
|
||||||
title: readString(raw.title) || "Notification",
|
|
||||||
subtitle: readString(raw.subtitle),
|
|
||||||
firstSavedAt: now,
|
|
||||||
lastSeenAt: now,
|
|
||||||
raw: { ...raw },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeNotificationsIntoArchive(
|
export function mergeNotificationsIntoArchive(
|
||||||
existing: ArchiveMap,
|
existing: ArchiveMap,
|
||||||
notifications: RawNotification[],
|
notifications: RawNotification[],
|
||||||
): ArchiveMap {
|
): { archive: ArchiveMap; changed: boolean } {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const merged: ArchiveMap = { ...existing };
|
let changed = false;
|
||||||
|
const archive = { ...existing };
|
||||||
|
|
||||||
for (const raw of notifications) {
|
for (const raw of notifications) {
|
||||||
const normalized = normalizeArchivedNotification(raw, now);
|
const notificationID = Number(raw.notificationID);
|
||||||
if (!normalized) continue;
|
if (!notificationID || Number.isNaN(notificationID)) continue;
|
||||||
|
|
||||||
const key = String(normalized.notificationID);
|
const key = String(notificationID);
|
||||||
const prev = merged[key];
|
const prev = archive[key];
|
||||||
if (prev) {
|
archive[key] = prev
|
||||||
merged[key] = {
|
? { ...prev, lastSeenAt: now, raw: { ...prev.raw, ...raw } }
|
||||||
...prev,
|
: { notificationID, firstSavedAt: now, lastSeenAt: now, raw: { ...raw } };
|
||||||
...normalized,
|
changed = true;
|
||||||
timestamp: normalized.timestamp || prev.timestamp,
|
|
||||||
firstSavedAt: prev.firstSavedAt,
|
|
||||||
lastSeenAt: now,
|
|
||||||
raw: { ...prev.raw, ...raw },
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
merged[key] = normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return merged;
|
return { archive, changed };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listArchivedNotifications(archive: ArchiveMap): ArchivedNotification[] {
|
export function listArchivedNotifications(
|
||||||
|
archive: ArchiveMap,
|
||||||
|
): ArchivedNotification[] {
|
||||||
return Object.values(archive).sort(
|
return Object.values(archive).sort(
|
||||||
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
(a, b) => archiveTimestamp(b) - archiveTimestamp(a),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function archivedToApiNotification(
|
export function archivedToApiNotification(
|
||||||
item: ArchivedNotification,
|
item: ArchivedNotification,
|
||||||
): RawNotification {
|
): RawNotification {
|
||||||
if (item.raw && typeof item.raw === "object") {
|
return { ...item.raw, notificationID: item.notificationID };
|
||||||
return {
|
|
||||||
...item.raw,
|
|
||||||
notificationID: item.notificationID,
|
|
||||||
type: item.type,
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "message") {
|
|
||||||
return {
|
|
||||||
notificationID: item.notificationID,
|
|
||||||
type: "message",
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
message: {
|
|
||||||
title: item.title,
|
|
||||||
subtitle: item.subtitle,
|
|
||||||
messageID: item.messageID,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === "coneqtassessments") {
|
|
||||||
return {
|
|
||||||
notificationID: item.notificationID,
|
|
||||||
type: "coneqtassessments",
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
coneqtAssessments: {
|
|
||||||
title: item.title,
|
|
||||||
subtitle: item.subtitle,
|
|
||||||
assessmentID: item.assessmentID,
|
|
||||||
programmeID: item.programmeID,
|
|
||||||
metaclassID: item.metaclassID,
|
|
||||||
subjectCode: item.subjectCode,
|
|
||||||
term: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
notificationID: item.notificationID,
|
|
||||||
type: item.type,
|
|
||||||
timestamp: item.timestamp,
|
|
||||||
title: item.title,
|
|
||||||
subtitle: item.subtitle,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import {
|
|||||||
} from "./injectArchivedNotifications";
|
} from "./injectArchivedNotifications";
|
||||||
import styles from "./styles.css?inline";
|
import styles from "./styles.css?inline";
|
||||||
|
|
||||||
|
const BUBBLE_SELECTOR = "[class*='notifications__bubble___']";
|
||||||
|
const LIST_SELECTOR = '[class*="notifications__list___"]';
|
||||||
|
|
||||||
const notificationCollectorSettings = {
|
const notificationCollectorSettings = {
|
||||||
saveLocally: booleanSetting({
|
saveLocally: booleanSetting({
|
||||||
default: true,
|
default: true,
|
||||||
@@ -60,15 +63,9 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
const baseInterval = 30000;
|
const baseInterval = 30000;
|
||||||
const maxInterval = 300000;
|
const maxInterval = 300000;
|
||||||
|
|
||||||
if (!api.storage.lastNotificationCount) {
|
api.storage.lastNotificationCount ||= 0;
|
||||||
api.storage.lastNotificationCount = 0;
|
api.storage.consecutiveErrors ||= 0;
|
||||||
}
|
api.storage.archivesByUser ||= {};
|
||||||
if (!api.storage.consecutiveErrors) {
|
|
||||||
api.storage.consecutiveErrors = 0;
|
|
||||||
}
|
|
||||||
if (!api.storage.archivesByUser) {
|
|
||||||
api.storage.archivesByUser = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncArchive = async () => {
|
const syncArchive = async () => {
|
||||||
if (!api.settings.saveLocally || archiveSyncInFlight) return;
|
if (!api.settings.saveLocally || archiveSyncInFlight) return;
|
||||||
@@ -81,12 +78,15 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
const notifications = await fetchAllNotifications();
|
const notifications = await fetchAllNotifications();
|
||||||
const archivesByUser = { ...(api.storage.archivesByUser ?? {}) };
|
const archivesByUser = { ...(api.storage.archivesByUser ?? {}) };
|
||||||
const existing = archivesByUser[userKey] ?? {};
|
const existing = archivesByUser[userKey] ?? {};
|
||||||
const merged = mergeNotificationsIntoArchive(existing, notifications);
|
const { archive: merged, changed } = mergeNotificationsIntoArchive(
|
||||||
|
existing,
|
||||||
|
notifications,
|
||||||
|
);
|
||||||
|
|
||||||
if (JSON.stringify(existing) !== JSON.stringify(merged)) {
|
if (changed) {
|
||||||
archivesByUser[userKey] = merged;
|
archivesByUser[userKey] = merged;
|
||||||
api.storage.archivesByUser = archivesByUser;
|
api.storage.archivesByUser = archivesByUser;
|
||||||
} else if (document.querySelector('[class*="notifications__list___"]')) {
|
} else if (document.querySelector(LIST_SELECTOR)) {
|
||||||
await injectArchivedForUser(merged);
|
await injectArchivedForUser(merged);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -100,9 +100,7 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
if (!isVisible) return;
|
if (!isVisible) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const alertDiv = document.querySelector(
|
const alertDiv = document.querySelector(BUBBLE_SELECTOR) as HTMLElement;
|
||||||
"[class*='notifications__bubble___']",
|
|
||||||
) as HTMLElement;
|
|
||||||
|
|
||||||
if (alertDiv && api.storage.lastNotificationCount !== 0) {
|
if (alertDiv && api.storage.lastNotificationCount !== 0) {
|
||||||
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
alertDiv.textContent = api.storage.lastNotificationCount.toString();
|
||||||
@@ -159,9 +157,7 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
window.clearTimeout(pollInterval);
|
window.clearTimeout(pollInterval);
|
||||||
pollInterval = null;
|
pollInterval = null;
|
||||||
const alertDiv = document.querySelector(
|
const alertDiv = document.querySelector(BUBBLE_SELECTOR) as HTMLElement;
|
||||||
"[class*='notifications__bubble___']",
|
|
||||||
) as HTMLElement;
|
|
||||||
if (alertDiv) {
|
if (alertDiv) {
|
||||||
if (api.storage.lastNotificationCount > 9) {
|
if (api.storage.lastNotificationCount > 9) {
|
||||||
alertDiv.textContent = "9+";
|
alertDiv.textContent = "9+";
|
||||||
@@ -175,10 +171,7 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
isVisible = !document.hidden;
|
isVisible = !document.hidden;
|
||||||
if (isVisible && !pollInterval) {
|
if (isVisible && !pollInterval) {
|
||||||
const alertDiv = document.querySelector(
|
if (document.querySelector(BUBBLE_SELECTOR)) startPolling();
|
||||||
"[class*='notifications__bubble___']",
|
|
||||||
);
|
|
||||||
if (alertDiv) startPolling();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,18 +188,16 @@ const notificationCollectorPlugin: Plugin<
|
|||||||
resolveNotificationUserKey,
|
resolveNotificationUserKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
api.seqta.onMount("[class*='notifications__bubble___']", () => {
|
const onBubbleMount = () => {
|
||||||
startPolling();
|
startPolling();
|
||||||
if (api.settings.saveLocally) {
|
if (api.settings.saveLocally) void syncArchive();
|
||||||
void syncArchive();
|
};
|
||||||
}
|
const onListMount = () => {
|
||||||
});
|
if (api.settings.saveLocally) void syncArchive();
|
||||||
|
};
|
||||||
|
|
||||||
api.seqta.onMount("[class*='notifications__list___']", () => {
|
api.seqta.onMount(BUBBLE_SELECTOR, onBubbleMount);
|
||||||
if (api.settings.saveLocally) {
|
api.seqta.onMount(LIST_SELECTOR, onListMount);
|
||||||
void syncArchive();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const ITEM_SELECTOR = '[class*="notifications__item___"]';
|
|||||||
const BACKED_UP_CLASS = "bsplus-notification-backed-up";
|
const BACKED_UP_CLASS = "bsplus-notification-backed-up";
|
||||||
const BACKUP_BADGE_CLASS = "bsplus-notification-backup-badge";
|
const BACKUP_BADGE_CLASS = "bsplus-notification-backup-badge";
|
||||||
|
|
||||||
const BACKUP_CHECK_SVG =
|
|
||||||
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>';
|
|
||||||
|
|
||||||
function notificationTimestamp(item: Record<string, unknown>): number {
|
function notificationTimestamp(item: Record<string, unknown>): number {
|
||||||
const ms = new Date(String(item.timestamp ?? 0)).getTime();
|
const ms = new Date(String(item.timestamp ?? 0)).getTime();
|
||||||
return Number.isNaN(ms) ? 0 : ms;
|
return Number.isNaN(ms) ? 0 : ms;
|
||||||
@@ -38,17 +35,6 @@ function mergeLiveWithArchived(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameItemOrder(
|
|
||||||
current: Record<string, unknown>[],
|
|
||||||
merged: Record<string, unknown>[],
|
|
||||||
): boolean {
|
|
||||||
if (current.length !== merged.length) return false;
|
|
||||||
return current.every(
|
|
||||||
(item, index) =>
|
|
||||||
Number(item.notificationID) === Number(merged[index]?.notificationID),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function tryInjectArchived(archive: ArchiveMap): Promise<boolean> {
|
async function tryInjectArchived(archive: ArchiveMap): Promise<boolean> {
|
||||||
if (!document.querySelector(LIST_SELECTOR)) return false;
|
if (!document.querySelector(LIST_SELECTOR)) return false;
|
||||||
|
|
||||||
@@ -58,16 +44,22 @@ async function tryInjectArchived(archive: ArchiveMap): Promise<boolean> {
|
|||||||
const liveItems = state.items as Record<string, unknown>[];
|
const liveItems = state.items as Record<string, unknown>[];
|
||||||
const merged = mergeLiveWithArchived(liveItems, archive);
|
const merged = mergeLiveWithArchived(liveItems, archive);
|
||||||
if (!merged) return true;
|
if (!merged) return true;
|
||||||
if (sameItemOrder(liveItems, merged)) return true;
|
|
||||||
|
const sameOrder =
|
||||||
|
liveItems.length === merged.length &&
|
||||||
|
liveItems.every(
|
||||||
|
(item, index) =>
|
||||||
|
Number(item.notificationID) === Number(merged[index]?.notificationID),
|
||||||
|
);
|
||||||
|
if (sameOrder) return true;
|
||||||
|
|
||||||
await ReactFiber.find(LIST_SELECTOR).setState({ items: merged });
|
await ReactFiber.find(LIST_SELECTOR).setState({ items: merged });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function injectWithRetries(archive: ArchiveMap, attempts = 10) {
|
async function injectWithRetries(archive: ArchiveMap) {
|
||||||
for (let i = 0; i < attempts; i++) {
|
for (let attempt = 0; attempt < 10; attempt++) {
|
||||||
const done = await tryInjectArchived(archive);
|
if (await tryInjectArchived(archive)) break;
|
||||||
if (done) break;
|
|
||||||
await delay(120);
|
await delay(120);
|
||||||
}
|
}
|
||||||
applyBackupBadges(archive);
|
applyBackupBadges(archive);
|
||||||
@@ -86,7 +78,7 @@ export function applyBackupBadges(archive: ArchiveMap) {
|
|||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
badge.className = BACKUP_BADGE_CLASS;
|
badge.className = BACKUP_BADGE_CLASS;
|
||||||
badge.title = "Saved locally";
|
badge.title = "Saved locally";
|
||||||
badge.innerHTML = BACKUP_CHECK_SVG;
|
badge.textContent = "✓";
|
||||||
itemEl.appendChild(badge);
|
itemEl.appendChild(badge);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -119,30 +111,23 @@ export function mountArchivedNotificationInjection(
|
|||||||
const watchItemsContainer = () => {
|
const watchItemsContainer = () => {
|
||||||
const itemsEl = document.querySelector(ITEMS_SELECTOR);
|
const itemsEl = document.querySelector(ITEMS_SELECTOR);
|
||||||
if (!itemsEl) return;
|
if (!itemsEl) return;
|
||||||
if (observer) observer.disconnect();
|
observer?.disconnect();
|
||||||
observer = new MutationObserver(() => scheduleInject());
|
observer = new MutationObserver(scheduleInject);
|
||||||
observer.observe(itemsEl, { childList: true });
|
observer.observe(itemsEl, { childList: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
api.seqta.onMount(LIST_SELECTOR, () => {
|
const onNotificationsMount = () => {
|
||||||
scheduleInject();
|
scheduleInject();
|
||||||
watchItemsContainer();
|
watchItemsContainer();
|
||||||
});
|
|
||||||
|
|
||||||
api.seqta.onMount(ITEMS_SELECTOR, () => {
|
|
||||||
watchItemsContainer();
|
|
||||||
scheduleInject();
|
|
||||||
});
|
|
||||||
|
|
||||||
api.storage.onChange("archivesByUser", () => scheduleInject());
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (observer) observer.disconnect();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.seqta.onMount(LIST_SELECTOR, onNotificationsMount);
|
||||||
|
api.seqta.onMount(ITEMS_SELECTOR, onNotificationsMount);
|
||||||
|
api.storage.onChange("archivesByUser", scheduleInject);
|
||||||
|
|
||||||
|
return () => observer?.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectArchivedForUser(
|
export async function injectArchivedForUser(archive: ArchiveMap): Promise<void> {
|
||||||
archive: ArchiveMap,
|
|
||||||
): Promise<void> {
|
|
||||||
await injectWithRetries(archive);
|
await injectWithRetries(archive);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,16 +10,11 @@
|
|||||||
height: 14px;
|
height: 14px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--better-main, #22c55e);
|
background: var(--better-main, #22c55e);
|
||||||
display: flex;
|
color: #fff;
|
||||||
align-items: center;
|
font-size: 9px;
|
||||||
justify-content: center;
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bsplus-notification-backup-badge svg {
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { SettingsState } from "@/types/storage";
|
import type { SettingsState } from "@/types/storage";
|
||||||
import { settingsState } from "../listeners/SettingsState";
|
import { settingsState } from "../listeners/SettingsState";
|
||||||
import { applyMenuItemVisibility } from "../menuItemVisibility";
|
import { applyMenuItemVisibility } from "../menuItemVisibility";
|
||||||
import { insertKeyAfterInOrder } from "@/seqta/utils/sidebarMenuIcons";
|
import { ensureAnalyticsMenuOrder } from "@/seqta/utils/sidebarMenuIcons";
|
||||||
import stringToHTML from "../stringToHTML";
|
import stringToHTML from "../stringToHTML";
|
||||||
import Sortable from "sortablejs";
|
import Sortable from "sortablejs";
|
||||||
|
|
||||||
@@ -30,20 +30,10 @@ function syncDefaultMenuOrder(menu: HTMLElement) {
|
|||||||
for (let i = 0; i < childnodes.length; i++) {
|
for (let i = 0; i < childnodes.length; i++) {
|
||||||
const key = (childnodes[i] as HTMLElement).dataset.key;
|
const key = (childnodes[i] as HTMLElement).dataset.key;
|
||||||
if (key && settingsState.defaultmenuorder.indexOf(key) === -1) {
|
if (key && settingsState.defaultmenuorder.indexOf(key) === -1) {
|
||||||
if (key === "analytics") {
|
settingsState.defaultmenuorder = [...settingsState.defaultmenuorder, key];
|
||||||
settingsState.defaultmenuorder = insertKeyAfterInOrder(
|
|
||||||
settingsState.defaultmenuorder,
|
|
||||||
key,
|
|
||||||
"courses",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
settingsState.defaultmenuorder = [
|
|
||||||
...settingsState.defaultmenuorder,
|
|
||||||
key,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ensureAnalyticsMenuOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeMenuItemsFromDom(
|
function mergeMenuItemsFromDom(
|
||||||
@@ -242,7 +232,6 @@ export function OpenMenuOptions() {
|
|||||||
if (sortable) {
|
if (sortable) {
|
||||||
saveNewOrder(sortable);
|
saveNewOrder(sortable);
|
||||||
}
|
}
|
||||||
applyMenuItemVisibility();
|
|
||||||
closeAll();
|
closeAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { settingsState } from "./listeners/SettingsState";
|
import { settingsState } from "./listeners/SettingsState";
|
||||||
import { verboseInfo } from "@/utils/verboseLog";
|
|
||||||
|
|
||||||
const STYLE_ID = "bsplus-menuitem-visibility";
|
const STYLE_ID = "bsplus-menuitem-visibility";
|
||||||
|
|
||||||
/** Whether a sidebar key is hidden via Edit Sidebar toggles. */
|
|
||||||
export function isMenuItemHidden(key: string): boolean {
|
|
||||||
const entry = (settingsState.menuitems as Record<string, { toggle?: boolean }>)?.[key];
|
|
||||||
return entry != null && entry.toggle === false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Apply hide rules from `menuitems` (re-runnable after edit / storage sync). */
|
/** Apply hide rules from `menuitems` (re-runnable after edit / storage sync). */
|
||||||
export function applyMenuItemVisibility(): void {
|
export function applyMenuItemVisibility(): void {
|
||||||
if (document.querySelector(".editmenuoption-container")) return;
|
if (document.querySelector(".editmenuoption-container")) return;
|
||||||
@@ -18,7 +11,6 @@ export function applyMenuItemVisibility(): void {
|
|||||||
for (const [menuItem, config] of Object.entries(settingsState.menuitems ?? {})) {
|
for (const [menuItem, config] of Object.entries(settingsState.menuitems ?? {})) {
|
||||||
if (config && !config.toggle) {
|
if (config && !config.toggle) {
|
||||||
css += `li[data-key=${menuItem}],section[data-key=${menuItem}]{display:var(--menuHidden) !important;transition:1s;}`;
|
css += `li[data-key=${menuItem}],section[data-key=${menuItem}]{display:var(--menuHidden) !important;transition:1s;}`;
|
||||||
verboseInfo(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,109 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
|||||||
import { noticeMatchesLabelFilter } from "@/seqta/utils/notices/noticeLabelFilters";
|
import { noticeMatchesLabelFilter } from "@/seqta/utils/notices/noticeLabelFilters";
|
||||||
import stringToHTML from "@/seqta/utils/stringToHTML";
|
import stringToHTML from "@/seqta/utils/stringToHTML";
|
||||||
|
|
||||||
|
const PLACEHOLDER_RE = /\[\[[\w]+[:][\w]+[\]\]]+/g;
|
||||||
|
const SPRING_OPEN = { type: "spring" as const, stiffness: 280, damping: 24, duration: 0.5 };
|
||||||
|
const SPRING_CLOSE = { type: "spring" as const, stiffness: 400, damping: 35, duration: 0.35 };
|
||||||
|
|
||||||
|
const colourStr = (colour?: string) => colour || "#8e8e8e";
|
||||||
|
|
||||||
|
function stripPlaceholders(html: string): string {
|
||||||
|
return html.replace(PLACEHOLDER_RE, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function noticePreview(contents: string): string {
|
||||||
|
const text = stripPlaceholders(contents)
|
||||||
|
.replace(/<[^>]*>/g, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
return text.substring(0, 150) + (contents.length > 150 ? "..." : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function noticeBody(contents: string): string {
|
||||||
|
return stripPlaceholders(contents).replace(/ +/, " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
type NoticeCardOpts = {
|
||||||
|
wrapperClass?: string;
|
||||||
|
wrapperStyle?: string;
|
||||||
|
hideClose?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function noticeCardHtml(
|
||||||
|
notice: { title: string; staff: string; label_title?: string },
|
||||||
|
colour: string | undefined,
|
||||||
|
body: string,
|
||||||
|
opts: NoticeCardOpts = {},
|
||||||
|
): string {
|
||||||
|
const c = colourStr(colour);
|
||||||
|
const closeBtn = opts.hideClose
|
||||||
|
? '<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">×</button>'
|
||||||
|
: '<button class="notice-close-btn">×</button>';
|
||||||
|
return `<div class="notice-unified-content ${opts.wrapperClass ?? "notice-card-state"}" style="--colour: ${c}; ${opts.wrapperStyle ?? ""}">
|
||||||
|
<div class="notice-header">
|
||||||
|
<div class="notice-badge-row">
|
||||||
|
<span class="notice-badge" style="background: linear-gradient(135deg, ${c}, ${c}dd); color: white;">${notice.label_title || "General"}</span>
|
||||||
|
<span class="notice-staff">${notice.staff}</span>
|
||||||
|
</div>
|
||||||
|
${closeBtn}
|
||||||
|
</div>
|
||||||
|
<h2 class="notice-content-title">${notice.title}</h2>
|
||||||
|
<div class="notice-content-body">${body}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modalTargetSize(sourceWidth: number, contentHeight: number) {
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const scrollY = Math.round(window.scrollY);
|
||||||
|
const width = Math.round(Math.min(Math.max(sourceWidth, 800), vw - 40));
|
||||||
|
const height = Math.round(Math.min(Math.max(contentHeight + 32, 200), vh * 0.9));
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
left: Math.round((vw - width) / 2),
|
||||||
|
top: Math.round((vh - height) / 2) + scrollY,
|
||||||
|
scrollX: Math.round(window.scrollX),
|
||||||
|
scrollY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureNoticeHeight(
|
||||||
|
notice: { title: string; staff: string; label_title?: string },
|
||||||
|
body: string,
|
||||||
|
targetWidth: number,
|
||||||
|
): number {
|
||||||
|
const measure = document.createElement("div");
|
||||||
|
measure.style.cssText = `position:absolute;left:-9999px;width:${targetWidth}px;visibility:hidden`;
|
||||||
|
measure.innerHTML = noticeCardHtml(notice, undefined, body, {
|
||||||
|
wrapperClass: "notice-modal-state",
|
||||||
|
wrapperStyle:
|
||||||
|
"position:relative;width:100%;padding:16px;border:1px solid rgba(255,255,255,0.1)",
|
||||||
|
});
|
||||||
|
document.body.appendChild(measure);
|
||||||
|
const height = measure.firstElementChild!.getBoundingClientRect().height;
|
||||||
|
measure.remove();
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSourceElement(el: HTMLElement) {
|
||||||
|
el.style.opacity = "1";
|
||||||
|
el.style.transform = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementScale(el: HTMLElement) {
|
||||||
|
const transform = getComputedStyle(el).transform;
|
||||||
|
if (!transform || transform === "none") return { x: 1, y: 1 };
|
||||||
|
const match = transform.match(/matrix.*\((.+)\)/);
|
||||||
|
if (!match) return { x: 1, y: 1 };
|
||||||
|
const values = match[1].split(", ");
|
||||||
|
return { x: parseFloat(values[0]), y: parseFloat(values[3]) };
|
||||||
|
}
|
||||||
|
|
||||||
export function processNoticeColor(colour: unknown): string | undefined {
|
export function processNoticeColor(colour: unknown): string | undefined {
|
||||||
if (typeof colour !== "string") return undefined;
|
if (typeof colour !== "string") return undefined;
|
||||||
const rgb = GetThresholdOfColor(colour);
|
const rgb = GetThresholdOfColor(colour);
|
||||||
if (rgb < 100 && settingsState.DarkMode) {
|
if (rgb < 100 && settingsState.DarkMode) return undefined;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return colour;
|
return colour;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,35 +126,13 @@ export function appendNoticeEmptyState(container: HTMLElement, message: string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
function createNoticeElement(notice: any, colour: string | undefined): Node {
|
||||||
const textPreview =
|
const htmlContent = noticeCardHtml(notice, colour, noticePreview(notice.contents), {
|
||||||
notice.contents
|
wrapperStyle:
|
||||||
.replace(/<[^>]*>/g, "")
|
"position: relative; background: var(--background-primary); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);",
|
||||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
hideClose: true,
|
||||||
.replace(/\s+/g, " ")
|
});
|
||||||
.trim()
|
|
||||||
.substring(0, 150) + (notice.contents.length > 150 ? "..." : "");
|
|
||||||
|
|
||||||
const noticeId = `notice-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
|
|
||||||
const htmlContent = `
|
|
||||||
<div class="notice-unified-content notice-card-state" data-notice-id="${noticeId}" style="--colour: ${colour || "#8e8e8e"}; position: relative; background: var(--background-primary); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.1);">
|
|
||||||
<div class="notice-header">
|
|
||||||
<div class="notice-badge-row">
|
|
||||||
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
|
||||||
${notice.label_title || "General"}
|
|
||||||
</span>
|
|
||||||
<span class="notice-staff">${notice.staff}</span>
|
|
||||||
</div>
|
|
||||||
<button class="notice-close-btn" style="opacity: 0; pointer-events: none;">×</button>
|
|
||||||
</div>
|
|
||||||
<h2 class="notice-content-title">${notice.title}</h2>
|
|
||||||
<div class="notice-content-body">${textPreview}</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
|
const element = stringToHTML(htmlContent).firstChild as HTMLElement;
|
||||||
element.addEventListener("click", () =>
|
element.addEventListener("click", () => openNoticeModal(notice, colour, element));
|
||||||
openNoticeModal(notice, colour, element),
|
|
||||||
);
|
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,10 +141,7 @@ export function openNoticeModal(
|
|||||||
colour: string | undefined,
|
colour: string | undefined,
|
||||||
sourceElement: HTMLElement,
|
sourceElement: HTMLElement,
|
||||||
) {
|
) {
|
||||||
const cleanContent = notice.contents
|
const cleanContent = noticeBody(notice.contents);
|
||||||
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
|
|
||||||
.replace(/ +/, " ");
|
|
||||||
|
|
||||||
document.getElementById("notice-modal")?.remove();
|
document.getElementById("notice-modal")?.remove();
|
||||||
|
|
||||||
const sourceRect = sourceElement.getBoundingClientRect();
|
const sourceRect = sourceElement.getBoundingClientRect();
|
||||||
@@ -80,84 +152,37 @@ export function openNoticeModal(
|
|||||||
let sourceWidth = sourceRect.width;
|
let sourceWidth = sourceRect.width;
|
||||||
let sourceHeight = sourceRect.height;
|
let sourceHeight = sourceRect.height;
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `<div id="notice-modal" class="notice-modal-overlay" style="opacity: 0;">
|
||||||
<div id="notice-modal" class="notice-modal-overlay" style="opacity: 0;">
|
<div class="notice-modal-transition" style="position:fixed;left:${sourceLeft + scrollX}px;top:${sourceTop + scrollY}px;width:${sourceWidth}px;height:${sourceHeight}px;transform-origin:center;z-index:10001;">
|
||||||
<div class="notice-modal-transition" style="
|
|
||||||
position: fixed;
|
|
||||||
left: ${sourceLeft + scrollX}px;
|
|
||||||
top: ${sourceTop + scrollY}px;
|
|
||||||
width: ${sourceWidth}px;
|
|
||||||
height: ${sourceHeight}px;
|
|
||||||
transform-origin: center;
|
|
||||||
z-index: 10001;
|
|
||||||
">
|
|
||||||
<div class="notice-modal-content notice-transitioning">
|
<div class="notice-modal-content notice-transitioning">
|
||||||
<div class="notice-unified-content notice-card-state">
|
${noticeCardHtml(notice, colour, cleanContent)}
|
||||||
<div class="notice-header">
|
|
||||||
<div class="notice-badge-row">
|
|
||||||
<span class="notice-badge" style="background: linear-gradient(135deg, ${colour || "#8e8e8e"}, ${colour || "#8e8e8e"}dd); color: white;">
|
|
||||||
${notice.label_title || "General"}
|
|
||||||
</span>
|
|
||||||
<span class="notice-staff">${notice.staff}</span>
|
|
||||||
</div>
|
|
||||||
<button class="notice-close-btn">×</button>
|
|
||||||
</div>
|
|
||||||
<h2 class="notice-content-title">${notice.title}</h2>
|
|
||||||
<div class="notice-content-body">${cleanContent}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const modal = stringToHTML(modalHtml).firstChild as HTMLElement;
|
const modal = stringToHTML(modalHtml).firstChild as HTMLElement;
|
||||||
const transitionContainer = modal.querySelector(
|
const transitionContainer = modal.querySelector(".notice-modal-transition") as HTMLElement;
|
||||||
".notice-modal-transition",
|
const unifiedContent = modal.querySelector(".notice-unified-content") as HTMLElement;
|
||||||
) as HTMLElement;
|
|
||||||
const unifiedContent = modal.querySelector(
|
|
||||||
".notice-unified-content",
|
|
||||||
) as HTMLElement;
|
|
||||||
const closeBtn = modal.querySelector(".notice-close-btn") as HTMLElement;
|
const closeBtn = modal.querySelector(".notice-close-btn") as HTMLElement;
|
||||||
|
|
||||||
document.body.appendChild(modal);
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
sourceElement.setAttribute("data-transitioning", "true");
|
sourceElement.setAttribute("data-transitioning", "true");
|
||||||
sourceElement.style.opacity = "0";
|
sourceElement.style.opacity = "0";
|
||||||
sourceElement.style.transform = "scale(0.95)";
|
sourceElement.style.transform = "scale(0.95)";
|
||||||
|
|
||||||
const viewportWidth = window.innerWidth;
|
const initialWidth = Math.round(
|
||||||
const viewportHeight = window.innerHeight;
|
Math.min(Math.max(sourceWidth, 800), window.innerWidth - 40),
|
||||||
let targetWidth = Math.round(
|
|
||||||
Math.min(Math.max(sourceWidth, 800), viewportWidth - 40),
|
|
||||||
);
|
);
|
||||||
|
const measuredHeight = measureNoticeHeight(notice, cleanContent, initialWidth);
|
||||||
|
let { width: targetWidth, height: targetHeight, left: targetLeft, top: targetTop } =
|
||||||
|
modalTargetSize(sourceWidth, measuredHeight);
|
||||||
|
|
||||||
const tempMeasureDiv = document.createElement("div");
|
const applyTargetLayout = () => {
|
||||||
tempMeasureDiv.style.position = "absolute";
|
transitionContainer.style.left = `${Math.round(targetLeft + scrollX)}px`;
|
||||||
tempMeasureDiv.style.left = "-9999px";
|
transitionContainer.style.top = `${Math.round(targetTop)}px`;
|
||||||
tempMeasureDiv.style.width = targetWidth + "px";
|
transitionContainer.style.width = `${Math.round(targetWidth)}px`;
|
||||||
tempMeasureDiv.style.visibility = "hidden";
|
transitionContainer.style.height = `${Math.round(targetHeight)}px`;
|
||||||
tempMeasureDiv.innerHTML = `
|
};
|
||||||
<div class="notice-unified-content notice-modal-state" style="position: relative; width: 100%; padding: 16px; border: 1px solid rgba(255, 255, 255, 0.1);">
|
|
||||||
<div class="notice-header">
|
|
||||||
<div class="notice-badge-row">
|
|
||||||
<span class="notice-badge">${notice.label_title || "General"}</span>
|
|
||||||
<span class="notice-staff">${notice.staff}</span>
|
|
||||||
</div>
|
|
||||||
<button class="notice-close-btn">×</button>
|
|
||||||
</div>
|
|
||||||
<h2 class="notice-content-title">${notice.title}</h2>
|
|
||||||
<div class="notice-content-body">${cleanContent}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(tempMeasureDiv);
|
|
||||||
const measuredHeight =
|
|
||||||
tempMeasureDiv.firstElementChild!.getBoundingClientRect().height;
|
|
||||||
document.body.removeChild(tempMeasureDiv);
|
|
||||||
|
|
||||||
let targetHeight = Math.round(
|
|
||||||
Math.min(Math.max(measuredHeight + 32, 200), viewportHeight * 0.9),
|
|
||||||
);
|
|
||||||
let targetLeft = Math.round((viewportWidth - targetWidth) / 2);
|
|
||||||
let targetTop = Math.round((viewportHeight - targetHeight) / 2) + scrollY;
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
window.removeEventListener("resize", handleResize);
|
window.removeEventListener("resize", handleResize);
|
||||||
@@ -165,8 +190,7 @@ export function openNoticeModal(
|
|||||||
|
|
||||||
if (!settingsState.animations) {
|
if (!settingsState.animations) {
|
||||||
modal.remove();
|
modal.remove();
|
||||||
sourceElement.style.opacity = "1";
|
showSourceElement(sourceElement);
|
||||||
sourceElement.style.transform = "";
|
|
||||||
sourceElement.removeAttribute("data-transitioning");
|
sourceElement.removeAttribute("data-transitioning");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,146 +203,65 @@ export function openNoticeModal(
|
|||||||
},
|
},
|
||||||
{ duration: 0.2 },
|
{ duration: 0.2 },
|
||||||
);
|
);
|
||||||
|
animate(transitionContainer, { opacity: [1, 0] }, { duration: 0.2, delay: 0.3 });
|
||||||
animate(
|
showSourceElement(sourceElement);
|
||||||
transitionContainer,
|
|
||||||
{ opacity: [1, 0] },
|
|
||||||
{ duration: 0.2, delay: 0.3 },
|
|
||||||
);
|
|
||||||
|
|
||||||
sourceElement.style.opacity = "1";
|
|
||||||
sourceElement.style.transform = "";
|
|
||||||
|
|
||||||
modal.style.pointerEvents = "none";
|
modal.style.pointerEvents = "none";
|
||||||
|
animate(transitionContainer, {
|
||||||
animate(
|
left: [targetLeft + scrollX, sourceLeft + scrollX],
|
||||||
transitionContainer,
|
top: [targetTop, sourceTop + scrollY],
|
||||||
{
|
width: [targetWidth, sourceWidth],
|
||||||
left: [targetLeft + scrollX, sourceLeft + scrollX],
|
height: [targetHeight, sourceHeight],
|
||||||
top: [targetTop, sourceTop + scrollY],
|
}, SPRING_CLOSE).finished.then(() => {
|
||||||
width: [targetWidth, sourceWidth],
|
|
||||||
height: [targetHeight, sourceHeight],
|
|
||||||
scale: [1, 1],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 0.35,
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 400,
|
|
||||||
damping: 35,
|
|
||||||
},
|
|
||||||
).finished.then(async () => {
|
|
||||||
modal.remove();
|
modal.remove();
|
||||||
sourceElement.removeAttribute("data-transitioning");
|
sourceElement.removeAttribute("data-transitioning");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
closeBtn?.addEventListener("click", closeModal);
|
closeBtn.addEventListener("click", closeModal);
|
||||||
modal?.addEventListener("click", (e) => {
|
modal.addEventListener("click", (e) => {
|
||||||
if (e.target === modal) {
|
if (e.target === modal) closeModal();
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") closeModal();
|
||||||
closeModal();
|
|
||||||
document.removeEventListener("keydown", handleEscape);
|
|
||||||
window.removeEventListener("resize", handleResize);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
document.addEventListener("keydown", handleEscape);
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const newSourceRect = sourceElement.getBoundingClientRect();
|
const rect = sourceElement.getBoundingClientRect();
|
||||||
const newScrollY = Math.round(window.scrollY);
|
scrollY = Math.round(window.scrollY);
|
||||||
const newScrollX = Math.round(window.scrollX);
|
scrollX = Math.round(window.scrollX);
|
||||||
|
const scale = elementScale(sourceElement);
|
||||||
|
sourceWidth = rect.width / scale.x;
|
||||||
|
sourceHeight = rect.height / scale.y;
|
||||||
|
sourceLeft = rect.left - (sourceWidth - rect.width) / 2;
|
||||||
|
sourceTop = rect.top - (sourceHeight - rect.height) / 2;
|
||||||
|
|
||||||
const computedStyle = getComputedStyle(sourceElement);
|
const next = modalTargetSize(
|
||||||
const transform = computedStyle.transform;
|
sourceWidth,
|
||||||
let scaleX = 1,
|
unifiedContent.getBoundingClientRect().height,
|
||||||
scaleY = 1;
|
|
||||||
|
|
||||||
if (transform && transform !== "none") {
|
|
||||||
const matrix = transform.match(/matrix.*\((.+)\)/);
|
|
||||||
if (matrix) {
|
|
||||||
const values = matrix[1].split(", ");
|
|
||||||
scaleX = parseFloat(values[0]);
|
|
||||||
scaleY = parseFloat(values[3]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newSourceWidth = newSourceRect.width / scaleX;
|
|
||||||
const newSourceHeight = newSourceRect.height / scaleY;
|
|
||||||
|
|
||||||
const deltaX = (newSourceWidth - newSourceRect.width) / 2;
|
|
||||||
const deltaY = (newSourceHeight - newSourceRect.height) / 2;
|
|
||||||
|
|
||||||
const newSourceLeft = newSourceRect.left - deltaX;
|
|
||||||
const newSourceTop = newSourceRect.top - deltaY;
|
|
||||||
|
|
||||||
const newViewportWidth = window.innerWidth;
|
|
||||||
const newViewportHeight = window.innerHeight;
|
|
||||||
const newTargetWidth = Math.round(
|
|
||||||
Math.min(Math.max(newSourceWidth, 800), newViewportWidth - 40),
|
|
||||||
);
|
);
|
||||||
const currentHeight = unifiedContent.getBoundingClientRect().height;
|
targetLeft = next.left;
|
||||||
const newTargetHeight = Math.round(
|
targetTop = next.top;
|
||||||
Math.min(Math.max(currentHeight + 32, 200), newViewportHeight * 0.9),
|
targetWidth = next.width;
|
||||||
);
|
targetHeight = next.height;
|
||||||
const newTargetLeft = Math.round((newViewportWidth - newTargetWidth) / 2);
|
applyTargetLayout();
|
||||||
const newTargetTop =
|
|
||||||
Math.round((newViewportHeight - newTargetHeight) / 2) + newScrollY;
|
|
||||||
|
|
||||||
transitionContainer.style.left =
|
|
||||||
Math.round(newTargetLeft + newScrollX) + "px";
|
|
||||||
transitionContainer.style.top = Math.round(newTargetTop) + "px";
|
|
||||||
transitionContainer.style.width = Math.round(newTargetWidth) + "px";
|
|
||||||
transitionContainer.style.height = Math.round(newTargetHeight) + "px";
|
|
||||||
|
|
||||||
sourceLeft = newSourceLeft;
|
|
||||||
sourceTop = newSourceTop;
|
|
||||||
sourceWidth = newSourceWidth;
|
|
||||||
sourceHeight = newSourceHeight;
|
|
||||||
targetLeft = newTargetLeft;
|
|
||||||
targetTop = newTargetTop;
|
|
||||||
targetWidth = newTargetWidth;
|
|
||||||
targetHeight = newTargetHeight;
|
|
||||||
scrollY = newScrollY;
|
|
||||||
scrollX = newScrollX;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("resize", handleResize);
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
unifiedContent.classList.replace("notice-card-state", "notice-modal-state");
|
||||||
if (settingsState.animations) {
|
if (settingsState.animations) {
|
||||||
animate(modal, { opacity: [0, 1] }, { duration: 0.2 });
|
animate(modal, { opacity: [0, 1] }, { duration: 0.2 });
|
||||||
|
animate(transitionContainer, {
|
||||||
animate(
|
left: [sourceLeft + scrollX, targetLeft + scrollX],
|
||||||
transitionContainer,
|
top: [sourceTop + scrollY, targetTop],
|
||||||
{
|
width: [sourceWidth, targetWidth],
|
||||||
left: [sourceLeft + scrollX, targetLeft + scrollX],
|
height: [sourceHeight, targetHeight],
|
||||||
top: [sourceTop + scrollY, targetTop],
|
}, SPRING_OPEN);
|
||||||
width: [sourceWidth, targetWidth],
|
|
||||||
height: [sourceHeight, targetHeight],
|
|
||||||
scale: [1, 1],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
duration: 0.5,
|
|
||||||
type: "spring",
|
|
||||||
stiffness: 280,
|
|
||||||
damping: 24,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
unifiedContent.classList.remove("notice-card-state");
|
|
||||||
unifiedContent.classList.add("notice-modal-state");
|
|
||||||
} else {
|
} else {
|
||||||
modal.style.opacity = "1";
|
modal.style.opacity = "1";
|
||||||
transitionContainer.style.left = Math.round(targetLeft + scrollX) + "px";
|
applyTargetLayout();
|
||||||
transitionContainer.style.top = Math.round(targetTop) + "px";
|
|
||||||
transitionContainer.style.width = Math.round(targetWidth) + "px";
|
|
||||||
transitionContainer.style.height = Math.round(targetHeight) + "px";
|
|
||||||
unifiedContent.classList.remove("notice-card-state");
|
|
||||||
unifiedContent.classList.add("notice-modal-state");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,22 +284,19 @@ export function renderNoticesIntoContainer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (const notice of notices) {
|
||||||
notices.forEach((notice: any) => {
|
if (
|
||||||
const shouldInclude =
|
settingsState.mockNotices ||
|
||||||
settingsState.mockNotices || noticeMatchesLabelFilter(notice, labelTokens);
|
noticeMatchesLabelFilter(notice, labelTokens)
|
||||||
|
) {
|
||||||
if (shouldInclude) {
|
fragment.appendChild(createNoticeElement(notice, processNoticeColor(notice.colour)));
|
||||||
const colour = processNoticeColor(notice.colour);
|
|
||||||
fragment.appendChild(createNoticeElement(notice, colour));
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
if (fragment.childNodes.length === 0) {
|
if (!fragment.childNodes.length) {
|
||||||
appendNoticeEmptyState(noticeContainer, emptyMessage);
|
appendNoticeEmptyState(noticeContainer, emptyMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
noticeContainer.appendChild(fragment);
|
noticeContainer.appendChild(fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,8 +312,9 @@ export async function fetchNoticesForDate(
|
|||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let data: { payload?: unknown };
|
||||||
try {
|
try {
|
||||||
const data = settingsState.mockNotices
|
data = settingsState.mockNotices
|
||||||
? getMockNotices()
|
? getMockNotices()
|
||||||
: await (
|
: await (
|
||||||
await fetch(noticesUrl, {
|
await fetch(noticesUrl, {
|
||||||
@@ -383,11 +324,10 @@ export async function fetchNoticesForDate(
|
|||||||
body: JSON.stringify({ date }),
|
body: JSON.stringify({ date }),
|
||||||
})
|
})
|
||||||
).json();
|
).json();
|
||||||
|
|
||||||
renderNoticesIntoContainer(containerId, data, labelTokens);
|
|
||||||
} catch {
|
} catch {
|
||||||
renderNoticesIntoContainer(containerId, { payload: [] }, labelTokens);
|
data = { payload: [] };
|
||||||
}
|
}
|
||||||
|
renderNoticesIntoContainer(containerId, data, labelTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SetupNoticesSectionOptions = {
|
export type SetupNoticesSectionOptions = {
|
||||||
@@ -405,9 +345,7 @@ export function setupNoticesSection(options: SetupNoticesSectionOptions): () =>
|
|||||||
? (document.querySelector(options.dateInput) as HTMLInputElement | null)
|
? (document.querySelector(options.dateInput) as HTMLInputElement | null)
|
||||||
: options.dateInput;
|
: options.dateInput;
|
||||||
|
|
||||||
if (dateControl) {
|
if (dateControl) dateControl.value = options.initialDate;
|
||||||
dateControl.value = options.initialDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
const debouncedInputChange = debounce((e: Event) => {
|
const debouncedInputChange = debounce((e: Event) => {
|
||||||
void fetchNoticesForDate(
|
void fetchNoticesForDate(
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
type PrefEntry = { name?: string; value?: unknown };
|
type PrefEntry = { name?: string; value?: unknown };
|
||||||
|
|
||||||
|
const NOTICES_FILTER = "notices.filters";
|
||||||
|
const JSON_HEADERS = { "Content-Type": "application/json; charset=utf-8" };
|
||||||
|
|
||||||
/** Parse `notices.filters` pref (space-separated label IDs). */
|
/** Parse `notices.filters` pref (space-separated label IDs). */
|
||||||
export function parseNoticesFilterPref(prefsPayload: unknown): string[] {
|
export function parseNoticesFilterPref(prefsPayload: unknown): string[] {
|
||||||
if (!Array.isArray(prefsPayload)) return [];
|
if (!Array.isArray(prefsPayload)) return [];
|
||||||
const values = (prefsPayload as PrefEntry[])
|
const raw = (prefsPayload as PrefEntry[]).find((item) => item?.name === NOTICES_FILTER)?.value;
|
||||||
.filter((item) => item?.name === "notices.filters")
|
return typeof raw === "string" ? raw.split(" ").filter(Boolean) : [];
|
||||||
.map((item) => item?.value)
|
|
||||||
.filter((v): v is string => typeof v === "string");
|
|
||||||
if (values.length === 0) return [];
|
|
||||||
return String(values[0]).split(" ").filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Label IDs from `load/notices` with `{ mode: "labels" }`. */
|
/** Label IDs from `load/notices` with `{ mode: "labels" }`. */
|
||||||
@@ -16,13 +15,12 @@ export async function fetchNoticeLabelIds(noticesUrl: string): Promise<string[]>
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(noticesUrl, {
|
const res = await fetch(noticesUrl, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
headers: JSON_HEADERS,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify({ mode: "labels" }),
|
body: JSON.stringify({ mode: "labels" }),
|
||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
if (!res.ok) return [];
|
||||||
const json = (await res.json()) as { payload?: Array<{ id?: number }> };
|
const payload = ((await res.json()) as { payload?: Array<{ id?: number }> })?.payload;
|
||||||
const payload = json?.payload;
|
|
||||||
if (!Array.isArray(payload)) return [];
|
if (!Array.isArray(payload)) return [];
|
||||||
return payload
|
return payload
|
||||||
.map((entry) => entry?.id)
|
.map((entry) => entry?.id)
|
||||||
@@ -39,26 +37,16 @@ export async function resolveNoticeFilterTokens(
|
|||||||
noticesUrl: string,
|
noticesUrl: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const fromPref = parseNoticesFilterPref(prefsPayload);
|
const fromPref = parseNoticesFilterPref(prefsPayload);
|
||||||
if (fromPref.length > 0) return fromPref;
|
return fromPref.length > 0 ? fromPref : await fetchNoticeLabelIds(noticesUrl);
|
||||||
return await fetchNoticeLabelIds(noticesUrl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeNoticeLabelId(label: unknown): string | null {
|
export function normalizeNoticeLabelId(label: unknown): string | null {
|
||||||
if (typeof label === "number" && !Number.isNaN(label)) {
|
if (typeof label === "number" && !Number.isNaN(label)) return String(label);
|
||||||
return String(label);
|
if (typeof label === "string" && label.trim()) return label.trim();
|
||||||
}
|
const id =
|
||||||
if (typeof label === "string" && label.trim()) {
|
label && typeof label === "object" ? (label as { id?: unknown }).id : undefined;
|
||||||
return label.trim();
|
if (typeof id === "number" && !Number.isNaN(id)) return String(id);
|
||||||
}
|
if (typeof id === "string" && id.trim()) return id.trim();
|
||||||
if (label && typeof label === "object") {
|
|
||||||
const obj = label as Record<string, unknown>;
|
|
||||||
if (typeof obj.id === "number" && !Number.isNaN(obj.id)) {
|
|
||||||
return String(obj.id);
|
|
||||||
}
|
|
||||||
if (typeof obj.id === "string" && obj.id.trim()) {
|
|
||||||
return obj.id.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +56,6 @@ export function noticeMatchesLabelFilter(
|
|||||||
): boolean {
|
): boolean {
|
||||||
if (filterTokens.length === 0) return true;
|
if (filterTokens.length === 0) return true;
|
||||||
const id = normalizeNoticeLabelId(notice?.label);
|
const id = normalizeNoticeLabelId(notice?.label);
|
||||||
if (id !== null && filterTokens.includes(id)) return true;
|
return (id !== null && filterTokens.includes(id)) ||
|
||||||
return filterTokens.includes(JSON.stringify(notice?.label));
|
filterTokens.includes(JSON.stringify(notice?.label));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,44 +8,36 @@ import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
|
|||||||
import { verboseInfo } from "@/utils/verboseLog";
|
import { verboseInfo } from "@/utils/verboseLog";
|
||||||
|
|
||||||
const PAGE_PATCH_LOADER_ID = "bsplus-seqta-menu-colour-patch-loader";
|
const PAGE_PATCH_LOADER_ID = "bsplus-seqta-menu-colour-patch-loader";
|
||||||
|
let colorisRecoveryAttached = false;
|
||||||
|
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
export function dismissStaleModaliserContainers(): number {
|
function dismissStaleColourDialogs(forceColourChooser = false): {
|
||||||
let removed = 0;
|
slideRemoved: number;
|
||||||
for (const container of document.querySelectorAll(".modaliser-container")) {
|
modalRemoved: number;
|
||||||
const modal = container.querySelector(".modaliser");
|
} {
|
||||||
if (!modal?.childElementCount || !container.classList.contains("visible")) {
|
let slideRemoved = 0;
|
||||||
container.remove();
|
|
||||||
removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dismissStaleColourSlidePanes(
|
|
||||||
forceColourChooser = false,
|
|
||||||
): number {
|
|
||||||
let removed = 0;
|
|
||||||
for (const pane of document.querySelectorAll(".uiSlidePane")) {
|
for (const pane of document.querySelectorAll(".uiSlidePane")) {
|
||||||
if (pane.querySelector(".pane.colourChooser")) {
|
if (pane.querySelector(".pane.colourChooser")) {
|
||||||
pane.remove();
|
pane.remove();
|
||||||
removed++;
|
slideRemoved++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!forceColourChooser && pane.classList.contains("shown")) continue;
|
if (!forceColourChooser && pane.classList.contains("shown")) continue;
|
||||||
if (!pane.classList.contains("shown")) {
|
if (!pane.classList.contains("shown")) {
|
||||||
pane.remove();
|
pane.remove();
|
||||||
removed++;
|
slideRemoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let modalRemoved = 0;
|
||||||
|
for (const container of document.querySelectorAll(".modaliser-container")) {
|
||||||
|
const modal = container.querySelector(".modaliser");
|
||||||
|
if (!modal?.childElementCount || !container.classList.contains("visible")) {
|
||||||
|
container.remove();
|
||||||
|
modalRemoved++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return removed;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dismissStaleColourDialogs(forceColourChooser = false): {
|
|
||||||
slideRemoved: number;
|
|
||||||
modalRemoved: number;
|
|
||||||
} {
|
|
||||||
const slideRemoved = dismissStaleColourSlidePanes(forceColourChooser);
|
|
||||||
const modalRemoved = dismissStaleModaliserContainers();
|
|
||||||
document.body.classList.remove("clr-open");
|
document.body.classList.remove("clr-open");
|
||||||
document.documentElement.classList.remove("clr-open");
|
document.documentElement.classList.remove("clr-open");
|
||||||
return { slideRemoved, modalRemoved };
|
return { slideRemoved, modalRemoved };
|
||||||
@@ -69,8 +61,7 @@ function setClrPickerState(reset: boolean): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hide colour-picker / modal layers that intercept clicks after a colour save. */
|
function dismissTimetableUiBlockers(): {
|
||||||
export function dismissTimetableUiBlockers(): {
|
|
||||||
slideRemoved: number;
|
slideRemoved: number;
|
||||||
modalRemoved: number;
|
modalRemoved: number;
|
||||||
} {
|
} {
|
||||||
@@ -79,14 +70,10 @@ export function dismissTimetableUiBlockers(): {
|
|||||||
return dismissStaleColourDialogs();
|
return dismissStaleColourDialogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear inline styles that can prevent Coloris from reopening. */
|
function prepareColorisPickerOpen(): void {
|
||||||
export function prepareColorisPickerOpen(): void {
|
|
||||||
setClrPickerState(true);
|
setClrPickerState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let colorisRecoveryAttached = false;
|
|
||||||
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
|
|
||||||
|
|
||||||
export function attachTimetableColorisRecovery(): void {
|
export function attachTimetableColorisRecovery(): void {
|
||||||
if (colorisRecoveryAttached) return;
|
if (colorisRecoveryAttached) return;
|
||||||
colorisRecoveryAttached = true;
|
colorisRecoveryAttached = true;
|
||||||
|
|||||||
@@ -7,62 +7,54 @@
|
|||||||
var MENU_UPDATE_COLOURS = "menu.update.colours";
|
var MENU_UPDATE_COLOURS = "menu.update.colours";
|
||||||
var SUBJECT_PREFIX = "timetable.subject.colour.";
|
var SUBJECT_PREFIX = "timetable.subject.colour.";
|
||||||
var TUTOR_PREFIX = "timetable.tutor.";
|
var TUTOR_PREFIX = "timetable.tutor.";
|
||||||
|
var CLEANUP_FOLLOWUP_MS = 300;
|
||||||
|
var cleanupFollowup = null;
|
||||||
|
|
||||||
function isTesStyling() {
|
function isTesStyling() {
|
||||||
var el = document.getElementById("logo-style");
|
var el = document.getElementById("logo-style");
|
||||||
return el && el.textContent.indexOf("tesSeqta") !== -1;
|
return el && el.textContent.indexOf("tesSeqta") !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dismissModalisers() {
|
function dismissStaleDialogs(forceColour) {
|
||||||
var n = 0;
|
var slide = 0;
|
||||||
var containers = document.querySelectorAll(".modaliser-container");
|
|
||||||
for (var i = 0; i < containers.length; i++) {
|
|
||||||
var c = containers[i];
|
|
||||||
var m = c.querySelector(".modaliser");
|
|
||||||
if (!m || !m.childElementCount || !c.classList.contains("visible")) {
|
|
||||||
c.remove();
|
|
||||||
n++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissSlidePanes(forceColour) {
|
|
||||||
var n = 0;
|
|
||||||
var panes = document.querySelectorAll(".uiSlidePane");
|
var panes = document.querySelectorAll(".uiSlidePane");
|
||||||
for (var i = 0; i < panes.length; i++) {
|
for (var i = 0; i < panes.length; i++) {
|
||||||
var p = panes[i];
|
var p = panes[i];
|
||||||
if (p.querySelector(".pane.colourChooser")) {
|
if (p.querySelector(".pane.colourChooser")) {
|
||||||
p.remove();
|
p.remove();
|
||||||
n++;
|
slide++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!forceColour && p.classList.contains("shown")) continue;
|
if (!forceColour && p.classList.contains("shown")) continue;
|
||||||
if (!p.classList.contains("shown")) {
|
if (!p.classList.contains("shown")) {
|
||||||
p.remove();
|
p.remove();
|
||||||
n++;
|
slide++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var modal = 0;
|
||||||
|
var containers = document.querySelectorAll(".modaliser-container");
|
||||||
|
for (var j = 0; j < containers.length; j++) {
|
||||||
|
var c = containers[j];
|
||||||
|
var m = c.querySelector(".modaliser");
|
||||||
|
if (!m || !m.childElementCount || !c.classList.contains("visible")) {
|
||||||
|
c.remove();
|
||||||
|
modal++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dismissStaleDialogs(forceColour) {
|
|
||||||
var slide = dismissSlidePanes(forceColour);
|
|
||||||
var modal = dismissModalisers();
|
|
||||||
document.body.classList.remove("clr-open");
|
document.body.classList.remove("clr-open");
|
||||||
document.documentElement.classList.remove("clr-open");
|
document.documentElement.classList.remove("clr-open");
|
||||||
return { slideRemoved: slide, modalRemoved: modal };
|
return { slideRemoved: slide, modalRemoved: modal };
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleCleanup() {
|
function scheduleCleanup() {
|
||||||
var delays = [0, 100, 300, 600];
|
dismissStaleDialogs(true);
|
||||||
for (var i = 0; i < delays.length; i++) {
|
if (cleanupFollowup) clearTimeout(cleanupFollowup);
|
||||||
(function (d) {
|
cleanupFollowup = setTimeout(function () {
|
||||||
setTimeout(function () {
|
cleanupFollowup = null;
|
||||||
dismissStaleDialogs(true);
|
dismissStaleDialogs(true);
|
||||||
}, d);
|
}, CLEANUP_FOLLOWUP_MS);
|
||||||
})(delays[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMenuColours() {
|
function applyMenuColours() {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
var THEME_STYLE_ID = "custom-theme";
|
var THEME_STYLE_ID = "custom-theme";
|
||||||
var PREVIEW_STYLE_ID = "custom-theme-preview";
|
var PREVIEW_STYLE_ID = "custom-theme-preview";
|
||||||
var urlCache = {};
|
var urlCache = {};
|
||||||
var cssState = { custom: "", preview: "" };
|
|
||||||
var headObserver = null;
|
var headObserver = null;
|
||||||
|
|
||||||
function log(event, detail) {
|
function log(event, detail) {
|
||||||
@@ -77,8 +76,6 @@
|
|||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
releaseCachedUrls();
|
releaseCachedUrls();
|
||||||
cssState.custom = "";
|
|
||||||
cssState.preview = "";
|
|
||||||
setStyleText(IMAGES_STYLE_ID, "");
|
setStyleText(IMAGES_STYLE_ID, "");
|
||||||
document.getElementById(THEME_STYLE_ID)?.remove();
|
document.getElementById(THEME_STYLE_ID)?.remove();
|
||||||
document.getElementById(PREVIEW_STYLE_ID)?.remove();
|
document.getElementById(PREVIEW_STYLE_ID)?.remove();
|
||||||
@@ -128,18 +125,15 @@
|
|||||||
if (payload.images !== undefined) applyThemeImages(payload.images);
|
if (payload.images !== undefined) applyThemeImages(payload.images);
|
||||||
|
|
||||||
if (payload.customCss !== undefined) {
|
if (payload.customCss !== undefined) {
|
||||||
cssState.custom = payload.customCss || "";
|
setStyleText(THEME_STYLE_ID, payload.customCss || "", true);
|
||||||
setStyleText(THEME_STYLE_ID, cssState.custom, true);
|
|
||||||
log("custom css applied");
|
log("custom css applied");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.previewCss !== undefined) {
|
if (payload.previewCss !== undefined) {
|
||||||
cssState.preview = payload.previewCss || "";
|
setStyleText(PREVIEW_STYLE_ID, payload.previewCss || "");
|
||||||
setStyleText(PREVIEW_STYLE_ID, cssState.preview);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.clearPreview) {
|
if (payload.clearPreview) {
|
||||||
cssState.preview = "";
|
|
||||||
setStyleText(PREVIEW_STYLE_ID, "");
|
setStyleText(PREVIEW_STYLE_ID, "");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user