<!doctype html>
<html lang="es" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meguilat Esther - Lector para Streaming</title>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script src="/_sdk/element_sdk.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Frank+Ruhl+Libre:wght@400;700&family=Secular+One&display=swap" rel="stylesheet">
<style>
@keyframes sparkle {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 1; transform: scale(1.2); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.sparkle { animation: sparkle 2s ease-in-out infinite; }
.float { animation: float 3s ease-in-out infinite; }
.haman-mark {
background: linear-gradient(135deg, #dc2626, #991b1b);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
}
.esther-mark {
background: linear-gradient(135deg, #7c3aed, #4c1d95);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
}
.mordechai-mark {
background: linear-gradient(135deg, #0891b2, #0e7490);
color: white;
padding: 2px 8px;
border-radius: 4px;
font-weight: bold;
}
.verse-item:hover { transform: translateX(5px); }
.verse-item.active {
background: linear-gradient(90deg, rgba(251,191,36,0.3), transparent);
border-left: 4px solid #f59e0b;
}
.purim-bg {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e1b4b 100%);
}
.gold-text {
background: linear-gradient(135deg, #fbbf24, #f59e0b, #fbbf24);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.scroll-decoration {
background: linear-gradient(to bottom, #92400e, #78350f, #92400e);
border-radius: 8px;
}
</style>
<style>body { box-sizing: border-box; }</style>
<script src="/_sdk/data_sdk.js" type="text/javascript"></script>
</head>
<body class="h-full bg-gray-900 text-white overflow-auto">
<div id="app-wrapper" class="h-full w-full">
<!-- Vista de Control (para el operador) -->
<div id="control-view" class="h-full flex flex-col">
<!-- Header -->
<header class="bg-gradient-to-r from-purple-900 via-indigo-900 to-purple-900 p-4 border-b-2 border-amber-500">
<div class="flex items-center justify-between max-w-7xl mx-auto">
<div class="flex items-center gap-3">
<span class="text-4xl float">📜</span>
<div>
<h1 class="text-2xl font-bold gold-text" style="font-family: 'Secular One', sans-serif;">מגילת אסתר</h1>
<p class="text-amber-200 text-sm">Meguilat Esther - Panel de Control</p>
</div>
</div>
<div class="flex items-center gap-4">
<button id="toggle-view-btn" onclick="togglePresentationMode()" class="bg-amber-600 hover:bg-amber-500 px-4 py-2 rounded-lg font-bold transition-all flex items-center gap-2"> <span>🖥️</span> Ver Pantalla OBS </button>
<div class="flex gap-2 text-2xl">
<span class="sparkle" style="animation-delay: 0s;">✡️</span> <span class="sparkle" style="animation-delay: 0.5s;">👑</span> <span class="sparkle" style="animation-delay: 1s;">🎭</span>
</div>
</div>
</div>
</header><!-- Leyenda de Marcas -->
<div class="bg-gray-800 p-3 border-b border-gray-700">
<div class="max-w-7xl mx-auto flex flex-wrap items-center justify-center gap-4 text-sm">
<span class="text-gray-400">Marcas especiales:</span> <span class="haman-mark">🔊 Hamán (hacer ruido)</span> <span class="esther-mark">👑 Esther (la reina)</span> <span class="mordechai-mark">✡️ Mordejai (el justo)</span>
</div>
</div><!-- Contenido Principal -->
<div class="flex-1 flex overflow-hidden">
<!-- Panel de Selección de Versículos -->
<div class="w-1/2 border-r border-gray-700 flex flex-col">
<div class="bg-gray-800 p-3 border-b border-gray-700">
<h2 class="font-bold text-amber-400 flex items-center gap-2"><span>📖</span> Seleccionar Versículo</h2>
<p class="text-xs text-gray-400 mt-1">Haz clic en un versículo para mostrarlo en pantalla</p>
</div><!-- Selector de Capítulo -->
<div class="bg-gray-800 p-3 border-b border-gray-700">
<label class="text-sm text-gray-400 block mb-2">Capítulo (Perek):</label> <select id="chapter-select" onchange="loadChapter()" class="w-full bg-gray-700 border border-gray-600 rounded-lg p-2 text-white"> <option value="1">Capítulo 1</option> <option value="2">Capítulo 2</option> <option value="3">Capítulo 3</option> <option value="4">Capítulo 4</option> <option value="5">Capítulo 5</option> <option value="6">Capítulo 6</option> <option value="7">Capítulo 7</option> <option value="8">Capítulo 8</option> <option value="9">Capítulo 9</option> <option value="10">Capítulo 10</option> </select>
</div><!-- Lista de Versículos -->
<div id="verse-list" class="flex-1 overflow-y-auto p-3 space-y-2">
<div class="text-center text-gray-500 py-8">
<span class="text-4xl mb-4 block">📜</span>
<p>Sube el texto de la Meguilá para comenzar</p>
<p class="text-sm mt-2">Usa el panel de la derecha para cargar el contenido</p>
</div>
</div>
</div><!-- Panel de Preview y Carga -->
<div class="w-1/2 flex flex-col">
<!-- Preview actual -->
<div class="bg-gray-800 p-3 border-b border-gray-700">
<h2 class="font-bold text-amber-400 flex items-center gap-2"><span>👁️</span> Vista Previa (lo que se ve en OBS)</h2>
</div>
<div class="flex-1 p-4 overflow-y-auto">
<!-- Mini Preview -->
<div id="mini-preview" class="purim-bg rounded-xl p-6 mb-6 border-2 border-amber-500/30">
<div class="text-center">
<p class="text-amber-300 text-sm mb-2">Capítulo 1 • Versículo 1</p>
<div class="scroll-decoration p-4 mb-4">
<p id="preview-phonetic" class="text-xl text-amber-100 mb-3" style="font-family: 'Frank Ruhl Libre', serif;">[Fonética aparecerá aquí]</p>
</div>
<p id="preview-spanish" class="text-lg text-white" style="font-family: 'Secular One', sans-serif;">[Traducción aparecerá aquí]</p>
</div>
</div><!-- Panel de Carga de Texto -->
<div class="bg-gray-800 rounded-xl p-4 border border-gray-700">
<h3 class="font-bold text-amber-400 mb-3 flex items-center gap-2"><span>📝</span> Cargar Texto de la Meguilá</h3>
<p class="text-sm text-gray-400 mb-3">Carga un archivo CSV o pega el texto:</p>
<div class="bg-gray-900 p-3 rounded-lg mb-3 text-xs text-gray-400 font-mono">
<p><strong>Formato CSV:</strong></p>
<p class="text-amber-300 mt-1">capitulo,versiculo,fonetica,español</p>
<p class="text-gray-500 mt-1">1,1,Vayehí bimé Ajashverosh...,Y satisfacer en los días de Ajashverosh...</p>
</div><!-- File Upload -->
<div class="mb-3"><input type="file" id="csv-file" accept=".csv" class="w-full bg-gray-900 border border-gray-600 rounded-lg p-2 text-white text-sm cursor-pointer">
<p class="text-xs text-gray-500 mt-1">Soporta archivos .csv</p>
</div><!-- Textarea para texto manual --> <textarea id="text-input" class="w-full h-24 bg-gray-900 border border-gray-600 rounded-lg p-3 text-sm text-white font-mono" placeholder="O pega aquí el texto en formato CSV o texto simple..."></textarea>
<div class="flex gap-2 mt-3">
<button id="load-btn" onclick="loadTextFromInput()" class="flex-1 bg-amber-600 hover:bg-amber-500 px-4 py-2 rounded-lg font-bold transition-all"> 📥 Cargar Texto </button> <button onclick="loadSampleText()" class="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg transition-all"> 📋 Cargar Ejemplo </button>
</div>
</div><!-- Instrucciones para OBS -->
<div class="mt-4 bg-indigo-900/50 rounded-xl p-4 border border-indigo-700">
<h3 class="font-bold text-indigo-300 mb-2 flex items-center gap-2"><span>🎬</span> Configuración para OBS</h3>
<ol class="text-sm text-gray-300 space-y-1 list-decimal list-inside">
<li>Haz clic en "Ver Pantalla OBS"</li>
<li>En OBS, añade una fuente "Navegador"</li>
<li>Usa esta misma URL</li>
<li>Configura el tamaño a 1920x540 (mitad inferior)</li>
<li>Posiciona en la parte inferior de tu escena</li>
</ol>
</div>
</div>
</div>
</div>
</div><!-- Vista de Presentación (para OBS) -->
<div id="presentation-view" class="h-full w-full purim-bg hidden">
<!-- Decoraciones de fondo -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute top-4 left-4 text-4xl float sparkle">
✡️
</div>
<div class="absolute top-4 right-4 text-4xl float sparkle" style="animation-delay: 0.5s;">
👑
</div>
<div class="absolute bottom-4 left-4 text-4xl float sparkle" style="animation-delay: 1s;">
🎭
</div>
<div class="absolute bottom-4 right-4 text-4xl float sparkle" style="animation-delay: 1.5s;">
📜
</div>
<div class="absolute top-1/2 left-8 text-2xl sparkle" style="animation-delay: 0.3s;">
⭐
</div>
<div class="absolute top-1/2 right-8 text-2xl sparkle" style="animation-delay: 0.8s;">
⭐
</div>
</div><!-- Contenido Principal de Presentación -->
<div class="h-full flex flex-col items-center justify-center p-8 relative z-10">
<!-- Header con referencia -->
<div class="text-center mb-6">
<h1 id="obs-title" class="text-3xl font-bold gold-text mb-2" style="font-family: 'Secular One', sans-serif;">מגילת אסתר</h1>
<p id="obs-reference" class="text-amber-300 text-lg">Capítulo 1 • Versículo 1</p>
</div><!-- Caja de Texto Principal -->
<div class="max-w-5xl w-full">
<!-- Fonética -->
<div class="scroll-decoration p-6 mb-6">
<p id="obs-phonetic" class="text-3xl text-amber-100 text-center leading-relaxed" style="font-family: 'Frank Ruhl Libre', serif;">Selecciona un versículo para comenzar la lectura</p>
</div><!-- Traducción -->
<div class="bg-white/10 backdrop-blur rounded-xl p-6 border border-amber-500/30">
<p id="obs-spanish" class="text-2xl text-white text-center leading-relaxed" style="font-family: 'Secular One', sans-serif;">La traducción aparecerá aquí</p>
</div>
</div><!-- Botón para volver (solo visible al usuario, no en OBS) --> <button onclick="togglePresentationMode()" class="absolute top-4 right-4 bg-gray-800/80 hover:bg-gray-700 px-4 py-2 rounded-lg text-sm transition-all"> ← Volver al Control </button>
</div>
</div>
</div>
<script>
// Configuración y estado
const defaultConfig = {
title_text: 'מגילת אסתר',
current_chapter: 1,
current_verse: 0
};
let config = { ...defaultConfig };
let meguilaData = {};
let isPresentationMode = false;
// Datos de ejemplo (texto de muestra)
const sampleData = {
1: [
{ verse: 1, phonetic: "Vayehí bimé Ajashverósh, hu Ajashverósh hamolékh meHódu ve'ád Kush, shéva ve'esrím umeá mediná.", spanish: "Y satisfacer en los días de Ajashverosh, él es Ajashverosh que reinaba desde la India hasta Etiopía, ciento veintisiete provincias." },
{ verse: 2, phonetic: "Bayamím hahém, keshévet hamélej Ajashverósh al kisé maljutó, ashér beShushan habirá.", spanish: "En aquellos días, cuando el rey Ajashverosh se sentó en el trono de su reino, que estaba en Shushan la capital." },
{ verse: 3, phonetic: "Bishnát shalósh lemoljó, asá mishté lejól saráv vaavadáv, jéil Parás uMadái, hapartemím vesaréi hamedinót lefanáv.", spanish: "En el tercer año de su reinado, hizo un banquete para todos sus príncipes y siervos, el ejército de Persia y Media, los nobles y príncipes de las provincias delante de él." }
]
};
// Función para marcar nombres especiales
function markSpecialNames(text, isHebrew = false) {
let marked = text;
// Variantes de Hamán
const hamanVariants = ['Hamán', 'Haman', 'המן'];
hamanVariants.forEach(name => {
const regex = new RegExp(`(${name})`, 'gi');
marked = marked.replace(regex, '<span class="haman-mark">🔊 $1</span>');
});
// Variantes de Esther
const estherVariants = ['Esther', 'Ester', 'אסתר'];
estherVariants.forEach(name => {
const regex = new RegExp(`(${name})`, 'gi');
marked = marked.replace(regex, '<span class="esther-mark">👑 $1</span>');
});
// Variantes de Mordejai
const mordechaiVariants = ['Mordejai', 'Mordecai', 'Mordejái', 'מרדכי'];
mordechaiVariants.forEach(name => {
const regex = new RegExp(`(${name})`, 'gi');
marked = marked.replace(regex, '<span class="mordechai-mark">✡️ $1</span>');
});
return marked;
}
// Cargar capítulo
function loadChapter() {
const chapter = parseInt(document.getElementById('chapter-select').value);
config.current_chapter = chapter;
renderVerseList();
}
// Renderizar lista de versículos
function renderVerseList() {
const container = document.getElementById('verse-list');
const chapter = config.current_chapter;
const verses = meguilaData[chapter] || [];
if (verses.length === 0) {
container.innerHTML = `
<div class="text-center text-gray-500 py-8">
<span class="text-4xl mb-4 block">📜</span>
<p>No hay versículos cargados para el Capítulo ${chapter}</p>
<p class="text-sm mt-2">Carga el texto usando el panel de la derecha</p>
</div>
`;
return;
}
container.innerHTML = verses.map((v, index) => `
<div class="verse-item p-3 bg-gray-800 rounded-lg cursor-pointer transition-all hover:bg-gray-700 ${config.current_verse === index ? 'active' : ''}" onclick="selectVerse(${index})">
<div class="flex items-start gap-3">
<span class="bg-amber-600 text-white text-xs font-bold px-2 py-1 rounded">${chapter}:${v.verse}</span>
<div class="flex-1 min-w-0">
<p class="text-amber-200 text-sm truncate" style="font-family: 'Frank Ruhl Libre', serif;">
${v.phonetic.substring(0, 60)}...
</p>
<p class="text-gray-400 text-xs mt-1 truncate">
${v.spanish.substring(0, 60)}...
</p>
</div>
</div>
</div>
`).join('');
}
// Seleccionar versículo
function selectVerse(index) {
config.current_verse = index;
const chapter = config.current_chapter;
const verse = meguilaData[chapter]?.[index];
if (!verse) return;
// Actualizar preview
document.getElementById('preview-phonetic').innerHTML = markSpecialNames(verse.phonetic);
document.getElementById('preview-spanish').innerHTML = markSpecialNames(verse.spanish);
// Actualizar vista OBS
document.getElementById('obs-reference').textContent = `Capítulo ${chapter} • Versículo ${verse.verse}`;
document.getElementById('obs-phonetic').innerHTML = markSpecialNames(verse.phonetic);
document.getElementById('obs-spanish').innerHTML = markSpecialNames(verse.spanish);
// Actualizar lista
renderVerseList();
}
// Cargar texto desde input
function loadTextFromInput() {
const input = document.getElementById('text-input').value.trim();
if (!input) {
showErrorMessage('El campo de texto está vacío');
return;
}
parseTextData(input);
}
// Parsear datos en formato CSV o simple
function parseTextData(input) {
try {
// Parsear como CSV o formato simple
const lines = input.split('\n').filter(l => l.trim() && !l.startsWith('#'));
const newMeguilaData = {};
let loadedCount = 0;
lines.forEach((line, lineIndex) => {
line = line.trim();
if (!line) return;
// Saltar encabezados
if (lineIndex === 0 && (line.toLowerCase().includes('capitulo') || line.toLowerCase().includes('chapter'))) {
return;
}
// Detectar si es CSV (contiene comas) o formato simple (contiene |)
let parts;
if (line.includes(',') && !line.includes('|')) {
// Parsear CSV - manejo mejorado
parts = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
parts.push(current.trim().replace(/^"|"$/g, ''));
current = '';
} else {
current += char;
}
}
parts.push(current.trim().replace(/^"|"$/g, ''));
} else if (line.includes('|')) {
// Formato simple con |
parts = line.split('|').map(p => p.trim());
} else {
return; // Saltar líneas sin separador
}
if (parts.length >= 3) {
const chapter = parseInt(parts[0]) || 1;
const verse = parseInt(parts[1]) || 1;
const phonetic = parts[2].trim();
const spanish = parts[3]?.trim() || parts[2].trim(); // Si no hay español, usar fonética
if (!newMeguilaData[chapter]) {
newMeguilaData[chapter] = [];
}
newMeguilaData[chapter].push({
verse: verse,
phonetic: phonetic,
spanish: spanish
});
loadedCount++;
}
});
if (loadedCount > 0) {
meguilaData = newMeguilaData;
config.current_verse = 0;
// Actualizar selector de capítulos si hay nuevos capítulos
const chapters = Object.keys(newMeguilaData).map(Number).sort((a, b) => a - b);
if (chapters.length > 0) {
config.current_chapter = chapters[0];
document.getElementById('chapter-select').value = chapters[0];
}
renderVerseList();
// Seleccionar primer versículo
if (meguilaData[config.current_chapter]?.length > 0) {
selectVerse(0);
}
showSuccessMessage(`✅ ${loadedCount} versículos cargados`);
} else {
showErrorMessage('No se encontraron versículos válidos');
}
} catch (error) {
showErrorMessage('Error al cargar el texto: ' + error.message);
console.error(error);
}
}
// Manejar carga de archivo CSV
document.addEventListener('DOMContentLoaded', () => {
const fileInput = document.getElementById('csv-file');
if (fileInput) {
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const csv = event.target.result;
document.getElementById('text-input').value = csv;
parseTextData(csv);
// Mostrar nombre del archivo cargado
showSuccessMessage(`✅ Archivo "${file.name}" cargado correctamente`);
} catch (error) {
showErrorMessage('Error al leer el archivo: ' + error.message);
}
};
reader.readAsText(file);
});
}
});
// Mostrar mensaje de éxito
function showSuccessMessage(customMsg = null) {
const btn = document.getElementById('load-btn');
const originalText = btn.innerHTML;
btn.innerHTML = customMsg || '✅ ¡Texto Cargado!';
btn.classList.remove('bg-amber-600', 'hover:bg-amber-500');
btn.classList.add('bg-green-600', 'hover:bg-green-500');
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('bg-green-600', 'hover:bg-green-500');
btn.classList.add('bg-amber-600', 'hover:bg-amber-500');
}, 2000);
}
// Mostrar mensaje de error
function showErrorMessage(msg) {
const btn = document.getElementById('load-btn');
const originalText = btn.innerHTML;
btn.innerHTML = '❌ Error';
btn.classList.remove('bg-amber-600', 'hover:bg-amber-500');
btn.classList.add('bg-red-600', 'hover:bg-red-500');
console.error(msg);
setTimeout(() => {
btn.innerHTML = originalText;
btn.classList.remove('bg-red-600', 'hover:bg-red-500');
btn.classList.add('bg-amber-600', 'hover:bg-amber-500');
}, 2000);
}
// Cargar texto de ejemplo
function loadSampleText() {
meguilaData = { ...sampleData };
document.getElementById('chapter-select').value = '1';
config.current_chapter = 1;
renderVerseList();
document.getElementById('text-input').value = '// Texto de ejemplo cargado\n// Capítulo 1 con 3 versículos de muestra';
}
// Alternar modo presentación
function togglePresentationMode() {
isPresentationMode = !isPresentationMode;
document.getElementById('control-view').classList.toggle('hidden', isPresentationMode);
document.getElementById('presentation-view').classList.toggle('hidden', !isPresentationMode);
}
// Atajos de teclado
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
e.preventDefault();
const maxVerse = (meguilaData[config.current_chapter]?.length || 1) - 1;
if (config.current_verse < maxVerse) {
selectVerse(config.current_verse + 1);
}
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
e.preventDefault();
if (config.current_verse > 0) {
selectVerse(config.current_verse - 1);
}
} else if (e.key === 'p' || e.key === 'P') {
togglePresentationMode();
}
});
// Inicialización del SDK
if (window.elementSdk) {
window.elementSdk.init({
defaultConfig,
onConfigChange: async (newConfig) => {
config = { ...config, ...newConfig };
document.getElementById('obs-title').textContent = config.title_text || defaultConfig.title_text;
},
mapToCapabilities: (cfg) => ({
recolorables: [],
borderables: [],
fontEditable: undefined,
fontSizeable: undefined
}),
mapToEditPanelValues: (cfg) => new Map([
['title_text', cfg.title_text || defaultConfig.title_text]
])
});
}
// Cargar datos de ejemplo al inicio
loadSampleText();
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9d63fee9444c5266',t:'MTc3MjQ5MjE5Ny4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>