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;
|
right: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0.35rem;
|
padding: 0.5rem;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
border: 1px solid var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
|
border: 1px solid var(--theme-offset-bg, var(--theme-secondary, #e5e7eb));
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -139,8 +139,11 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
0 10px 25px -5px rgb(0 0 0 / 0.25),
|
||||||
0 8px 10px -6px rgb(0 0 0 / 0.2);
|
0 8px 10px -6px rgb(0 0 0 / 0.2);
|
||||||
max-height: 16rem;
|
max-height: 18rem;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-option {
|
.select-option {
|
||||||
@@ -150,9 +153,10 @@
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.625rem 0.875rem;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
line-height: 1.35;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 150ms ease;
|
transition: background-color 150ms ease;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { Plugin } from "@/plugins/core/types";
|
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 styles from "./styles.css?inline";
|
||||||
import BackgroundMusicSetting from "./BackgroundMusicSetting.svelte";
|
import BackgroundMusicSetting from "./BackgroundMusicSetting.svelte";
|
||||||
import localforage from "localforage";
|
import localforage from "localforage";
|
||||||
@@ -20,7 +25,8 @@ const settings = defineSettings({
|
|||||||
}),
|
}),
|
||||||
pauseOnHidden: booleanSetting({
|
pauseOnHidden: booleanSetting({
|
||||||
title: "Pause when tab hidden",
|
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,
|
default: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -30,10 +36,14 @@ const store = localforage.createInstance({
|
|||||||
storeName: "music",
|
storeName: "music",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HINT_ID = "bsplus-bg-music-hint";
|
||||||
|
|
||||||
let currentAudio: HTMLAudioElement | null = null;
|
let currentAudio: HTMLAudioElement | null = null;
|
||||||
let currentObjectUrl: string | null = null;
|
let currentObjectUrl: string | null = null;
|
||||||
let pendingGestureCancel: (() => void) | null = null;
|
let pendingGestureCancel: (() => void) | null = null;
|
||||||
let visibilityResumeTimeout: number | null = null;
|
let visibilityResumeTimeout: number | null = null;
|
||||||
|
let hintElement: HTMLElement | null = null;
|
||||||
|
let isPlaying = false;
|
||||||
|
|
||||||
async function loadAudioBlob(): Promise<Blob | null> {
|
async function loadAudioBlob(): Promise<Blob | null> {
|
||||||
const blob = await store.getItem<Blob>("audio-blob");
|
const blob = await store.getItem<Blob>("audio-blob");
|
||||||
@@ -51,6 +61,29 @@ function stopAndCleanupAudio(): void {
|
|||||||
URL.revokeObjectURL(currentObjectUrl);
|
URL.revokeObjectURL(currentObjectUrl);
|
||||||
currentObjectUrl = null;
|
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 {
|
function disarmGesturePlayback(): void {
|
||||||
@@ -60,26 +93,14 @@ function disarmGesturePlayback(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function armGesturePlayback(handler: () => void): void {
|
/** Prepare <audio> so play() can run synchronously inside a user-gesture handler. */
|
||||||
disarmGesturePlayback();
|
async function prepareAudioElement(volume: number): Promise<boolean> {
|
||||||
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> {
|
|
||||||
const blob = await loadAudioBlob();
|
const blob = await loadAudioBlob();
|
||||||
if (!blob) return false;
|
if (!blob) {
|
||||||
|
stopAndCleanupAudio();
|
||||||
|
hideAutoplayHint();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!currentAudio) {
|
if (!currentAudio) {
|
||||||
stopAndCleanupAudio();
|
stopAndCleanupAudio();
|
||||||
@@ -88,7 +109,6 @@ async function startPlayback(volume: number): Promise<boolean> {
|
|||||||
audio.loop = true;
|
audio.loop = true;
|
||||||
audio.volume = Math.max(0, Math.min(1, volume));
|
audio.volume = Math.max(0, Math.min(1, volume));
|
||||||
audio.preload = "auto";
|
audio.preload = "auto";
|
||||||
audio.crossOrigin = "anonymous";
|
|
||||||
audio.style.display = "none";
|
audio.style.display = "none";
|
||||||
document.body.appendChild(audio);
|
document.body.appendChild(audio);
|
||||||
currentAudio = audio;
|
currentAudio = audio;
|
||||||
@@ -96,14 +116,76 @@ async function startPlayback(volume: number): Promise<boolean> {
|
|||||||
currentAudio.volume = Math.max(0, Math.min(1, volume));
|
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 {
|
try {
|
||||||
await currentAudio.play();
|
const result = currentAudio.play();
|
||||||
|
void result
|
||||||
|
.then(() => {
|
||||||
|
isPlaying = true;
|
||||||
|
hideAutoplayHint();
|
||||||
|
disarmGesturePlayback();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
isPlaying = false;
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
|
isPlaying = false;
|
||||||
return 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> = {
|
const backgroundMusicPlugin: Plugin<typeof settings> = {
|
||||||
id: "background-music",
|
id: "background-music",
|
||||||
name: "Background Music",
|
name: "Background Music",
|
||||||
@@ -117,16 +199,28 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
run: async (api) => {
|
run: async (api) => {
|
||||||
await api.storage.loaded;
|
await api.storage.loaded;
|
||||||
|
|
||||||
const tryStart = async () => {
|
const getVolume = () =>
|
||||||
const blob = await loadAudioBlob();
|
(api.settings as { volume?: number }).volume ?? 0.5;
|
||||||
if (!blob) return;
|
|
||||||
|
|
||||||
const vol = (api.settings as { volume?: number }).volume ?? 0.5;
|
const gestureStart = () => {
|
||||||
const played = await startPlayback(vol);
|
if (!currentAudio) return;
|
||||||
if (played) {
|
playPreparedAudio(getVolume());
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensurePlayback = async () => {
|
||||||
|
const vol = getVolume();
|
||||||
|
const ready = await prepareAudioElement(vol);
|
||||||
|
if (!ready) return;
|
||||||
|
|
||||||
|
if (isPlaying && currentAudio && !currentAudio.paused) {
|
||||||
|
hideAutoplayHint();
|
||||||
disarmGesturePlayback();
|
disarmGesturePlayback();
|
||||||
} else {
|
return;
|
||||||
armGesturePlayback(() => void tryStart());
|
}
|
||||||
|
|
||||||
|
const autoplayed = await tryAutoplay(vol);
|
||||||
|
if (!autoplayed) {
|
||||||
|
armGesturePlayback(gestureStart);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,46 +236,59 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
currentAudio?.paused &&
|
currentAudio?.paused &&
|
||||||
document.visibilityState === "visible"
|
document.visibilityState === "visible"
|
||||||
) {
|
) {
|
||||||
void tryStart();
|
void ensurePlayback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
armGesturePlayback(() => void tryStart());
|
await ensurePlayback();
|
||||||
void tryStart();
|
|
||||||
|
|
||||||
const visHandler = () => {
|
const visHandler = () => {
|
||||||
if (!currentAudio) {
|
|
||||||
if (document.visibilityState === "visible") void tryStart();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pauseOnHidden =
|
const pauseOnHidden =
|
||||||
(api.settings as { pauseOnHidden?: boolean }).pauseOnHidden ?? true;
|
(api.settings as { pauseOnHidden?: boolean }).pauseOnHidden ?? true;
|
||||||
if (!pauseOnHidden) return;
|
|
||||||
|
|
||||||
if (document.visibilityState === "hidden") {
|
if (document.visibilityState === "hidden") {
|
||||||
|
if (!pauseOnHidden || !currentAudio) return;
|
||||||
if (visibilityResumeTimeout !== null) {
|
if (visibilityResumeTimeout !== null) {
|
||||||
clearTimeout(visibilityResumeTimeout);
|
clearTimeout(visibilityResumeTimeout);
|
||||||
visibilityResumeTimeout = null;
|
visibilityResumeTimeout = null;
|
||||||
}
|
}
|
||||||
currentAudio.pause();
|
currentAudio.pause();
|
||||||
} else {
|
isPlaying = false;
|
||||||
if (visibilityResumeTimeout !== null) {
|
return;
|
||||||
clearTimeout(visibilityResumeTimeout);
|
|
||||||
}
|
|
||||||
visibilityResumeTimeout = window.setTimeout(() => {
|
|
||||||
visibilityResumeTimeout = null;
|
|
||||||
void tryStart();
|
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
document.addEventListener("visibilitychange", visHandler);
|
||||||
|
|
||||||
const pageshowHandler = () => void tryStart();
|
const pageshowHandler = () => void ensurePlayback();
|
||||||
window.addEventListener("pageshow", pageshowHandler);
|
window.addEventListener("pageshow", pageshowHandler);
|
||||||
|
|
||||||
const uploadedHandler = () => void tryStart();
|
const uploadedHandler = () => void ensurePlayback();
|
||||||
window.addEventListener("betterseqta-background-music-updated", uploadedHandler);
|
window.addEventListener(
|
||||||
|
"betterseqta-background-music-updated",
|
||||||
|
uploadedHandler,
|
||||||
|
);
|
||||||
|
|
||||||
|
const stopHandler = () => {
|
||||||
|
disarmGesturePlayback();
|
||||||
|
stopAndCleanupAudio();
|
||||||
|
hideAutoplayHint();
|
||||||
|
};
|
||||||
|
window.addEventListener("betterseqta-background-music-stop", stopHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("visibilitychange", visHandler);
|
document.removeEventListener("visibilitychange", visHandler);
|
||||||
@@ -190,7 +297,12 @@ const backgroundMusicPlugin: Plugin<typeof settings> = {
|
|||||||
"betterseqta-background-music-updated",
|
"betterseqta-background-music-updated",
|
||||||
uploadedHandler,
|
uploadedHandler,
|
||||||
);
|
);
|
||||||
|
window.removeEventListener(
|
||||||
|
"betterseqta-background-music-stop",
|
||||||
|
stopHandler,
|
||||||
|
);
|
||||||
disarmGesturePlayback();
|
disarmGesturePlayback();
|
||||||
|
hideAutoplayHint();
|
||||||
if (visibilityResumeTimeout !== null) {
|
if (visibilityResumeTimeout !== null) {
|
||||||
clearTimeout(visibilityResumeTimeout);
|
clearTimeout(visibilityResumeTimeout);
|
||||||
visibilityResumeTimeout = null;
|
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 { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||||
import {
|
import {
|
||||||
applyMenuItemVisibility,
|
applyMenuItemVisibility,
|
||||||
isMenuItemHidden,
|
|
||||||
} from "@/seqta/utils/menuItemVisibility";
|
} from "@/seqta/utils/menuItemVisibility";
|
||||||
import { loadAnalyticsPage } from "../loadAnalyticsPage";
|
import { loadAnalyticsPage } from "../loadAnalyticsPage";
|
||||||
import styles from "../styles.css?inline";
|
import styles from "../styles.css?inline";
|
||||||
@@ -38,10 +37,6 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
|
|||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMenuItemHidden("analytics")) {
|
|
||||||
return () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuList = (await waitForElm("#menu > ul, #menu ul", true, 100, 60)) as HTMLElement;
|
const menuList = (await waitForElm("#menu > ul, #menu ul", true, 100, 60)) as HTMLElement;
|
||||||
|
|
||||||
const analyticsItem = document.createElement("li");
|
const analyticsItem = document.createElement("li");
|
||||||
@@ -68,7 +63,6 @@ const gradeAnalyticsPlugin: Plugin<{}> = {
|
|||||||
|
|
||||||
const menuObserver = new MutationObserver(() => {
|
const menuObserver = new MutationObserver(() => {
|
||||||
if (MenuOptionsOpen) return;
|
if (MenuOptionsOpen) return;
|
||||||
if (isMenuItemHidden("analytics")) return;
|
|
||||||
if (!menuList.contains(analyticsItem)) {
|
if (!menuList.contains(analyticsItem)) {
|
||||||
placeAnalyticsItem();
|
placeAnalyticsItem();
|
||||||
ensureAnalyticsMenuOrder();
|
ensureAnalyticsMenuOrder();
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ function mergeMenuItemsFromDom(): Record<string, { toggle: boolean }> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function storeMenuSettings(): void {
|
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");
|
const inputs = document.querySelectorAll<HTMLInputElement>(".menuitem");
|
||||||
for (const input of inputs) {
|
for (const input of inputs) {
|
||||||
if (input.id) {
|
if (input.id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user