feat: final fixes and improvements

This commit is contained in:
2026-07-19 09:44:11 +09:30
parent cf6e797510
commit a48d71ddd8
2 changed files with 120 additions and 36 deletions
+118 -35
View File
@@ -44,9 +44,25 @@ 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 gesturePending = false;
let ensureInFlight: Promise<void> | null = null;
const clamp = (v: number) => Math.max(0, Math.min(1, v)); const clamp = (v: number) => Math.max(0, Math.min(1, v));
async function waitForBody(): Promise<HTMLElement> {
if (document.body) return document.body;
await new Promise<void>((resolve) => {
const observer = new MutationObserver(() => {
if (document.body) {
observer.disconnect();
resolve();
}
});
observer.observe(document.documentElement, { childList: true });
});
return document.body!;
}
function clearHint(): void { function clearHint(): void {
hintEl?.remove(); hintEl?.remove();
hintEl = null; hintEl = null;
@@ -55,6 +71,7 @@ function clearHint(): void {
function disarmGesture(): void { function disarmGesture(): void {
gestureCleanup?.(); gestureCleanup?.();
gestureCleanup = null; gestureCleanup = null;
gesturePending = false;
} }
function stopAudio(): void { function stopAudio(): void {
@@ -73,6 +90,9 @@ async function prepareAudio(vol: number): Promise<boolean> {
clearHint(); clearHint();
return false; return false;
} }
const body = await waitForBody();
if (!audio) { if (!audio) {
stopAudio(); stopAudio();
objectUrl = URL.createObjectURL(blob); objectUrl = URL.createObjectURL(blob);
@@ -80,7 +100,7 @@ async function prepareAudio(vol: number): Promise<boolean> {
audio.loop = true; audio.loop = true;
audio.preload = "auto"; audio.preload = "auto";
audio.style.display = "none"; audio.style.display = "none";
document.body.append(audio); body.append(audio);
} }
audio.volume = clamp(vol); audio.volume = clamp(vol);
return true; return true;
@@ -93,11 +113,47 @@ function attemptPlay(vol: number): Promise<boolean> {
.play() .play()
.then(() => { .then(() => {
disarmGesture(); disarmGesture();
clearHint();
return true; return true;
}) })
.catch(() => false); .catch(() => false);
} }
/** Must stay synchronous — any await before play() drops user activation. */
function playFromUserGesture(vol: number): void {
if (!audio) {
gesturePending = true;
return;
}
audio.volume = clamp(vol);
void audio.play().then(
() => {
disarmGesture();
clearHint();
},
() => {
// Keep listeners armed; show hint if somehow missing.
if (!hintEl) showHint(() => playFromUserGesture(vol));
},
);
}
function showHint(onActivate: () => void): void {
clearHint();
if (!document.body) return;
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;
}
function armGesture(onGesture: () => void): void { function armGesture(onGesture: () => void): void {
disarmGesture(); disarmGesture();
const listener = (event: Event) => { const listener = (event: Event) => {
@@ -108,27 +164,16 @@ function armGesture(onGesture: () => void): void {
onGesture(); onGesture();
}; };
for (const type of GESTURE_EVENTS) { for (const type of GESTURE_EVENTS) {
window.addEventListener(type, listener, gestureOpts);
document.addEventListener(type, listener, gestureOpts); document.addEventListener(type, listener, gestureOpts);
} }
gestureCleanup = () => { gestureCleanup = () => {
for (const type of GESTURE_EVENTS) { for (const type of GESTURE_EVENTS) {
window.removeEventListener(type, listener, gestureOpts);
document.removeEventListener(type, listener, gestureOpts); document.removeEventListener(type, listener, gestureOpts);
} }
clearHint();
}; };
showHint(onGesture);
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();
onGesture();
});
document.body.append(hint);
hintEl = hint;
} }
const backgroundMusicPlugin: Plugin<typeof settings> = { const backgroundMusicPlugin: Plugin<typeof settings> = {
@@ -146,19 +191,48 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
type BgSettings = { volume?: number; pauseOnHidden?: boolean }; type BgSettings = { volume?: number; pauseOnHidden?: boolean };
const vol = () => (api.settings as BgSettings).volume ?? 0.5; const vol = () => (api.settings as BgSettings).volume ?? 0.5;
const pauseOnHidden = () => (api.settings as BgSettings).pauseOnHidden ?? true; const pauseOnHidden = () =>
(api.settings as BgSettings).pauseOnHidden ?? true;
const gesturePlay = () => { const runEnsurePlayback = async () => {
void attemptPlay(vol());
};
const ensurePlayback = async () => {
if (!(await prepareAudio(vol()))) return; if (!(await prepareAudio(vol()))) return;
if (audio && !audio.paused) { if (audio && !audio.paused) {
disarmGesture(); disarmGesture();
clearHint();
return; return;
} }
if (!(await attemptPlay(vol()))) armGesture(gesturePlay);
// Arm unlock before autoplay so the next click/key can call play()
// synchronously (async gaps drop user activation).
if (!gestureCleanup) {
armGesture(() => playFromUserGesture(vol()));
}
if (gesturePending) {
gesturePending = false;
playFromUserGesture(vol());
if (audio && !audio.paused) return;
}
if (await attemptPlay(vol())) return;
// Retry after load — some browsers allow autoplay once the page settles
// or Media Engagement Index applies from prior visits.
for (const delayMs of [500, 1500, 3000]) {
await new Promise((resolve) => setTimeout(resolve, delayMs));
if (!audio || !audio.paused) return;
if (await attemptPlay(vol())) return;
}
};
const ensurePlayback = () => {
if (!ensureInFlight) {
ensureInFlight = runEnsurePlayback().finally(() => {
ensureInFlight = null;
});
}
return ensureInFlight;
}; };
api.settings.onChange("volume" as never, (value: unknown) => { api.settings.onChange("volume" as never, (value: unknown) => {
@@ -175,8 +249,6 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
} }
}); });
await ensurePlayback();
const onVisibility = () => { const onVisibility = () => {
if (document.visibilityState === "hidden") { if (document.visibilityState === "hidden") {
if (!pauseOnHidden() || !audio) return; if (!pauseOnHidden() || !audio) return;
@@ -193,31 +265,42 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
if (resumeTimer) clearTimeout(resumeTimer); if (resumeTimer) clearTimeout(resumeTimer);
resumeTimer = setTimeout(() => { resumeTimer = setTimeout(() => {
resumeTimer = null; resumeTimer = null;
void attemptPlay(vol()); void ensurePlayback();
}, 200); }, 200);
}; };
const onUpdated = () => void ensurePlayback(); const onUpdated = () => void ensurePlayback();
const onStop = () => { const onStop = () => {
disarmGesture(); disarmGesture();
clearHint();
stopAudio(); stopAudio();
}; };
const teardown = () => {
document.removeEventListener("visibilitychange", onVisibility); const pageChange = api.seqta.onPageChange(() => {
window.removeEventListener("pageshow", onUpdated); void ensurePlayback();
window.removeEventListener("betterseqta-background-music-updated", onUpdated); });
window.removeEventListener("betterseqta-background-music-stop", onStop);
if (resumeTimer) clearTimeout(resumeTimer);
disarmGesture();
stopAudio();
};
document.addEventListener("visibilitychange", onVisibility); document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("pageshow", onUpdated); window.addEventListener("pageshow", onUpdated);
window.addEventListener("betterseqta-background-music-updated", onUpdated); window.addEventListener("betterseqta-background-music-updated", onUpdated);
window.addEventListener("betterseqta-background-music-stop", onStop); window.addEventListener("betterseqta-background-music-stop", onStop);
return teardown; void ensurePlayback();
return () => {
pageChange.unregister();
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("pageshow", onUpdated);
window.removeEventListener(
"betterseqta-background-music-updated",
onUpdated,
);
window.removeEventListener("betterseqta-background-music-stop", onStop);
if (resumeTimer) clearTimeout(resumeTimer);
disarmGesture();
clearHint();
stopAudio();
};
}, },
}; };
+2 -1
View File
@@ -42,7 +42,8 @@ export function OpenWhatsNewPopup(onDismissed?: () => void) {
const text = stringToHTML(/* html */ ` const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;"> <div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
<h1>3.7.3 Bugfix Bundle</h1> <h1>3.7.3 Timetable sync to Calendar & Bugfix Bundle</h1>
<li>Added an option in the Timetable to sync to Google Calendar and Outlook Calendar</li>
<li>Fixed dropdown contrast and readability in settings and across SEQTA pages.</li> <li>Fixed dropdown contrast and readability in settings and across SEQTA pages.</li>
<li>Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.</li> <li>Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.</li>
<li>Fixed timetable subject colour picker not reopening after closing (#221).</li> <li>Fixed timetable subject colour picker not reopening after closing (#221).</li>