156 lines
6.2 KiB
HTML
156 lines
6.2 KiB
HTML
{{define "content"}}
|
||
<h2>Звуковые уведомления</h2>
|
||
<div class="card">
|
||
<h3>События чата и Twitch</h3>
|
||
<table id="notif-table">
|
||
<thead>
|
||
<tr><th>Событие</th><th>Звуковой файл</th><th>Громкость</th><th>Вкл.</th><th>Действия</th></tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<button id="add-defaults">➕ Добавить события по умолчанию</button>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Управление звуковыми файлами</h3>
|
||
<input type="file" id="sound-upload" accept=".mp3,.wav">
|
||
<button id="upload-btn">Загрузить</button>
|
||
<ul id="sound-list"></ul>
|
||
</div>
|
||
|
||
<script>
|
||
let settings = [];
|
||
let soundFiles = [];
|
||
|
||
async function loadSettings() {
|
||
const res = await fetch('/api/notifications');
|
||
settings = await res.json();
|
||
renderTable();
|
||
}
|
||
|
||
async function loadSounds() {
|
||
const res = await fetch('/api/sounds');
|
||
soundFiles = await res.json();
|
||
const list = document.getElementById('sound-list');
|
||
if (soundFiles.length === 0) {
|
||
list.innerHTML = '<li>Нет загруженных звуков. Загрузите MP3 или WAV.</li>';
|
||
} else {
|
||
list.innerHTML = soundFiles.map(f => `<li>${escapeHtml(f)} <button class="delete-sound" data-file="${f}">🗑️</button></li>`).join('');
|
||
document.querySelectorAll('.delete-sound').forEach(btn => {
|
||
btn.addEventListener('click', () => deleteSound(btn.dataset.file));
|
||
});
|
||
}
|
||
// Обновляем выпадающие списки в таблице
|
||
renderTable();
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.querySelector('#notif-table tbody');
|
||
if (!settings.length) {
|
||
tbody.innerHTML = '<tr><td colspan="5">Нет настроек. Нажмите "Добавить события по умолчанию".</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = settings.map(s => `
|
||
<tr data-event="${s.event_name}">
|
||
<td>${escapeHtml(s.event_name)}</td>
|
||
<td>
|
||
<select class="sound-select" data-event="${s.event_name}">
|
||
<option value="">— без звука —</option>
|
||
${soundFiles.map(f => `<option value="data/sounds/${f}" ${s.sound_file === `data/sounds/${f}` ? 'selected' : ''}>${f}</option>`).join('')}
|
||
</select>
|
||
</td>
|
||
<td>
|
||
<input type="range" class="volume-slider" min="0" max="100" value="${s.volume}" data-event="${s.event_name}">
|
||
<span class="vol-value">${s.volume}</span>%
|
||
</td>
|
||
<td><input type="checkbox" class="enable-checkbox" ${s.enabled ? 'checked' : ''} data-event="${s.event_name}"></td>
|
||
<td><button class="test-btn" data-event="${s.event_name}">🔊 Тест</button></td>
|
||
</tr>
|
||
`).join('');
|
||
|
||
// Привязываем обработчики
|
||
document.querySelectorAll('.sound-select').forEach(sel => {
|
||
sel.addEventListener('change', (e) => updateSetting(e.target.dataset.event, 'sound_file', sel.value));
|
||
});
|
||
document.querySelectorAll('.volume-slider').forEach(slider => {
|
||
slider.addEventListener('input', (e) => {
|
||
const val = e.target.value;
|
||
const span = e.target.parentElement.querySelector('.vol-value');
|
||
span.innerText = val;
|
||
updateSetting(e.target.dataset.event, 'volume', parseInt(val));
|
||
});
|
||
});
|
||
document.querySelectorAll('.enable-checkbox').forEach(cb => {
|
||
cb.addEventListener('change', (e) => updateSetting(e.target.dataset.event, 'enabled', cb.checked));
|
||
});
|
||
document.querySelectorAll('.test-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => testSound(btn.dataset.event));
|
||
});
|
||
}
|
||
|
||
async function updateSetting(eventName, field, value) {
|
||
const setting = settings.find(s => s.event_name === eventName);
|
||
if (!setting) return;
|
||
setting[field] = value;
|
||
const res = await fetch('/api/notifications', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(setting)
|
||
});
|
||
if (!res.ok) alert('Ошибка сохранения');
|
||
}
|
||
|
||
async function testSound(eventName) {
|
||
await fetch(`/api/notifications/test?event=${encodeURIComponent(eventName)}`);
|
||
}
|
||
|
||
async function deleteSound(filename) {
|
||
if (!confirm(`Удалить ${filename}?`)) return;
|
||
const res = await fetch(`/api/sounds?file=${encodeURIComponent(filename)}`, { method: 'DELETE' });
|
||
if (res.ok) {
|
||
await loadSounds();
|
||
await loadSettings();
|
||
} else {
|
||
alert('Ошибка удаления');
|
||
}
|
||
}
|
||
|
||
document.getElementById('upload-btn').onclick = async () => {
|
||
const fileInput = document.getElementById('sound-upload');
|
||
if (!fileInput.files.length) return;
|
||
const formData = new FormData();
|
||
formData.append('sound', fileInput.files[0]);
|
||
const res = await fetch('/api/sounds/upload', { method: 'POST', body: formData });
|
||
if (res.ok) {
|
||
await loadSounds();
|
||
await loadSettings();
|
||
fileInput.value = '';
|
||
} else {
|
||
alert('Ошибка загрузки');
|
||
}
|
||
};
|
||
|
||
document.getElementById('add-defaults').onclick = async () => {
|
||
const res = await fetch('/api/notifications/defaults', { method: 'POST' });
|
||
if (res.ok) {
|
||
await loadSettings();
|
||
await loadSounds();
|
||
} else {
|
||
alert('Ошибка добавления событий по умолчанию');
|
||
}
|
||
};
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return str.replace(/[&<>]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
loadSettings();
|
||
loadSounds();
|
||
</script>
|
||
{{end}} |