залил

This commit is contained in:
2026-04-15 08:00:15 +03:00
commit 5549b3545e
51 changed files with 8073 additions and 0 deletions
+174
View File
@@ -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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
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}}