mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-08-28 17:21:05 +00:00
fix(sidebar): analytics not showing when disabled
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
if (visibilityResumeTimeout !== null) {
|
||||
clearTimeout(visibilityResumeTimeout);
|
||||
}
|
||||
visibilityResumeTimeout = window.setTimeout(() => {
|
||||
visibilityResumeTimeout = null;
|
||||
void tryStart();
|
||||
}, 200);
|
||||
isPlaying = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentAudio) {
|
||||
void ensurePlayback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pauseOnHidden) return;
|
||||
|
||||
if (visibilityResumeTimeout !== null) {
|
||||
clearTimeout(visibilityResumeTimeout);
|
||||
}
|
||||
visibilityResumeTimeout = window.setTimeout(() => {
|
||||
visibilityResumeTimeout = null;
|
||||
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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user