kdsobsvar / index.html
bakhtrv's picture
Update index.html
ce681e1 verified
Raw
History Blame Contribute Delete
13.4 kB
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FIDA Match System</title>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@900&display=swap" rel="stylesheet">
<style>
/* БАЗОВЫЕ СТИЛИ */
body { margin: 0; padding: 0; font-family: sans-serif; }
/* =========================================
СТИЛИ РЕЖИМА "ВИДЖЕТ" (OBS)
========================================= */
body.mode-view {
background-color: transparent; /* Прозрачный фон для OBS */
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* Контейнер виджета */
#display-value {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Текст */
.obs-widget-text {
font-family: 'Orbitron', sans-serif;
color: #fff;
font-size: 50vh;
font-weight: 900;
line-height: 1;
text-align: center;
white-space: pre-line;
text-shadow: 0px 0px 10px rgba(0,0,0,0.8); /* Тень для читаемости */
}
/* Размеры текста */
.obs-size-huge { font-size: 50vh; }
.obs-size-large { font-size: 35vh; }
.obs-size-medium { font-size: 20vh; }
.obs-size-small { font-size: 10vh; }
/* Картинка */
.obs-image-container {
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.obs-image-full {
max-width: 100%;
max-height: 100vh;
object-fit: contain;
display: block;
/* Анимация появления для плавности */
animation: fadeIn 0.3s ease-out forwards;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.98); }
to { opacity: 1; transform: scale(1); }
}
/* =========================================
СТИЛИ РЕЖИМА "РЕДАКТОР"
========================================= */
body.mode-editor {
background-color: #121212;
color: #e0e0e0;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.editor-container { max-width: 1200px; margin: 0 auto; }
h1 { color: #bb86fc; border-bottom: 1px solid #333; padding-bottom: 10px; }
.node { margin-left: 20px; border-left: 1px solid #333; padding-left: 10px; }
.node-row { display: flex; align-items: center; margin: 5px 0; background: #1e1e1e; padding: 8px; border-radius: 4px; gap: 10px; }
.key-label { color: #03dac6; font-weight: bold; min-width: 150px; }
.inp-val { background: #2c2c2c; border: 1px solid #444; color: white; padding: 5px; flex-grow: 1; border-radius: 4px; }
.inp-val:focus { outline: none; border-color: #bb86fc; }
.btn-obs { text-decoration: none; font-size: 1.2rem; cursor: pointer; filter: grayscale(100%); transition: 0.2s; }
.btn-obs:hover { filter: grayscale(0%); transform: scale(1.2); }
.status-toast { position: fixed; bottom: 20px; right: 20px; background: #03dac6; color: black; padding: 10px 20px; border-radius: 20px; display: none; font-weight: bold; }
#view-wrapper { display: none; }
#editor-wrapper { display: none; }
</style>
</head>
<body>
<!-- РЕДАКТОР -->
<div id="editor-wrapper" class="editor-container">
<h1>FIDA Control Panel</h1>
<div style="margin-bottom: 15px; color: #888;">
<p>Редактируйте значения. Нажмите 🔗 для открытия окна OBS.</p>
</div>
<div id="editor-root">Loading...</div>
</div>
<div id="toast" class="status-toast">Saved</div>
<!-- ВИДЖЕТ -->
<div id="view-wrapper">
<div id="display-value">...</div>
</div>
<!-- Script -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-app.js";
import { getDatabase, ref, onValue, set } from "https://www.gstatic.com/firebasejs/9.22.0/firebase-database.js";
const firebaseConfig = {
apiKey: "AIzaSyDi6znbXgOFR7aXZ9v7aVFP065yE98T7XU",
authDomain: "fida-kz.firebaseapp.com",
databaseURL: "https://fida-kz-default-rtdb.firebaseio.com",
projectId: "fida-kz",
storageBucket: "fida-kz.firebasestorage.app",
messagingSenderId: "728249202318",
appId: "1:728249202318:web:29ee414ef7eff47573fbdf",
measurementId: "G-226S5831R5"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
const params = new URLSearchParams(window.location.search);
const mode = params.get('mode');
const path = params.get('path') || 'active_match';
if (mode === 'view') {
initViewerMode(path);
} else {
initEditorMode(path);
}
// ======================= VIEW MODE (OBS) =======================
function initViewerMode(dbPath) {
document.body.classList.add('mode-view');
document.getElementById('view-wrapper').style.display = 'flex';
const displayEl = document.getElementById('display-value');
const dataRef = ref(db, dbPath);
// Кеш для предотвращения повторной отрисовки того же самого
let lastRenderedVal = null;
onValue(dataRef, (snapshot) => {
const val = snapshot.val();
// Если значение не изменилось, ничего не делаем (оптимизация)
if (JSON.stringify(val) === JSON.stringify(lastRenderedVal)) return;
lastRenderedVal = val;
// Проверка: это картинка?
const isUrl = typeof val === 'string' && (val.startsWith('http') || val.startsWith('data:image'));
const isImage = isUrl && (
val.match(/\.(jpeg|jpg|gif|png|webp|bmp|svg)/i) !== null ||
val.includes('cloudinary') ||
val.includes('firebasestorage')
);
if (isImage) {
let finalUrl = val;
// WebP оптимизация для Cloudinary
if (finalUrl.includes('res.cloudinary.com') && finalUrl.includes('/upload/')) {
if (!finalUrl.includes('/f_')) {
// q_auto:best сохраняет хорошее качество при малом весе
finalUrl = finalUrl.replace('/upload/', '/upload/f_webp,q_auto:best/');
}
}
// --- ПРЕДЗАГРУЗКА (PRELOADING) ---
// 1. Создаем объект изображения в памяти
const imgPreloader = new Image();
imgPreloader.onload = () => {
// 2. Как только загрузилось - вставляем в DOM
displayEl.innerHTML = `
<div class="obs-image-container">
<img src="${finalUrl}" class="obs-image-full" alt="OBS">
</div>
`;
};
// Начинаем загрузку
imgPreloader.src = finalUrl;
} else {
// Рендер ТЕКСТА (тут задержка не нужна)
let textToShow = "";
if (val === null) textToShow = "";
else if (typeof val === 'object') textToShow = JSON.stringify(val);
else textToShow = String(val);
// Сбрасываем содержимое
displayEl.innerHTML = `<div class="obs-widget-text">${textToShow}</div>`;
const textEl = displayEl.querySelector('.obs-widget-text');
if (textToShow.length <= 2) textEl.classList.add('obs-size-huge');
else if (textToShow.length <= 5) textEl.classList.add('obs-size-large');
else if (textToShow.length <= 15) textEl.classList.add('obs-size-medium');
else textEl.classList.add('obs-size-small');
}
});
}
// ======================= EDITOR MODE =======================
function initEditorMode(rootPath) {
document.body.classList.add('mode-editor');
document.getElementById('editor-wrapper').style.display = 'block';
const editorRoot = document.getElementById('editor-root');
const rootRef = ref(db, rootPath);
onValue(rootRef, (snapshot) => {
editorRoot.innerHTML = '';
const data = snapshot.val();
if(!data) { editorRoot.innerHTML = "Нет данных"; return; }
if (typeof data === 'object') renderTree(data, rootPath, editorRoot);
else renderLeaf("Value", data, rootPath, editorRoot);
});
}
function renderTree(obj, currentPath, container) {
const keys = Object.keys(obj).sort((a, b) => {
const isObjA = typeof obj[a] === 'object' && obj[a] !== null;
const isObjB = typeof obj[b] === 'object' && obj[b] !== null;
return isObjA - isObjB;
});
keys.forEach(key => {
const val = obj[key];
const fullPath = `${currentPath}/${key}`;
if (typeof val === 'object' && val !== null) {
const nodeDiv = document.createElement('div');
nodeDiv.className = 'node';
const header = document.createElement('div');
header.className = 'node-row';
header.innerHTML = `
<span class="key-label">${key}</span>
<span style="color:#666; font-size:0.8em;">{ ... }</span>
<a href="?mode=view&path=${fullPath}" target="_blank" class="btn-obs" title="Открыть виджет">🔗</a>
`;
const childrenDiv = document.createElement('div');
renderTree(val, fullPath, childrenDiv);
nodeDiv.appendChild(header);
nodeDiv.appendChild(childrenDiv);
container.appendChild(nodeDiv);
} else {
renderLeaf(key, val, fullPath, container);
}
});
}
function renderLeaf(key, val, fullPath, container) {
const row = document.createElement('div');
row.className = 'node-row node';
let imgTag = '';
// Превью в редакторе (маленькое)
if (typeof val === 'string' && (val.match(/^http/) || val.match(/^data:image/))) {
let previewUrl = val;
if (previewUrl.includes('res.cloudinary.com') && !previewUrl.includes('/f_')) {
previewUrl = previewUrl.replace('/upload/', '/upload/f_webp,h_60/');
}
imgTag = `<a href="${val}" target="_blank"><img src="${previewUrl}" style="height:30px; border-radius:2px; margin-left:5px;"></a>`;
}
row.innerHTML = `
<span class="key-label">${key}</span>
<input class="inp-val" value="${val}" data-path="${fullPath}" data-type="${typeof val}">
${imgTag}
<a href="?mode=view&path=${fullPath}" target="_blank" class="btn-obs" title="Открыть виджет">🔗</a>
`;
const input = row.querySelector('input');
input.addEventListener('change', (e) => {
let newVal = e.target.value;
const type = e.target.dataset.type;
if (type === 'number') newVal = Number(newVal);
if (type === 'boolean') newVal = (newVal === 'true');
set(ref(db, fullPath), newVal).then(() => showToast()).catch(err => alert(err));
});
container.appendChild(row);
}
function showToast() {
const t = document.getElementById('toast');
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 2000);
}
</script>
</body>
</html>