fix(plugins): music/BG after reload

This commit is contained in:
2026-06-22 22:26:20 +09:30
parent a106033b87
commit fadc06aa27
3 changed files with 173 additions and 93 deletions
@@ -0,0 +1,70 @@
import type { PluginAPI } from "@/plugins/core/types";
import { waitForElm } from "@/seqta/utils/waitForElm";
export const ANIMATED_BG_MARKER = "bsplus-animated-bg";
const LAYER_CLASSES = [
["bg", ANIMATED_BG_MARKER],
["bg", "bg2", ANIMATED_BG_MARKER],
["bg", "bg3", ANIMATED_BG_MARKER],
] as const;
export function updateAnimationSpeed(speed: number) {
const bgElements = document.querySelectorAll(`.bg.${ANIMATED_BG_MARKER}`);
Array.from(bgElements).forEach((element, index) => {
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
});
}
function countAnimatedLayers(container: HTMLElement): number {
return container.querySelectorAll(`:scope > div.bg.${ANIMATED_BG_MARKER}`).length;
}
export function ensureAnimatedBackgroundLayers(
container: HTMLElement,
menu: HTMLElement,
speed: number,
): void {
const count = countAnimatedLayers(container);
if (count >= 3) {
updateAnimationSpeed(speed);
return;
}
container
.querySelectorAll(`:scope > div.bg.${ANIMATED_BG_MARKER}`)
.forEach((el) => el.remove());
for (const classes of LAYER_CLASSES) {
const bk = document.createElement("div");
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
}
updateAnimationSpeed(speed);
}
export function removeAnimatedBackgroundLayers(): void {
document
.querySelectorAll(`div.bg.${ANIMATED_BG_MARKER}`)
.forEach((el) => el.remove());
}
export async function syncAnimatedBackground(
api: PluginAPI<{ speed: number }>,
): Promise<void> {
try {
const [container, menu] = await Promise.all([
waitForElm("#container", true),
waitForElm("#menu", true),
]);
ensureAnimatedBackgroundLayers(
container as HTMLElement,
menu as HTMLElement,
api.settings.speed,
);
} catch {
// #container / #menu not ready yet
}
}
@@ -6,7 +6,11 @@ import {
Setting, Setting,
} from "@/plugins/core/settingsHelpers"; } from "@/plugins/core/settingsHelpers";
import styles from "./styles.css?inline"; import styles from "./styles.css?inline";
import { waitForElm } from "@/seqta/utils/waitForElm"; import {
removeAnimatedBackgroundLayers,
syncAnimatedBackground,
updateAnimationSpeed,
} from "./backgroundLayers";
const settings = defineSettings({ const settings = defineSettings({
speed: numberSetting({ speed: numberSetting({
@@ -36,48 +40,35 @@ const animatedBackgroundPlugin: Plugin<typeof settings> = {
settings: instance.settings, settings: instance.settings,
run: async (api) => { run: async (api) => {
const [container, menu] = await Promise.all([ await syncAnimatedBackground(api);
waitForElm("#container", true),
waitForElm("#menu", true),
]);
const backgrounds = [ const speedUnregister = api.settings.onChange("speed", updateAnimationSpeed);
{ classes: ["bg"] },
{ classes: ["bg", "bg2"] },
{ classes: ["bg", "bg3"] },
];
backgrounds.forEach(({ classes }) => { const pageChangeUnregister = api.seqta.onPageChange(() => {
const bk = document.createElement("div"); void syncAnimatedBackground(api);
classes.forEach((cls) => bk.classList.add(cls));
container.insertBefore(bk, menu);
}); });
// Set initial speed const pageshowHandler = (event: PageTransitionEvent) => {
updateAnimationSpeed(api.settings.speed); if (event.persisted) void syncAnimatedBackground(api);
};
window.addEventListener("pageshow", pageshowHandler);
// Listen for speed changes const containerObserver = new MutationObserver(() => {
const speedUnregister = api.settings.onChange( void syncAnimatedBackground(api);
"speed", });
updateAnimationSpeed, const container = document.getElementById("container");
); if (container) {
containerObserver.observe(container, { childList: true });
}
// Return cleanup function
return () => { return () => {
speedUnregister.unregister(); speedUnregister.unregister();
// Remove background elements pageChangeUnregister.unregister();
const backgrounds = document.getElementsByClassName("bg"); window.removeEventListener("pageshow", pageshowHandler);
Array.from(backgrounds).forEach((element) => element.remove()); containerObserver.disconnect();
removeAnimatedBackgroundLayers();
}; };
}, },
}; };
function updateAnimationSpeed(speed: number) {
const bgElements = document.getElementsByClassName("bg");
Array.from(bgElements).forEach((element, index) => {
const baseSpeed = index === 0 ? 3 : index === 1 ? 4 : 5;
(element as HTMLElement).style.animationDuration = `${baseSpeed / speed}s`;
});
}
export default animatedBackgroundPlugin; export default animatedBackgroundPlugin;
+70 -51
View File
@@ -32,7 +32,6 @@ const store = localforage.createInstance({
let currentAudio: HTMLAudioElement | null = null; let currentAudio: HTMLAudioElement | null = null;
let currentObjectUrl: string | null = null; let currentObjectUrl: string | null = null;
let cleanupRegistered = false;
let pendingGestureCancel: (() => void) | null = null; let pendingGestureCancel: (() => void) | null = null;
let visibilityResumeTimeout: number | null = null; let visibilityResumeTimeout: number | null = null;
@@ -54,30 +53,36 @@ function stopAndCleanupAudio(): void {
} }
} }
function ensureGestureStart(handler: () => void): () => void { function disarmGesturePlayback(): void {
const eventTypes = ["pointerdown", "keydown", "touchstart"]; // broad user gesture coverage if (pendingGestureCancel) {
const listener = () => { pendingGestureCancel();
handler(); pendingGestureCancel = null;
for (const type of eventTypes) {
window.removeEventListener(type, listener);
} }
}
function armGesturePlayback(handler: () => void): void {
disarmGesturePlayback();
const eventTypes = ["pointerdown", "keydown", "touchstart"] as const;
const listener = () => {
disarmGesturePlayback();
handler();
}; };
for (const type of eventTypes) { for (const type of eventTypes) {
window.addEventListener(type, listener, { once: true, passive: true }); window.addEventListener(type, listener, { once: true, passive: true });
} }
return () => { pendingGestureCancel = () => {
for (const type of eventTypes) { for (const type of eventTypes) {
window.removeEventListener(type, listener); window.removeEventListener(type, listener);
} }
}; };
} }
async function startPlayback(volume: number): Promise<void> { async function startPlayback(volume: number): Promise<boolean> {
const blob = await loadAudioBlob(); const blob = await loadAudioBlob();
if (!blob) return; if (!blob) return false;
if (!currentAudio) {
stopAndCleanupAudio(); stopAndCleanupAudio();
currentObjectUrl = URL.createObjectURL(blob); currentObjectUrl = URL.createObjectURL(blob);
const audio = new Audio(currentObjectUrl); const audio = new Audio(currentObjectUrl);
audio.loop = true; audio.loop = true;
@@ -87,12 +92,15 @@ async function startPlayback(volume: number): Promise<void> {
audio.style.display = "none"; audio.style.display = "none";
document.body.appendChild(audio); document.body.appendChild(audio);
currentAudio = audio; currentAudio = audio;
} else {
currentAudio.volume = Math.max(0, Math.min(1, volume));
}
try { try {
// Attempt immediate play; may be blocked until gesture await currentAudio.play();
await audio.play(); return true;
} catch { } catch {
// Ignore; will be started after gesture if enabled return false;
} }
} }
@@ -109,73 +117,84 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
run: async (api) => { run: async (api) => {
await api.storage.loaded; await api.storage.loaded;
// react to specific setting changes const tryStart = async () => {
api.settings.onChange("volume" as any, (value: any) => { const blob = await loadAudioBlob();
const vol = (typeof value === "number" ? value : 0.5) as number; if (!blob) return;
const vol = (api.settings as { volume?: number }).volume ?? 0.5;
const played = await startPlayback(vol);
if (played) {
disarmGesturePlayback();
} else {
armGesturePlayback(() => void tryStart());
}
};
api.settings.onChange("volume" as never, (value: unknown) => {
const vol = typeof value === "number" ? value : 0.5;
if (currentAudio) currentAudio.volume = Math.max(0, Math.min(1, vol)); if (currentAudio) currentAudio.volume = Math.max(0, Math.min(1, vol));
}); });
api.settings.onChange("pauseOnHidden" as any, (value: any) => { api.settings.onChange("pauseOnHidden" as never, (value: unknown) => {
const pauseOnHidden = (typeof value === "boolean" ? value : true) as boolean; const pauseOnHidden = typeof value === "boolean" ? value : true;
// If the setting is disabled and audio is currently paused due to tab being hidden, resume it if (
if (!pauseOnHidden && currentAudio && currentAudio.paused && document.visibilityState === "hidden") { !pauseOnHidden &&
currentAudio.play().catch(() => {}); currentAudio?.paused &&
document.visibilityState === "visible"
) {
void tryStart();
} }
}); });
// Note: Stop button/event removed by user; no stop handling needed armGesturePlayback(() => void tryStart());
void tryStart();
// Start if we have audio and autoplay is enabled
const tryStart = async () => {
const vol = (api.settings as any).volume ?? 0.5;
await startPlayback(vol);
};
// Always arm gesture start and attempt immediate start
const cancel = ensureGestureStart(() => { tryStart(); });
cleanupRegistered = true;
(window as any).__betterseqta_bg_music_cancel__ = cancel;
tryStart();
// Pause on tab hide, resume on show with a small delay (if enabled)
const visHandler = () => { const visHandler = () => {
if (!currentAudio) return; if (!currentAudio) {
const pauseOnHidden = (api.settings as any).pauseOnHidden ?? true; if (document.visibilityState === "visible") void tryStart();
return;
}
const pauseOnHidden =
(api.settings as { pauseOnHidden?: boolean }).pauseOnHidden ?? true;
if (!pauseOnHidden) return; if (!pauseOnHidden) return;
if (document.visibilityState === "hidden") { if (document.visibilityState === "hidden") {
if (visibilityResumeTimeout !== null) { if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout); clearTimeout(visibilityResumeTimeout);
visibilityResumeTimeout = null; visibilityResumeTimeout = null;
} }
currentAudio.pause(); currentAudio.pause();
} else if (document.visibilityState === "visible") { } else {
if (visibilityResumeTimeout !== null) { if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout); clearTimeout(visibilityResumeTimeout);
} }
visibilityResumeTimeout = window.setTimeout(() => { visibilityResumeTimeout = window.setTimeout(() => {
visibilityResumeTimeout = null; visibilityResumeTimeout = null;
currentAudio?.play().catch(() => {}); void tryStart();
}, 200); }, 200);
} }
}; };
document.addEventListener("visibilitychange", visHandler); document.addEventListener("visibilitychange", visHandler);
// Allow uploads to trigger refresh const pageshowHandler = () => void tryStart();
const uploadedHandler = () => { window.addEventListener("pageshow", pageshowHandler);
const vol = (api.settings as any).volume ?? 0.5;
startPlayback(vol); const uploadedHandler = () => void tryStart();
};
window.addEventListener("betterseqta-background-music-updated", uploadedHandler); window.addEventListener("betterseqta-background-music-updated", uploadedHandler);
return () => { return () => {
document.removeEventListener("visibilitychange", visHandler); document.removeEventListener("visibilitychange", visHandler);
window.removeEventListener("betterseqta-background-music-updated", uploadedHandler); window.removeEventListener("pageshow", pageshowHandler);
if (cleanupRegistered && (window as any).__betterseqta_bg_music_cancel__) { window.removeEventListener(
(window as any).__betterseqta_bg_music_cancel__(); "betterseqta-background-music-updated",
(window as any).__betterseqta_bg_music_cancel__ = undefined; uploadedHandler,
);
disarmGesturePlayback();
if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout);
visibilityResumeTimeout = null;
} }
if (pendingGestureCancel) { pendingGestureCancel(); pendingGestureCancel = null; }
if (visibilityResumeTimeout !== null) { clearTimeout(visibilityResumeTimeout); visibilityResumeTimeout = null; }
stopAndCleanupAudio(); stopAndCleanupAudio();
}; };
}, },