залил

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