From 3f709f3a4ecefba947f74066a40564068a1cbba4 Mon Sep 17 00:00:00 2001 From: rajchales-monito Date: Fri, 24 Jul 2026 14:52:20 +0200 Subject: [PATCH] Add admin sound preferences --- public/admin.css | 35 +++++++++++++- public/admin.html | 99 +++++++++++++++++++++++++--------------- public/admin.js | 114 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 183 insertions(+), 65 deletions(-) diff --git a/public/admin.css b/public/admin.css index eb8acab..8f8cc33 100644 --- a/public/admin.css +++ b/public/admin.css @@ -124,8 +124,7 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } } .logout-compact { height: 32px; - padding: 0 8px; - font-size: 11px; + padding: 0; } .online-dot-toggle { width: 28px; @@ -919,6 +918,38 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; } .settings h2 { margin: 0 0 12px; } .settings h3 { margin: 20px 0 8px; } .settings form { display: grid; gap: 12px; } +.settings-tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; + margin: 0 0 14px; + padding: 4px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f7faf8; +} +.settings-tabs button { + padding: 9px 10px; + background: transparent; + color: var(--muted); +} +.settings-tabs button.active { + background: var(--brand); + color: white; +} +.settings-tab-panel { display: none; } +.settings-tab-panel.active { display: block; } +.admin-preferences { + display: grid; + gap: 14px; +} +.admin-preferences label:not(.setting-switch) { + gap: 8px; +} +.admin-preferences input[type="range"] { + padding: 0; + accent-color: var(--brand); +} pre { white-space: pre-wrap; overflow: auto; diff --git a/public/admin.html b/public/admin.html index 7e23d3c..80b3a62 100644 --- a/public/admin.html +++ b/public/admin.html @@ -44,7 +44,11 @@ - +
@@ -101,44 +105,65 @@ diff --git a/public/admin.js b/public/admin.js index 1b1cd13..11ed0de 100644 --- a/public/admin.js +++ b/public/admin.js @@ -11,6 +11,7 @@ const state = { hideBots: localStorage.getItem("mf_admin_hide_bots") !== "0", soundMuted: localStorage.getItem("mf_admin_sound_muted") === "1", soundVolume: Number(localStorage.getItem("mf_admin_sound_volume") || 70), + soundStyle: localStorage.getItem("mf_admin_sound_style") || "bright", replyFiles: [], attentionTimer: null, attentionOn: false, @@ -48,22 +49,35 @@ $("#logoutButton").addEventListener("click", async () => { location.reload(); }); -$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden")); +$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.toggle("hidden")); $("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden")); $("#adminSoundToggle").addEventListener("click", () => { - state.soundMuted = !state.soundMuted; - if (!state.soundMuted && state.soundVolume <= 0) state.soundVolume = 70; - localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); - localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); - renderAdminSoundControls(); + setAdminSoundMuted(!state.soundMuted); }); $("#adminSoundVolume").addEventListener("input", (event) => { - state.soundVolume = clamp(Number(event.target.value), 0, 100); - state.soundMuted = state.soundVolume <= 0; - localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); - localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); + setAdminSoundVolume(Number(event.target.value)); +}); +$("#adminSoundToggleSettings").addEventListener("change", (event) => { + setAdminSoundMuted(!event.target.checked); +}); +$("#adminSoundVolumeSettings").addEventListener("input", (event) => { + setAdminSoundVolume(Number(event.target.value)); +}); +$("#adminSoundStyle").addEventListener("change", (event) => { + state.soundStyle = event.target.value; + localStorage.setItem("mf_admin_sound_style", state.soundStyle); renderAdminSoundControls(); }); +$("#adminSoundTest").addEventListener("click", () => { + playNotifySound({ ignoreMuted: true }); +}); +document.querySelectorAll("[data-settings-tab]").forEach((button) => { + button.addEventListener("click", () => { + const tab = button.dataset.settingsTab; + document.querySelectorAll("[data-settings-tab]").forEach((item) => item.classList.toggle("active", item === button)); + document.querySelectorAll("[data-settings-panel]").forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === tab)); + }); +}); $("#settingsForm").brand.addEventListener("input", (event) => { $("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value; }); @@ -795,33 +809,81 @@ function notify() { function renderAdminSoundControls() { const toggle = $("#adminSoundToggle"); const volume = $("#adminSoundVolume"); - if (!toggle || !volume) return; + const settingsToggle = $("#adminSoundToggleSettings"); + const settingsVolume = $("#adminSoundVolumeSettings"); + const settingsStyle = $("#adminSoundStyle"); const safeVolume = clamp(state.soundVolume, 0, 100); - toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0); - toggle.setAttribute("aria-label", state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"); - toggle.title = state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"; - volume.value = String(safeVolume); - volume.title = `Hlasitost ${safeVolume}%`; + if (toggle) { + toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0); + toggle.setAttribute("aria-label", state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"); + toggle.title = state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci"; + } + if (volume) { + volume.value = String(safeVolume); + volume.title = `Hlasitost ${safeVolume}%`; + } + if (settingsToggle) settingsToggle.checked = !state.soundMuted; + if (settingsVolume) settingsVolume.value = String(safeVolume); + if (settingsStyle) settingsStyle.value = state.soundStyle; } -function playNotifySound() { +function setAdminSoundMuted(isMuted) { + state.soundMuted = Boolean(isMuted); + if (!state.soundMuted && state.soundVolume <= 0) state.soundVolume = 70; + localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); + localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); + renderAdminSoundControls(); +} + +function setAdminSoundVolume(value) { + state.soundVolume = clamp(value, 0, 100); + state.soundMuted = state.soundVolume <= 0; + localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume)); + localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0"); + renderAdminSoundControls(); +} + +function playNotifySound(options = {}) { const volume = clamp(state.soundVolume, 0, 100); - if (state.soundMuted || volume <= 0) return; + if (!options.ignoreMuted && (state.soundMuted || volume <= 0)) return; const AudioCtor = window.AudioContext || window.webkitAudioContext; if (!AudioCtor) return; const audio = new AudioCtor(); const gain = audio.createGain(); - gain.gain.value = 0.12 * (volume / 100); + gain.gain.value = 0.12 * ((volume || 70) / 100); gain.connect(audio.destination); - [0, 0.14].forEach((offset, index) => { + for (const tone of soundPattern(state.soundStyle)) { const osc = audio.createOscillator(); - osc.type = "sine"; - osc.frequency.value = index ? 1040 : 820; + osc.type = tone.type; + osc.frequency.value = tone.frequency; osc.connect(gain); - osc.start(audio.currentTime + offset); - osc.stop(audio.currentTime + offset + 0.11); - }); - setTimeout(() => audio.close().catch(() => {}), 520); + osc.start(audio.currentTime + tone.offset); + osc.stop(audio.currentTime + tone.offset + tone.duration); + } + setTimeout(() => audio.close().catch(() => {}), 720); +} + +function soundPattern(style) { + const patterns = { + soft: [ + { offset: 0, duration: 0.13, frequency: 520, type: "sine" }, + { offset: 0.16, duration: 0.12, frequency: 680, type: "sine" } + ], + chime: [ + { offset: 0, duration: 0.16, frequency: 880, type: "triangle" }, + { offset: 0.18, duration: 0.18, frequency: 1320, type: "triangle" } + ], + urgent: [ + { offset: 0, duration: 0.1, frequency: 760, type: "square" }, + { offset: 0.13, duration: 0.1, frequency: 760, type: "square" }, + { offset: 0.26, duration: 0.12, frequency: 980, type: "square" } + ], + bright: [ + { offset: 0, duration: 0.11, frequency: 820, type: "sine" }, + { offset: 0.14, duration: 0.11, frequency: 1040, type: "sine" } + ] + }; + return patterns[style] || patterns.bright; } function clamp(value, min, max) {