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) {