mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
fix(plugins): music/BG after reload
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user