залил
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
{{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>
|
||||
<div class="card">
|
||||
<h3>Общие настройки звуков</h3>
|
||||
<label>
|
||||
<input type="checkbox" id="duplicate-sounds-checkbox"> Дублировать звуки из команд в локальное воспроизведение
|
||||
</label>
|
||||
<p class="help">При включении, звуки, отправляемые в веб-сервисы оповещений, также будут проигрываться на компьютере стримера.</p>
|
||||
</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;
|
||||
});
|
||||
}
|
||||
async function loadDuplicateSetting() {
|
||||
const res = await fetch('/api/settings/duplicate_sounds');
|
||||
const data = await res.json();
|
||||
document.getElementById('duplicate-sounds-checkbox').checked = data.enabled;
|
||||
}
|
||||
document.getElementById('duplicate-sounds-checkbox').addEventListener('change', async (e) => {
|
||||
await fetch('/api/settings/duplicate_sounds', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: e.target.checked })
|
||||
});
|
||||
});
|
||||
loadDuplicateSetting();
|
||||
loadSettings();
|
||||
loadSounds();
|
||||
</script>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user