diff --git a/public/admin.html b/public/admin.html
index 80b3a62..e11b349 100644
--- a/public/admin.html
+++ b/public/admin.html
@@ -159,8 +159,11 @@
+
+
+
diff --git a/public/admin.js b/public/admin.js
index 11ed0de..ddbfcd9 100644
--- a/public/admin.js
+++ b/public/admin.js
@@ -12,8 +12,10 @@ const state = {
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",
+ soundRepeat: localStorage.getItem("mf_admin_sound_repeat") === "1",
replyFiles: [],
attentionTimer: null,
+ soundRepeatTimer: null,
attentionOn: false,
originalTitle: document.title,
originalFavicon: document.querySelector("link[rel='icon']")?.href || "/favicon.svg"
@@ -68,6 +70,12 @@ $("#adminSoundStyle").addEventListener("change", (event) => {
localStorage.setItem("mf_admin_sound_style", state.soundStyle);
renderAdminSoundControls();
});
+$("#adminSoundRepeat").addEventListener("change", (event) => {
+ state.soundRepeat = event.target.checked;
+ localStorage.setItem("mf_admin_sound_repeat", state.soundRepeat ? "1" : "0");
+ if (!state.soundRepeat) stopRepeatingSound();
+ renderAdminSoundControls();
+});
$("#adminSoundTest").addEventListener("click", () => {
playNotifySound({ ignoreMuted: true });
});
@@ -804,6 +812,7 @@ async function saveSite(patch) {
function notify() {
startAttention();
playNotifySound();
+ startRepeatingSound();
}
function renderAdminSoundControls() {
@@ -812,6 +821,7 @@ function renderAdminSoundControls() {
const settingsToggle = $("#adminSoundToggleSettings");
const settingsVolume = $("#adminSoundVolumeSettings");
const settingsStyle = $("#adminSoundStyle");
+ const settingsRepeat = $("#adminSoundRepeat");
const safeVolume = clamp(state.soundVolume, 0, 100);
if (toggle) {
toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0);
@@ -825,6 +835,7 @@ function renderAdminSoundControls() {
if (settingsToggle) settingsToggle.checked = !state.soundMuted;
if (settingsVolume) settingsVolume.value = String(safeVolume);
if (settingsStyle) settingsStyle.value = state.soundStyle;
+ if (settingsRepeat) settingsRepeat.checked = state.soundRepeat;
}
function setAdminSoundMuted(isMuted) {
@@ -832,6 +843,7 @@ function setAdminSoundMuted(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));
+ if (state.soundMuted) stopRepeatingSound();
renderAdminSoundControls();
}
@@ -840,6 +852,7 @@ function setAdminSoundVolume(value) {
state.soundMuted = state.soundVolume <= 0;
localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume));
localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0");
+ if (state.soundMuted) stopRepeatingSound();
renderAdminSoundControls();
}
@@ -863,8 +876,28 @@ function playNotifySound(options = {}) {
setTimeout(() => audio.close().catch(() => {}), 720);
}
+function startRepeatingSound() {
+ if (!state.soundRepeat || state.soundRepeatTimer || state.soundMuted || clamp(state.soundVolume, 0, 100) <= 0) return;
+ state.soundRepeatTimer = setInterval(() => {
+ playNotifySound();
+ }, 5500);
+}
+
+function stopRepeatingSound() {
+ if (state.soundRepeatTimer) clearInterval(state.soundRepeatTimer);
+ state.soundRepeatTimer = null;
+}
+
function soundPattern(style) {
const patterns = {
+ tap: [
+ { offset: 0, duration: 0.08, frequency: 720, type: "sine" }
+ ],
+ double: [
+ { offset: 0, duration: 0.09, frequency: 760, type: "sine" },
+ { offset: 0.12, duration: 0.09, frequency: 760, type: "sine" },
+ { offset: 0.28, duration: 0.1, frequency: 980, type: "triangle" }
+ ],
soft: [
{ offset: 0, duration: 0.13, frequency: 520, type: "sine" },
{ offset: 0.16, duration: 0.12, frequency: 680, type: "sine" }
@@ -905,6 +938,7 @@ function startAttention() {
function stopAttention() {
if (state.attentionTimer) clearInterval(state.attentionTimer);
state.attentionTimer = null;
+ stopRepeatingSound();
state.attentionOn = false;
document.title = state.originalTitle;
const favicon = document.querySelector("link[rel='icon']");