296 lines
15 KiB
HTML
296 lines
15 KiB
HTML
{{define "content"}}
|
||
<h2>События Twitch</h2>
|
||
<p>Настройте цепочку действий, которые будут выполняться при наступлении события (подписка, рейд, награда и т.д.)</p>
|
||
|
||
<div class="card">
|
||
<label for="event-select">Выберите событие:</label>
|
||
<select id="event-select">
|
||
<option value="follow">Подписка на канал (follow)</option>
|
||
<option value="subscribe">Подписка (subscribe)</option>
|
||
<option value="gift_sub">Подарочная подписка (gift_sub)</option>
|
||
<option value="raid">Рейд (raid)</option>
|
||
<option value="reward_redemption">Награда за баллы (reward_redemption)</option>
|
||
</select>
|
||
<button id="load-actions">Загрузить действия</button>
|
||
</div>
|
||
|
||
<div class="card" id="actions-editor" style="display: none;">
|
||
<h3>Цепочка действий</h3>
|
||
<div id="actions-list"></div>
|
||
<button id="add-action">➕ Добавить действие</button>
|
||
<button id="save-actions">💾 Сохранить</button>
|
||
</div>
|
||
|
||
<!-- Модальное окно для создания/редактирования действия -->
|
||
<div id="action-modal" class="modal">
|
||
<div class="modal-content">
|
||
<h3 id="modal-title">Действие</h3>
|
||
<form id="action-form">
|
||
<input type="hidden" id="action-index" value="-1">
|
||
<label>Тип действия:</label>
|
||
<select id="action-type">
|
||
<option value="send_message">Отправить сообщение</option>
|
||
<option value="play_sound">Воспроизвести звук</option>
|
||
<option value="press_hotkey">Нажать горячую клавишу</option>
|
||
<option value="http_request">HTTP запрос (GET)</option>
|
||
<option value="run_program">Запустить программу</option>
|
||
<option value="send_alert">Отправить уведомление (alert)</option>
|
||
</select>
|
||
|
||
<div id="fields-container"></div>
|
||
|
||
<div style="margin-top: 15px;">
|
||
<button type="submit">Сохранить</button>
|
||
<button type="button" id="close-action-modal">Отмена</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentPlatform = "twitch";
|
||
let currentEvent = "";
|
||
let actions = [];
|
||
let editingIndex = -1;
|
||
|
||
// Списки для выпадающих полей
|
||
let soundFiles = [];
|
||
let imageFiles = [];
|
||
|
||
const eventSelect = document.getElementById('event-select');
|
||
const loadBtn = document.getElementById('load-actions');
|
||
const actionsDiv = document.getElementById('actions-list');
|
||
const editorDiv = document.getElementById('actions-editor');
|
||
const addBtn = document.getElementById('add-action');
|
||
const saveBtn = document.getElementById('save-actions');
|
||
|
||
const actionModal = document.getElementById('action-modal');
|
||
const actionTypeSelect = document.getElementById('action-type');
|
||
const fieldsContainer = document.getElementById('fields-container');
|
||
const actionForm = document.getElementById('action-form');
|
||
const actionIndexInput = document.getElementById('action-index');
|
||
const closeModalBtn = document.getElementById('close-action-modal');
|
||
|
||
// Загрузка списков звуков и изображений
|
||
async function loadSoundList() {
|
||
const res = await fetch('/api/sounds/list');
|
||
if (res.ok) {
|
||
soundFiles = await res.json();
|
||
} else {
|
||
soundFiles = [];
|
||
}
|
||
}
|
||
|
||
async function loadImageList() {
|
||
const res = await fetch('/api/images/list');
|
||
if (res.ok) {
|
||
imageFiles = await res.json();
|
||
} else {
|
||
imageFiles = [];
|
||
}
|
||
}
|
||
|
||
// Функция отображения полей в зависимости от типа
|
||
function renderFieldsForType(type, data = {}) {
|
||
let html = '';
|
||
switch (type) {
|
||
case 'send_message':
|
||
html = `<label>Текст сообщения:</label><textarea id="action-text" rows="3" style="width:100%;">${escapeHtml(data.text || '')}</textarea>
|
||
<small>Доступны плейсхолдеры: {{.username}}, {{.reward_title}}, {{.tier}}, {{.viewers}} и др.</small>`;
|
||
break;
|
||
case 'play_sound':
|
||
html = `<label>Звуковой файл:</label>
|
||
<select id="action-sound-file" style="width:100%;">
|
||
<option value="">— без звука —</option>
|
||
${soundFiles.map(f => `<option value="data/sounds/${f}" ${data.sound_file === `data/sounds/${f}` ? 'selected' : ''}>${f}</option>`).join('')}
|
||
</select>`;
|
||
break;
|
||
case 'press_hotkey':
|
||
html = `<label>Комбинация клавиш:</label><input type="text" id="action-keys" style="width:100%;" value="${escapeHtml(data.keys || '')}" placeholder="CTRL+ALT+Q">
|
||
<small>Поддерживаемые модификаторы: CTRL, ALT, SHIFT, WIN. Основные клавиши: A-Z, 0-9, F1-F12, SPACE, ENTER и др.</small>`;
|
||
break;
|
||
case 'http_request':
|
||
html = `<label>URL (GET):</label><input type="text" id="action-url" style="width:100%;" value="${escapeHtml(data.url || '')}">`;
|
||
break;
|
||
case 'run_program':
|
||
html = `<label>Полный путь к исполняемому файлу:</label><input type="text" id="action-executable" style="width:100%;" value="${escapeHtml(data.executable || '')}">
|
||
<label>Аргументы (через пробел):</label><input type="text" id="action-args" style="width:100%;" value="${escapeHtml(data.args || '')}">`;
|
||
break;
|
||
case 'send_alert':
|
||
html = `<label>Заголовок:</label><input type="text" id="action-title" style="width:100%;" value="${escapeHtml(data.title || '')}">
|
||
<label>Текст:</label><textarea id="action-alert-text" rows="2" style="width:100%;">${escapeHtml(data.alert_text || '')}</textarea>
|
||
<label>Изображение:</label>
|
||
<select id="action-image" style="width:100%;">
|
||
<option value="">— без изображения —</option>
|
||
${imageFiles.map(f => `<option value="/static/${f}" ${data.image === `/static/${f}` ? 'selected' : ''}>${f}</option>`).join('')}
|
||
</select>
|
||
<label>Звук:</label>
|
||
<select id="action-sound" style="width:100%;">
|
||
<option value="">— без звука —</option>
|
||
${soundFiles.map(f => `<option value="/sounds/${f}" ${data.sound_file === `/sounds/${f}` ? 'selected' : ''}>${f}</option>`).join('')}
|
||
</select>
|
||
<label>Длительность (сек):</label><input type="number" id="action-duration" value="${data.duration || 5}" min="1" max="30">
|
||
<label>Целевой веб-сервис (ID):</label><input type="number" id="action-target-id" value="${data.target_web_service_id || 0}" min="0">
|
||
<small>0 = все alert-сервисы. Узнать ID можно на странице "Веб-сервисы".</small>`;
|
||
break;
|
||
default:
|
||
html = '<p>Неизвестный тип действия</p>';
|
||
}
|
||
fieldsContainer.innerHTML = html;
|
||
}
|
||
|
||
// Открыть модалку для создания/редактирования
|
||
async function openActionModal(index) {
|
||
editingIndex = index;
|
||
actionIndexInput.value = index;
|
||
let data = {};
|
||
if (index >= 0 && index < actions.length) {
|
||
data = actions[index];
|
||
actionTypeSelect.value = data.type;
|
||
} else {
|
||
actionTypeSelect.value = 'send_message';
|
||
}
|
||
// Загружаем списки, если ещё не загружены
|
||
if (soundFiles.length === 0) await loadSoundList();
|
||
if (imageFiles.length === 0) await loadImageList();
|
||
renderFieldsForType(actionTypeSelect.value, data);
|
||
actionModal.style.display = 'flex';
|
||
document.getElementById('modal-title').innerText = index >= 0 ? 'Редактировать действие' : 'Новое действие';
|
||
}
|
||
|
||
// Собрать данные из формы
|
||
function collectActionData() {
|
||
const type = actionTypeSelect.value;
|
||
const base = { type: type };
|
||
switch (type) {
|
||
case 'send_message':
|
||
base.text = document.getElementById('action-text')?.value || '';
|
||
break;
|
||
case 'play_sound':
|
||
base.sound_file = document.getElementById('action-sound-file')?.value || '';
|
||
break;
|
||
case 'press_hotkey':
|
||
base.keys = document.getElementById('action-keys')?.value || '';
|
||
break;
|
||
case 'http_request':
|
||
base.url = document.getElementById('action-url')?.value || '';
|
||
break;
|
||
case 'run_program':
|
||
base.executable = document.getElementById('action-executable')?.value || '';
|
||
base.args = document.getElementById('action-args')?.value || '';
|
||
break;
|
||
case 'send_alert':
|
||
base.title = document.getElementById('action-title')?.value || '';
|
||
base.alert_text = document.getElementById('action-alert-text')?.value || '';
|
||
base.image = document.getElementById('action-image')?.value || '';
|
||
base.sound_file = document.getElementById('action-sound')?.value || '';
|
||
base.duration = parseInt(document.getElementById('action-duration')?.value) || 5;
|
||
base.target_web_service_id = parseInt(document.getElementById('action-target-id')?.value) || 0;
|
||
break;
|
||
}
|
||
return base;
|
||
}
|
||
|
||
// Отобразить список действий
|
||
function renderActions() {
|
||
actionsDiv.innerHTML = '';
|
||
if (actions.length === 0) {
|
||
actionsDiv.innerHTML = '<p>Нет действий. Нажмите "Добавить действие".</p>';
|
||
return;
|
||
}
|
||
actions.forEach((act, idx) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'action-item';
|
||
let typeName = '';
|
||
switch (act.type) {
|
||
case 'send_message': typeName = '📨 Отправить сообщение'; break;
|
||
case 'play_sound': typeName = '🔊 Воспроизвести звук'; break;
|
||
case 'press_hotkey': typeName = '⌨️ Нажать клавиши'; break;
|
||
case 'http_request': typeName = '🌐 HTTP запрос'; break;
|
||
case 'run_program': typeName = '⚙️ Запустить программу'; break;
|
||
case 'send_alert': typeName = '🔔 Уведомление (alert)'; break;
|
||
default: typeName = act.type;
|
||
}
|
||
let summary = '';
|
||
if (act.type === 'send_message') summary = act.text;
|
||
else if (act.type === 'play_sound') summary = act.sound_file;
|
||
else if (act.type === 'press_hotkey') summary = act.keys;
|
||
else if (act.type === 'http_request') summary = act.url;
|
||
else if (act.type === 'run_program') summary = act.executable + (act.args ? ' ' + act.args : '');
|
||
else if (act.type === 'send_alert') summary = act.title + ' / ' + act.alert_text;
|
||
div.innerHTML = `
|
||
<strong>${typeName}</strong><br>
|
||
<small>${escapeHtml(summary)}</small><br>
|
||
<button class="edit-action" data-idx="${idx}">✏️ Редактировать</button>
|
||
<button class="remove-action" data-idx="${idx}">🗑️ Удалить</button>
|
||
`;
|
||
actionsDiv.appendChild(div);
|
||
});
|
||
document.querySelectorAll('.edit-action').forEach(btn => {
|
||
btn.addEventListener('click', (e) => openActionModal(parseInt(btn.dataset.idx)));
|
||
});
|
||
document.querySelectorAll('.remove-action').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const idx = parseInt(btn.dataset.idx);
|
||
actions.splice(idx, 1);
|
||
renderActions();
|
||
});
|
||
});
|
||
}
|
||
|
||
// Загрузка действий с сервера
|
||
async function loadEventActions() {
|
||
currentEvent = eventSelect.value;
|
||
const res = await fetch(`/api/events?platform=${currentPlatform}&event=${currentEvent}`);
|
||
if (res.ok) {
|
||
actions = await res.json();
|
||
if (!Array.isArray(actions)) actions = [];
|
||
renderActions();
|
||
editorDiv.style.display = 'block';
|
||
} else {
|
||
alert('Ошибка загрузки действий');
|
||
}
|
||
}
|
||
|
||
// Сохранение действий
|
||
async function saveEventActions() {
|
||
const res = await fetch(`/api/events?platform=${currentPlatform}&event=${currentEvent}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(actions)
|
||
});
|
||
if (res.ok) {
|
||
alert('Действия сохранены!');
|
||
} else {
|
||
alert('Ошибка сохранения');
|
||
}
|
||
}
|
||
|
||
// Обработчики UI
|
||
loadBtn.addEventListener('click', loadEventActions);
|
||
addBtn.addEventListener('click', () => openActionModal(-1));
|
||
saveBtn.addEventListener('click', saveEventActions);
|
||
|
||
actionForm.addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const newAction = collectActionData();
|
||
if (editingIndex >= 0 && editingIndex < actions.length) {
|
||
actions[editingIndex] = newAction;
|
||
} else {
|
||
actions.push(newAction);
|
||
}
|
||
renderActions();
|
||
actionModal.style.display = 'none';
|
||
});
|
||
|
||
actionTypeSelect.addEventListener('change', () => {
|
||
const data = editingIndex >= 0 ? actions[editingIndex] : {};
|
||
renderFieldsForType(actionTypeSelect.value, data);
|
||
});
|
||
|
||
closeModalBtn.addEventListener('click', () => actionModal.style.display = 'none');
|
||
window.addEventListener('click', (e) => { if (e.target === actionModal) actionModal.style.display = 'none'; });
|
||
|
||
function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); }
|
||
</script>
|
||
{{end}} |