Add admin sound preferences
This commit is contained in:
+33
-2
@@ -124,8 +124,7 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
}
|
}
|
||||||
.logout-compact {
|
.logout-compact {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 8px;
|
padding: 0;
|
||||||
font-size: 11px;
|
|
||||||
}
|
}
|
||||||
.online-dot-toggle {
|
.online-dot-toggle {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
@@ -919,6 +918,38 @@ label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; }
|
|||||||
.settings h2 { margin: 0 0 12px; }
|
.settings h2 { margin: 0 0 12px; }
|
||||||
.settings h3 { margin: 20px 0 8px; }
|
.settings h3 { margin: 20px 0 8px; }
|
||||||
.settings form { display: grid; gap: 12px; }
|
.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 {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|||||||
+62
-37
@@ -44,7 +44,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<input id="adminSoundVolume" type="range" min="0" max="100" step="5" aria-label="Hlasitost notifikaci">
|
<input id="adminSoundVolume" type="range" min="0" max="100" step="5" aria-label="Hlasitost notifikaci">
|
||||||
</div>
|
</div>
|
||||||
<button id="logoutButton" class="logout-compact" type="button">Odhlasit</button>
|
<button id="logoutButton" class="logout-compact icon-button" type="button" title="Odhlasit" aria-label="Odhlasit">
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path d="M10 3h9v18h-9v-2h7V5h-7V3Zm-1.2 5.2 1.4 1.4L7.8 12l2.4 2.4-1.4 1.4L4 11l4.8-2.8ZM5.4 11h8.8v2H5.4v-2Z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="list-tools">
|
<div class="list-tools">
|
||||||
@@ -101,44 +105,65 @@
|
|||||||
|
|
||||||
<aside id="settingsPanel" class="settings hidden">
|
<aside id="settingsPanel" class="settings hidden">
|
||||||
<div class="settings-head">
|
<div class="settings-head">
|
||||||
<h2>Nastaveni widgetu</h2>
|
<h2>Nastaveni</h2>
|
||||||
<button id="closeSettingsButton" type="button">Zavrit</button>
|
<button id="closeSettingsButton" type="button">Zavrit</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="settingsForm">
|
<div class="settings-tabs" role="tablist" aria-label="Nastaveni">
|
||||||
<label>Titulek CS <input name="titleCs"></label>
|
<button class="active" type="button" data-settings-tab="widget">Widget</button>
|
||||||
<label>Intro CS <textarea name="introCs" rows="2"></textarea></label>
|
<button type="button" data-settings-tab="admin">BO</button>
|
||||||
<label>Titulek EN <input name="titleEn"></label>
|
</div>
|
||||||
<label>Intro EN <textarea name="introEn" rows="2"></textarea></label>
|
<section class="settings-tab-panel active" data-settings-panel="widget">
|
||||||
<label class="color-field">Barva
|
<form id="settingsForm">
|
||||||
<span class="color-row">
|
<label>Titulek CS <input name="titleCs"></label>
|
||||||
<input name="brand" type="color">
|
<label>Intro CS <textarea name="introCs" rows="2"></textarea></label>
|
||||||
<input name="brandHex" class="hex-input" type="text" inputmode="text" maxlength="7" placeholder="#0f8f6f" pattern="#?[0-9a-fA-F]{6}">
|
<label>Titulek EN <input name="titleEn"></label>
|
||||||
</span>
|
<label>Intro EN <textarea name="introEn" rows="2"></textarea></label>
|
||||||
</label>
|
<label class="color-field">Barva
|
||||||
<label>Pozice desktop <select name="desktopSide"><option>right</option><option>left</option></select></label>
|
<span class="color-row">
|
||||||
<label>Desktop bottom px <input name="desktopBottom" type="number"></label>
|
<input name="brand" type="color">
|
||||||
<label>Desktop offset px <input name="desktopOffset" type="number"></label>
|
<input name="brandHex" class="hex-input" type="text" inputmode="text" maxlength="7" placeholder="#0f8f6f" pattern="#?[0-9a-fA-F]{6}">
|
||||||
<label>Pozice mobil <select name="mobileSide"><option>right</option><option>left</option></select></label>
|
</span>
|
||||||
<label>Mobil bottom px <input name="mobileBottom" type="number"></label>
|
</label>
|
||||||
<label>Mobil offset px <input name="mobileOffset" type="number"></label>
|
<label>Pozice desktop <select name="desktopSide"><option>right</option><option>left</option></select></label>
|
||||||
<fieldset class="effect-settings">
|
<label>Desktop bottom px <input name="desktopBottom" type="number"></label>
|
||||||
<legend>Online efekt tlacitka</legend>
|
<label>Desktop offset px <input name="desktopOffset" type="number"></label>
|
||||||
<label><input name="launcherBreathe" type="checkbox"> jemne dychani</label>
|
<label>Pozice mobil <select name="mobileSide"><option>right</option><option>left</option></select></label>
|
||||||
<label><input name="launcherRing" type="checkbox"> svetelny prstenec</label>
|
<label>Mobil bottom px <input name="mobileBottom" type="number"></label>
|
||||||
<label><input name="launcherHoverLabel" type="checkbox"> napis pri najeti</label>
|
<label>Mobil offset px <input name="mobileOffset" type="number"></label>
|
||||||
<label><input name="launcherAvatar" type="checkbox"> mini avatar styl</label>
|
<fieldset class="effect-settings">
|
||||||
</fieldset>
|
<legend>Online efekt tlacitka</legend>
|
||||||
<fieldset class="effect-settings">
|
<label><input name="launcherBreathe" type="checkbox"> jemne dychani</label>
|
||||||
<legend>Efekt pri nove zprave</legend>
|
<label><input name="launcherRing" type="checkbox"> svetelny prstenec</label>
|
||||||
<label><input name="messagePulse" type="checkbox"> pulzovani</label>
|
<label><input name="launcherHoverLabel" type="checkbox"> napis pri najeti</label>
|
||||||
<label><input name="messageBadge" type="checkbox"> badge s poctem</label>
|
<label><input name="launcherAvatar" type="checkbox"> mini avatar styl</label>
|
||||||
<label><input name="messageLabel" type="checkbox"> kratky stitek</label>
|
</fieldset>
|
||||||
<label><input name="messageWiggle" type="checkbox"> zhoupnuti</label>
|
<fieldset class="effect-settings">
|
||||||
</fieldset>
|
<legend>Efekt pri nove zprave</legend>
|
||||||
<button type="submit">Ulozit</button>
|
<label><input name="messagePulse" type="checkbox"> pulzovani</label>
|
||||||
</form>
|
<label><input name="messageBadge" type="checkbox"> badge s poctem</label>
|
||||||
<h3>GTM snippet</h3>
|
<label><input name="messageLabel" type="checkbox"> kratky stitek</label>
|
||||||
<pre id="snippet"></pre>
|
<label><input name="messageWiggle" type="checkbox"> zhoupnuti</label>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Ulozit</button>
|
||||||
|
</form>
|
||||||
|
<h3>GTM snippet</h3>
|
||||||
|
<pre id="snippet"></pre>
|
||||||
|
</section>
|
||||||
|
<section class="settings-tab-panel" data-settings-panel="admin">
|
||||||
|
<div class="admin-preferences">
|
||||||
|
<label class="setting-switch">Zvuk notifikaci <input id="adminSoundToggleSettings" type="checkbox"></label>
|
||||||
|
<label>Hlasitost <input id="adminSoundVolumeSettings" type="range" min="0" max="100" step="5"></label>
|
||||||
|
<label>Styl zvuku
|
||||||
|
<select id="adminSoundStyle">
|
||||||
|
<option value="bright">Jasny</option>
|
||||||
|
<option value="soft">Jemny</option>
|
||||||
|
<option value="chime">Cinknuti</option>
|
||||||
|
<option value="urgent">Vyrazny</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button id="adminSoundTest" type="button">Otestovat zvuk</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+88
-26
@@ -11,6 +11,7 @@ const state = {
|
|||||||
hideBots: localStorage.getItem("mf_admin_hide_bots") !== "0",
|
hideBots: localStorage.getItem("mf_admin_hide_bots") !== "0",
|
||||||
soundMuted: localStorage.getItem("mf_admin_sound_muted") === "1",
|
soundMuted: localStorage.getItem("mf_admin_sound_muted") === "1",
|
||||||
soundVolume: Number(localStorage.getItem("mf_admin_sound_volume") || 70),
|
soundVolume: Number(localStorage.getItem("mf_admin_sound_volume") || 70),
|
||||||
|
soundStyle: localStorage.getItem("mf_admin_sound_style") || "bright",
|
||||||
replyFiles: [],
|
replyFiles: [],
|
||||||
attentionTimer: null,
|
attentionTimer: null,
|
||||||
attentionOn: false,
|
attentionOn: false,
|
||||||
@@ -48,22 +49,35 @@ $("#logoutButton").addEventListener("click", async () => {
|
|||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.remove("hidden"));
|
$("#settingsButton").addEventListener("click", () => $("#settingsPanel").classList.toggle("hidden"));
|
||||||
$("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden"));
|
$("#closeSettingsButton").addEventListener("click", () => $("#settingsPanel").classList.add("hidden"));
|
||||||
$("#adminSoundToggle").addEventListener("click", () => {
|
$("#adminSoundToggle").addEventListener("click", () => {
|
||||||
state.soundMuted = !state.soundMuted;
|
setAdminSoundMuted(!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();
|
|
||||||
});
|
});
|
||||||
$("#adminSoundVolume").addEventListener("input", (event) => {
|
$("#adminSoundVolume").addEventListener("input", (event) => {
|
||||||
state.soundVolume = clamp(Number(event.target.value), 0, 100);
|
setAdminSoundVolume(Number(event.target.value));
|
||||||
state.soundMuted = state.soundVolume <= 0;
|
});
|
||||||
localStorage.setItem("mf_admin_sound_volume", String(state.soundVolume));
|
$("#adminSoundToggleSettings").addEventListener("change", (event) => {
|
||||||
localStorage.setItem("mf_admin_sound_muted", state.soundMuted ? "1" : "0");
|
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();
|
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").brand.addEventListener("input", (event) => {
|
||||||
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
$("#settingsForm").brandHex.value = normalizeHexColor(event.target.value) || event.target.value;
|
||||||
});
|
});
|
||||||
@@ -795,33 +809,81 @@ function notify() {
|
|||||||
function renderAdminSoundControls() {
|
function renderAdminSoundControls() {
|
||||||
const toggle = $("#adminSoundToggle");
|
const toggle = $("#adminSoundToggle");
|
||||||
const volume = $("#adminSoundVolume");
|
const volume = $("#adminSoundVolume");
|
||||||
if (!toggle || !volume) return;
|
const settingsToggle = $("#adminSoundToggleSettings");
|
||||||
|
const settingsVolume = $("#adminSoundVolumeSettings");
|
||||||
|
const settingsStyle = $("#adminSoundStyle");
|
||||||
const safeVolume = clamp(state.soundVolume, 0, 100);
|
const safeVolume = clamp(state.soundVolume, 0, 100);
|
||||||
toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0);
|
if (toggle) {
|
||||||
toggle.setAttribute("aria-label", state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci");
|
toggle.classList.toggle("muted", state.soundMuted || safeVolume <= 0);
|
||||||
toggle.title = state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci";
|
toggle.setAttribute("aria-label", state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci");
|
||||||
volume.value = String(safeVolume);
|
toggle.title = state.soundMuted ? "Zapnout zvuk notifikaci" : "Vypnout zvuk notifikaci";
|
||||||
volume.title = `Hlasitost ${safeVolume}%`;
|
}
|
||||||
|
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);
|
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;
|
const AudioCtor = window.AudioContext || window.webkitAudioContext;
|
||||||
if (!AudioCtor) return;
|
if (!AudioCtor) return;
|
||||||
const audio = new AudioCtor();
|
const audio = new AudioCtor();
|
||||||
const gain = audio.createGain();
|
const gain = audio.createGain();
|
||||||
gain.gain.value = 0.12 * (volume / 100);
|
gain.gain.value = 0.12 * ((volume || 70) / 100);
|
||||||
gain.connect(audio.destination);
|
gain.connect(audio.destination);
|
||||||
[0, 0.14].forEach((offset, index) => {
|
for (const tone of soundPattern(state.soundStyle)) {
|
||||||
const osc = audio.createOscillator();
|
const osc = audio.createOscillator();
|
||||||
osc.type = "sine";
|
osc.type = tone.type;
|
||||||
osc.frequency.value = index ? 1040 : 820;
|
osc.frequency.value = tone.frequency;
|
||||||
osc.connect(gain);
|
osc.connect(gain);
|
||||||
osc.start(audio.currentTime + offset);
|
osc.start(audio.currentTime + tone.offset);
|
||||||
osc.stop(audio.currentTime + offset + 0.11);
|
osc.stop(audio.currentTime + tone.offset + tone.duration);
|
||||||
});
|
}
|
||||||
setTimeout(() => audio.close().catch(() => {}), 520);
|
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) {
|
function clamp(value, min, max) {
|
||||||
|
|||||||
Reference in New Issue
Block a user