434 lines
18 KiB
HTML
434 lines
18 KiB
HTML
{{define "content"}}
|
||
<h2>Команды чата</h2>
|
||
|
||
<div class="card">
|
||
<h3>Добавить / редактировать команду</h3>
|
||
<form id="command-form">
|
||
<input type="hidden" id="command-id" value="0">
|
||
<label>Триггер (без !):</label>
|
||
<input type="text" id="trigger" required placeholder="ping">
|
||
|
||
<label>Шаблон ответа:</label>
|
||
<textarea id="template" rows="5" required placeholder="Pong! <USERNAME/>"></textarea>
|
||
|
||
<!-- Панель кнопок для вставки тегов -->
|
||
<div style="margin: 8px 0; display: flex; flex-wrap: wrap; gap: 6px;">
|
||
<button type="button" class="tag-btn" data-tag="<USERNAME/>">👤 USERNAME</button>
|
||
<button type="button" class="tag-btn" data-tag="<ARG/>">📝 ARG</button>
|
||
<button type="button" class="tag-btn" data-tag="<AI/>">🤖 AI</button>
|
||
<button type="button" class="tag-btn" data-tag="<RANDOMUSER/>">🎲 RANDOMUSER</button>
|
||
<button type="button" id="random-btn">🔢 Случайное число</button>
|
||
<button type="button" id="song-btn">🎵 Вставить звук</button>
|
||
<button type="button" id="group-btn">📦 Обертка group</button>
|
||
<button type="button" id="timeout-btn">⏱ Отстранение</button>
|
||
</div>
|
||
|
||
<div>
|
||
<button type="button" id="test-btn">🧪 Тестировать</button>
|
||
</div>
|
||
|
||
<label>
|
||
<input type="checkbox" id="enabled" checked> Включена
|
||
</label>
|
||
<label>Кулдаун (секунды):</label>
|
||
<input type="number" id="cooldown" value="0" min="0">
|
||
<label>Права доступа:</label>
|
||
<select id="permission">
|
||
<option value="everyone">Все</option>
|
||
<option value="moderator">Модераторы</option>
|
||
<option value="subscriber">Подписчики</option>
|
||
<option value="vip">VIP</option>
|
||
<option value="broadcaster">Стример</option>
|
||
</select>
|
||
<button type="submit">Сохранить</button>
|
||
<button type="button" id="cancel-edit" style="background: #6c757d;">Отмена</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Список команд</h3>
|
||
<table style="width:100%; border-collapse: collapse;">
|
||
<thead>
|
||
<tr><th>Триггер</th><th>Шаблон</th><th>Кулдаун</th><th>Права</th><th>Статус</th><th>Действия</th></tr>
|
||
</thead>
|
||
<tbody id="commands-table">
|
||
<tr><td colspan="6">Загрузка...</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Модальное окно для тестирования -->
|
||
<div id="test-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>Результат тестирования</h3>
|
||
<p><strong>Текст для чата:</strong></p>
|
||
<pre id="test-result" style="background:#f0f0f0; padding:10px; border-radius:4px; white-space:pre-wrap;"></pre>
|
||
<p><strong>Звуковые файлы:</strong> <span id="test-sounds"></span></p>
|
||
<button id="close-modal" style="margin-top:10px;">Закрыть</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для случайного числа -->
|
||
<div id="random-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>Вставить случайное число</h3>
|
||
<label>Минимум:</label>
|
||
<input type="number" id="random-min" value="1">
|
||
<label>Максимум:</label>
|
||
<input type="number" id="random-max" value="100">
|
||
<div style="margin-top:15px;">
|
||
<button id="insert-random-btn">Вставить</button>
|
||
<button id="cancel-random-btn">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для звука -->
|
||
<div id="song-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>Вставить звук</h3>
|
||
<label>Выберите звуковой файл:</label>
|
||
<select id="song-select" style="width:100%;">
|
||
<option value="">-- нет звуков --</option>
|
||
</select>
|
||
<div style="margin-top:15px;">
|
||
<button id="insert-song-btn">Вставить</button>
|
||
<button id="cancel-song-btn">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для группы -->
|
||
<div id="group-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>Создать группу вариантов</h3>
|
||
<div id="group-variants">
|
||
<div class="group-variant" style="margin-bottom:8px;">
|
||
<input type="text" class="variant-input" placeholder="Вариант 1" style="width:80%;">
|
||
</div>
|
||
<div class="group-variant" style="margin-bottom:8px;">
|
||
<input type="text" class="variant-input" placeholder="Вариант 2" style="width:80%;">
|
||
</div>
|
||
</div>
|
||
<button type="button" id="add-variant-btn">+ Добавить вариант</button>
|
||
<button type="button" id="remove-variant-btn" style="margin-left:10px;">− Удалить последний</button>
|
||
<div style="margin-top:15px;">
|
||
<button id="insert-group-btn">Вставить</button>
|
||
<button id="cancel-group-btn">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Модальное окно для отстранения (timeout) -->
|
||
<div id="timeout-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3>Отстранение (таймаут)</h3>
|
||
<label>Количество минут:</label>
|
||
<input type="number" id="timeout-minutes" value="5" min="1" max="1440">
|
||
<div style="margin-top:15px;">
|
||
<button id="insert-timeout-btn">Вставить</button>
|
||
<button id="cancel-timeout-btn">Отмена</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let commands = [];
|
||
let soundList = [];
|
||
|
||
async function loadSoundList() {
|
||
try {
|
||
const res = await fetch('/api/sounds/list');
|
||
if (res.ok) {
|
||
soundList = await res.json();
|
||
const select = document.getElementById('song-select');
|
||
select.innerHTML = '<option value="">-- выберите звук --</option>';
|
||
soundList.forEach(sound => {
|
||
const option = document.createElement('option');
|
||
option.value = sound;
|
||
option.textContent = sound;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
} catch(e) { console.error('Failed to load sounds', e); }
|
||
}
|
||
|
||
function insertAtCursor(textareaId, text) {
|
||
const textarea = document.getElementById(textareaId);
|
||
if (!textarea) return;
|
||
const start = textarea.selectionStart;
|
||
const end = textarea.selectionEnd;
|
||
const value = textarea.value;
|
||
textarea.value = value.slice(0, start) + text + value.slice(end);
|
||
textarea.selectionStart = textarea.selectionEnd = start + text.length;
|
||
textarea.focus();
|
||
}
|
||
|
||
// Простые теги
|
||
document.querySelectorAll('.tag-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
insertAtCursor('template', btn.dataset.tag);
|
||
});
|
||
});
|
||
|
||
// Random
|
||
document.getElementById('random-btn').addEventListener('click', () => {
|
||
document.getElementById('random-modal').style.display = 'flex';
|
||
});
|
||
document.getElementById('insert-random-btn').addEventListener('click', () => {
|
||
const min = document.getElementById('random-min').value;
|
||
const max = document.getElementById('random-max').value;
|
||
const tag = `<random s=${min} e=${max}/>`;
|
||
insertAtCursor('template', tag);
|
||
document.getElementById('random-modal').style.display = 'none';
|
||
});
|
||
document.getElementById('cancel-random-btn').addEventListener('click', () => {
|
||
document.getElementById('random-modal').style.display = 'none';
|
||
});
|
||
|
||
// Song
|
||
document.getElementById('song-btn').addEventListener('click', async () => {
|
||
await loadSoundList();
|
||
document.getElementById('song-modal').style.display = 'flex';
|
||
});
|
||
document.getElementById('insert-song-btn').addEventListener('click', () => {
|
||
const selected = document.getElementById('song-select').value;
|
||
if (!selected) {
|
||
alert('Выберите звуковой файл');
|
||
return;
|
||
}
|
||
const tag = `<song f="data/sounds/${selected}"/>`;
|
||
insertAtCursor('template', tag);
|
||
document.getElementById('song-modal').style.display = 'none';
|
||
});
|
||
document.getElementById('cancel-song-btn').addEventListener('click', () => {
|
||
document.getElementById('song-modal').style.display = 'none';
|
||
});
|
||
|
||
// Group
|
||
document.getElementById('group-btn').addEventListener('click', () => {
|
||
const container = document.getElementById('group-variants');
|
||
container.innerHTML = `
|
||
<div class="group-variant" style="margin-bottom:8px;">
|
||
<input type="text" class="variant-input" placeholder="Вариант 1" style="width:80%;">
|
||
</div>
|
||
<div class="group-variant" style="margin-bottom:8px;">
|
||
<input type="text" class="variant-input" placeholder="Вариант 2" style="width:80%;">
|
||
</div>
|
||
`;
|
||
document.getElementById('group-modal').style.display = 'flex';
|
||
});
|
||
document.getElementById('add-variant-btn').addEventListener('click', () => {
|
||
const container = document.getElementById('group-variants');
|
||
const newDiv = document.createElement('div');
|
||
newDiv.className = 'group-variant';
|
||
newDiv.style.marginBottom = '8px';
|
||
newDiv.innerHTML = `<input type="text" class="variant-input" placeholder="Новый вариант" style="width:80%;">`;
|
||
container.appendChild(newDiv);
|
||
});
|
||
document.getElementById('remove-variant-btn').addEventListener('click', () => {
|
||
const container = document.getElementById('group-variants');
|
||
if (container.children.length > 2) {
|
||
container.removeChild(container.lastChild);
|
||
} else {
|
||
alert('Должно быть хотя бы два варианта');
|
||
}
|
||
});
|
||
document.getElementById('insert-group-btn').addEventListener('click', () => {
|
||
const inputs = document.querySelectorAll('#group-variants .variant-input');
|
||
let variants = [];
|
||
inputs.forEach(inp => {
|
||
let val = inp.value.trim();
|
||
if (val !== '') variants.push(val);
|
||
});
|
||
if (variants.length < 2) {
|
||
alert('Введите хотя бы два непустых варианта');
|
||
return;
|
||
}
|
||
let groupHtml = '<group>\n';
|
||
variants.forEach(v => {
|
||
groupHtml += ` <g>${escapeHtmlForGroup(v)}</g>\n`;
|
||
});
|
||
groupHtml += '</group>';
|
||
insertAtCursor('template', groupHtml);
|
||
document.getElementById('group-modal').style.display = 'none';
|
||
});
|
||
document.getElementById('cancel-group-btn').addEventListener('click', () => {
|
||
document.getElementById('group-modal').style.display = 'none';
|
||
});
|
||
|
||
// Timeout
|
||
document.getElementById('timeout-btn').addEventListener('click', () => {
|
||
document.getElementById('timeout-modal').style.display = 'flex';
|
||
});
|
||
document.getElementById('insert-timeout-btn').addEventListener('click', () => {
|
||
const minutes = document.getElementById('timeout-minutes').value;
|
||
const tag = `<timeout minutes="${minutes}"/>`;
|
||
insertAtCursor('template', tag);
|
||
document.getElementById('timeout-modal').style.display = 'none';
|
||
});
|
||
document.getElementById('cancel-timeout-btn').addEventListener('click', () => {
|
||
document.getElementById('timeout-modal').style.display = 'none';
|
||
});
|
||
|
||
function escapeHtmlForGroup(str) {
|
||
if (!str) return '';
|
||
return str.replace(/[&<>]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
// Закрытие модалок по клику вне области
|
||
window.addEventListener('click', (e) => {
|
||
const modals = ['random-modal', 'song-modal', 'group-modal', 'test-modal', 'timeout-modal'];
|
||
modals.forEach(id => {
|
||
const modal = document.getElementById(id);
|
||
if (e.target === modal) modal.style.display = 'none';
|
||
});
|
||
});
|
||
|
||
// --- Остальной код команд (без изменений) ---
|
||
async function loadCommands() {
|
||
const res = await fetch('/api/commands');
|
||
commands = await res.json();
|
||
renderTable();
|
||
}
|
||
|
||
function renderTable() {
|
||
const tbody = document.getElementById('commands-table');
|
||
if (!commands.length) {
|
||
tbody.innerHTML = '<tr><td colspan="6">Нет команд. Добавьте первую!</td></tr>';
|
||
return;
|
||
}
|
||
tbody.innerHTML = commands.map(cmd => `
|
||
<tr>
|
||
<td>!${escapeHtml(cmd.Trigger)}</td>
|
||
<td style="max-width: 300px; overflow-x: auto;">${escapeHtml(cmd.Template)}</td>
|
||
<td>${cmd.CooldownSec}</td>
|
||
<td>${cmd.Permission}</td>
|
||
<td>${cmd.Enabled ? '✅ Вкл' : '❌ Выкл'}</td>
|
||
<td>
|
||
<button onclick="editCommand(${cmd.ID})">✏️</button>
|
||
<button onclick="deleteCommand(${cmd.ID})">🗑️</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return str.replace(/[&<>]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
function editCommand(id) {
|
||
const cmd = commands.find(c => c.ID === id);
|
||
if (!cmd) return;
|
||
document.getElementById('command-id').value = cmd.ID;
|
||
document.getElementById('trigger').value = cmd.Trigger;
|
||
document.getElementById('template').value = cmd.Template;
|
||
document.getElementById('enabled').checked = cmd.Enabled;
|
||
document.getElementById('cooldown').value = cmd.CooldownSec;
|
||
document.getElementById('permission').value = cmd.Permission;
|
||
document.querySelector('.card').scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
|
||
async function deleteCommand(id) {
|
||
if (!confirm('Удалить команду?')) return;
|
||
await fetch(`/api/commands?id=${id}`, { method: 'DELETE' });
|
||
loadCommands();
|
||
if (document.getElementById('command-id').value == id) resetForm();
|
||
}
|
||
|
||
function resetForm() {
|
||
document.getElementById('command-id').value = 0;
|
||
document.getElementById('trigger').value = '';
|
||
document.getElementById('template').value = '';
|
||
document.getElementById('enabled').checked = true;
|
||
document.getElementById('cooldown').value = 0;
|
||
document.getElementById('permission').value = 'everyone';
|
||
}
|
||
|
||
document.getElementById('cancel-edit').addEventListener('click', resetForm);
|
||
|
||
document.getElementById('command-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const id = parseInt(document.getElementById('command-id').value);
|
||
const trigger = document.getElementById('trigger').value.trim();
|
||
const template = document.getElementById('template').value;
|
||
const enabled = document.getElementById('enabled').checked;
|
||
const cooldown = parseInt(document.getElementById('cooldown').value);
|
||
const permission = document.getElementById('permission').value;
|
||
|
||
if (!trigger || !template) {
|
||
alert('Заполните триггер и шаблон');
|
||
return;
|
||
}
|
||
|
||
const method = id === 0 ? 'POST' : 'PUT';
|
||
const url = '/api/commands';
|
||
const body = JSON.stringify({ ID: id, Trigger: trigger, Template: template, Enabled: enabled, CooldownSec: cooldown, Permission: permission });
|
||
|
||
try {
|
||
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body });
|
||
if (res.ok) {
|
||
loadCommands();
|
||
resetForm();
|
||
} else {
|
||
const err = await res.text();
|
||
alert('Ошибка: ' + err);
|
||
}
|
||
} catch (err) {
|
||
alert('Ошибка соединения: ' + err.message);
|
||
}
|
||
});
|
||
|
||
document.getElementById('test-btn').addEventListener('click', async () => {
|
||
const template = document.getElementById('template').value;
|
||
if (!template) {
|
||
alert('Введите шаблон для тестирования');
|
||
return;
|
||
}
|
||
const username = prompt('Введите имя пользователя для подстановки (без @):', 'TestUser');
|
||
if (!username) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/commands/test', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ template, username })
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.text();
|
||
alert('Ошибка тестирования: ' + err);
|
||
return;
|
||
}
|
||
const data = await res.json();
|
||
document.getElementById('test-result').textContent = data.result;
|
||
const soundsSpan = document.getElementById('test-sounds');
|
||
if (data.soundFiles && data.soundFiles.length) {
|
||
soundsSpan.innerHTML = data.soundFiles.join('<br>');
|
||
} else {
|
||
soundsSpan.textContent = 'нет';
|
||
}
|
||
document.getElementById('test-modal').style.display = 'flex';
|
||
} catch (err) {
|
||
alert('Ошибка: ' + err.message);
|
||
}
|
||
});
|
||
|
||
document.getElementById('close-modal').addEventListener('click', () => {
|
||
document.getElementById('test-modal').style.display = 'none';
|
||
});
|
||
|
||
loadCommands();
|
||
loadSoundList();
|
||
</script>
|
||
{{end}} |