| <!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; } |
| |
| |
| |
| |
| body.mode-view { |
| background-color: transparent; |
| 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 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); |
| } |
| |
| |
| 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; |
| |
| if (finalUrl.includes('res.cloudinary.com') && finalUrl.includes('/upload/')) { |
| if (!finalUrl.includes('/f_')) { |
| |
| finalUrl = finalUrl.replace('/upload/', '/upload/f_webp,q_auto:best/'); |
| } |
| } |
| |
| |
| |
| const imgPreloader = new Image(); |
| |
| imgPreloader.onload = () => { |
| |
| 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'); |
| } |
| }); |
| } |
| |
| |
| 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> |