fix(sidebar): analytics not showing when disabled

This commit is contained in:
2026-06-22 23:10:49 +09:30
parent fadc06aa27
commit 5df9169705
5 changed files with 204 additions and 62 deletions
+7 -3
View File
@@ -131,7 +131,7 @@
right: 0;
z-index: 50;
margin: 0;
padding: 0.35rem;
padding: 0.5rem;
list-style: none;
border: 1px solid var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
border-radius: 14px;
@@ -139,8 +139,11 @@
box-shadow:
0 10px 25px -5px rgb(0 0 0 / 0.25),
0 8px 10px -6px rgb(0 0 0 / 0.2);
max-height: 16rem;
max-height: 18rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.select-option {
@@ -150,9 +153,10 @@
border-radius: 10px;
background: transparent;
color: var(--text-primary);
padding: 0.5rem 0.75rem;
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;
+158 -46
View File
@@ -1,5 +1,10 @@
import type { Plugin } from "@/plugins/core/types";
import { booleanSetting, componentSetting, defineSettings, numberSetting } from "@/plugins/core/settingsHelpers";
import {
booleanSetting,
componentSetting,
defineSettings,
numberSetting,
} from "@/plugins/core/settingsHelpers";
import styles from "./styles.css?inline";
import BackgroundMusicSetting from "./BackgroundMusicSetting.svelte";
import localforage from "localforage";
@@ -20,7 +25,8 @@ const settings = defineSettings({
}),
pauseOnHidden: booleanSetting({
title: "Pause when tab hidden",
description: "Pause music when switching to another tab or minimizing the browser",
description:
"Pause music when switching to another tab or minimizing the browser",
default: true,
}),
});
@@ -30,10 +36,14 @@ const store = localforage.createInstance({
storeName: "music",
});
const HINT_ID = "bsplus-bg-music-hint";
let currentAudio: HTMLAudioElement | null = null;
let currentObjectUrl: string | null = null;
let pendingGestureCancel: (() => void) | null = null;
let visibilityResumeTimeout: number | null = null;
let hintElement: HTMLElement | null = null;
let isPlaying = false;
async function loadAudioBlob(): Promise<Blob | null> {
const blob = await store.getItem<Blob>("audio-blob");
@@ -51,6 +61,29 @@ function stopAndCleanupAudio(): void {
URL.revokeObjectURL(currentObjectUrl);
currentObjectUrl = null;
}
isPlaying = false;
}
function hideAutoplayHint(): void {
if (hintElement) {
hintElement.remove();
hintElement = null;
}
}
function showAutoplayHint(onActivate: () => void): void {
hideAutoplayHint();
const hint = document.createElement("button");
hint.id = HINT_ID;
hint.type = "button";
hint.className = "bsplus-bg-music-hint";
hint.textContent = "Tap to start background music";
hint.addEventListener("pointerdown", (event) => {
event.preventDefault();
onActivate();
});
document.body.appendChild(hint);
hintElement = hint;
}
function disarmGesturePlayback(): void {
@@ -60,26 +93,14 @@ function disarmGesturePlayback(): void {
}
}
function armGesturePlayback(handler: () => void): void {
disarmGesturePlayback();
const eventTypes = ["pointerdown", "keydown", "touchstart"] as const;
const listener = () => {
disarmGesturePlayback();
handler();
};
for (const type of eventTypes) {
window.addEventListener(type, listener, { once: true, passive: true });
}
pendingGestureCancel = () => {
for (const type of eventTypes) {
window.removeEventListener(type, listener);
}
};
}
async function startPlayback(volume: number): Promise<boolean> {
/** Prepare <audio> so play() can run synchronously inside a user-gesture handler. */
async function prepareAudioElement(volume: number): Promise<boolean> {
const blob = await loadAudioBlob();
if (!blob) return false;
if (!blob) {
stopAndCleanupAudio();
hideAutoplayHint();
return false;
}
if (!currentAudio) {
stopAndCleanupAudio();
@@ -88,7 +109,6 @@ async function startPlayback(volume: number): Promise<boolean> {
audio.loop = true;
audio.volume = Math.max(0, Math.min(1, volume));
audio.preload = "auto";
audio.crossOrigin = "anonymous";
audio.style.display = "none";
document.body.appendChild(audio);
currentAudio = audio;
@@ -96,14 +116,76 @@ async function startPlayback(volume: number): Promise<boolean> {
currentAudio.volume = Math.max(0, Math.min(1, volume));
}
return true;
}
/**
* Must be called synchronously from a user-gesture handler (no await before this).
*/
function playPreparedAudio(volume: number): boolean {
if (!currentAudio) return false;
currentAudio.volume = Math.max(0, Math.min(1, volume));
try {
await currentAudio.play();
const result = currentAudio.play();
void result
.then(() => {
isPlaying = true;
hideAutoplayHint();
disarmGesturePlayback();
})
.catch(() => {
isPlaying = false;
});
return true;
} catch {
isPlaying = false;
return false;
}
}
async function tryAutoplay(volume: number): Promise<boolean> {
const ready = await prepareAudioElement(volume);
if (!ready || !currentAudio) return false;
try {
await currentAudio.play();
isPlaying = true;
hideAutoplayHint();
disarmGesturePlayback();
return true;
} catch {
isPlaying = false;
return false;
}
}
function armGesturePlayback(onGesture: () => void): void {
disarmGesturePlayback();
const listener = (event: Event) => {
if (event.type === "keydown") {
const key = (event as KeyboardEvent).key;
if (key !== "Enter" && key !== " ") return;
}
onGesture();
};
const options: AddEventListenerOptions = { capture: true, passive: true };
const types = ["pointerdown", "keydown", "touchstart"] as const;
for (const type of types) {
document.addEventListener(type, listener, options);
}
pendingGestureCancel = () => {
for (const type of types) {
document.removeEventListener(type, listener, options);
}
hideAutoplayHint();
};
showAutoplayHint(onGesture);
}
const backgroundMusicPlugin: Plugin<typeof settings> = {
id: "background-music",
name: "Background Music",
@@ -117,16 +199,28 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
run: async (api) => {
await api.storage.loaded;
const tryStart = async () => {
const blob = await loadAudioBlob();
if (!blob) return;
const getVolume = () =>
(api.settings as { volume?: number }).volume ?? 0.5;
const vol = (api.settings as { volume?: number }).volume ?? 0.5;
const played = await startPlayback(vol);
if (played) {
const gestureStart = () => {
if (!currentAudio) return;
playPreparedAudio(getVolume());
};
const ensurePlayback = async () => {
const vol = getVolume();
const ready = await prepareAudioElement(vol);
if (!ready) return;
if (isPlaying && currentAudio && !currentAudio.paused) {
hideAutoplayHint();
disarmGesturePlayback();
} else {
armGesturePlayback(() => void tryStart());
return;
}
const autoplayed = await tryAutoplay(vol);
if (!autoplayed) {
armGesturePlayback(gestureStart);
}
};
@@ -142,46 +236,59 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
currentAudio?.paused &&
document.visibilityState === "visible"
) {
void tryStart();
void ensurePlayback();
}
});
armGesturePlayback(() => void tryStart());
void tryStart();
await ensurePlayback();
const visHandler = () => {
if (!currentAudio) {
if (document.visibilityState === "visible") void tryStart();
return;
}
const pauseOnHidden =
(api.settings as { pauseOnHidden?: boolean }).pauseOnHidden ?? true;
if (!pauseOnHidden) return;
if (document.visibilityState === "hidden") {
if (!pauseOnHidden || !currentAudio) return;
if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout);
visibilityResumeTimeout = null;
}
currentAudio.pause();
} else {
isPlaying = false;
return;
}
if (!currentAudio) {
void ensurePlayback();
return;
}
if (!pauseOnHidden) return;
if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout);
}
visibilityResumeTimeout = window.setTimeout(() => {
visibilityResumeTimeout = null;
void tryStart();
void tryAutoplay(getVolume());
}, 200);
}
};
document.addEventListener("visibilitychange", visHandler);
const pageshowHandler = () => void tryStart();
const pageshowHandler = () => void ensurePlayback();
window.addEventListener("pageshow", pageshowHandler);
const uploadedHandler = () => void tryStart();
window.addEventListener("betterseqta-background-music-updated", uploadedHandler);
const uploadedHandler = () => void ensurePlayback();
window.addEventListener(
"betterseqta-background-music-updated",
uploadedHandler,
);
const stopHandler = () => {
disarmGesturePlayback();
stopAndCleanupAudio();
hideAutoplayHint();
};
window.addEventListener("betterseqta-background-music-stop", stopHandler);
return () => {
document.removeEventListener("visibilitychange", visHandler);
@@ -190,7 +297,12 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
"betterseqta-background-music-updated",
uploadedHandler,
);
window.removeEventListener(
"betterseqta-background-music-stop",
stopHandler,
);
disarmGesturePlayback();
hideAutoplayHint();
if (visibilityResumeTimeout !== null) {
clearTimeout(visibilityResumeTimeout);
visibilityResumeTimeout = null;
@@ -1,2 +1,32 @@
.background-music-hidden{display:none}
.bsplus-bg-music-hint {
position: fixed;
bottom: 1rem;
left: 1rem;
z-index: 2147483000;
padding: 0.5rem 0.875rem;
border: 1px solid color-mix(in srgb, var(--better-main, #22c55e) 55%, transparent);
border-radius: 999px;
background: color-mix(in srgb, var(--theme-primary, #1a1a1a) 92%, black 8%);
color: var(--text-primary, #fff);
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.25;
cursor: pointer;
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);
}
}
@@ -14,7 +14,6 @@ import {
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
applyMenuItemVisibility,
isMenuItemHidden,
} from "@/seqta/utils/menuItemVisibility";
import { loadAnalyticsPage } from "../loadAnalyticsPage";
import styles from "../styles.css?inline";
@@ -38,10 +37,6 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
return () => {};
}
if (isMenuItemHidden("analytics")) {
return () => {};
}
const menuList = (await waitForElm("#menu > ul, #menu ul", true, 100, 60)) as HTMLElement;
const analyticsItem = document.createElement("li");
@@ -68,7 +63,6 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
const menuObserver = new MutationObserver(() => {
if (MenuOptionsOpen) return;
if (isMenuItemHidden("analytics")) return;
if (!menuList.contains(analyticsItem)) {
placeAnalyticsItem();
ensureAnalyticsMenuOrder();
+3 -1
View File
@@ -25,7 +25,9 @@ function mergeMenuItemsFromDom(): Record<string, { toggle: boolean }> {
}
function storeMenuSettings(): void {
const menuItems: Record<string, { toggle: boolean }> = {};
const menuItems: Record<string, { toggle: boolean }> = {
...(settingsState.menuitems as Record<string, { toggle: boolean }>),
};
const inputs = document.querySelectorAll<HTMLInputElement>(".menuitem");
for (const input of inputs) {
if (input.id) {