Inicio Red Activa NextCORS
\n `;\n\r\n return html;\r\n}","editor_js":"function(block) {\r\n const contenido = block.contenido || {};\r\n const items = contenido.items || [];\r\n\r\n function esc(value) {\r\n if (value === null || value === undefined) return '';\r\n return String(value).replace(/[&<>\"]/g, function(m) {\r\n return {'&':'&','<':'<','>':'>','\"':'"'}[m];\r\n });\r\n }\r\n\r\n return `\r\n
\r\n \n \r\n
\r\n
\n \n \n
\n
\n \n \n
\n
\r\n \r\n
\r\n
\n \n
\n
\n \n
\n
\n
\r\n \r\n
\r\n ${items.map((item, index) => `\r\n
\r\n
\r\n
\r\n
\n
\n
\n
\n
\n
\r\n \r\n
\r\n \r\n
\r\n `).join('')}\r\n
\r\n
\r\n
\r\n Endpoint publico: elementos/descarga_v/latest.php?app=NOMBRE%20DEL%20PROGRAMA&channel=stable\n
\r\n `;\r\n}","init_js":"function(block, editorContainer, updateBlock) {\r\n function getBaseUrlDescargaV() {\r\n const scripts = document.getElementsByTagName('script');\r\n let builderPath = '';\r\n\r\n for (let s of scripts) {\r\n if (s.src && s.src.includes('builder.php')) {\r\n let url = new URL(s.src);\r\n builderPath = url.pathname.substring(0, url.pathname.lastIndexOf('/') + 1);\r\n break;\r\n }\r\n }\r\n\r\n return builderPath ? builderPath + 'elementos/descarga_v/' : '/dmcc/builder/elementos/descarga_v/';\r\n }\r\n\r\n const baseUrl = getBaseUrlDescargaV();\r\n\r\n window.updateItemDescargaV = function(index, campo, valor) {\r\n const items = block.contenido.items || [];\r\n if (!items[index]) return;\r\n items[index][campo] = valor;\r\n updateBlock('contenido.items', items);\r\n };\r\n\r\n window.eliminarItemDescargaV = function(index) {\r\n const items = block.contenido.items || [];\r\n items.splice(index, 1);\r\n updateBlock('contenido.items', items);\r\n };\r\n\r\n window.refrescarVersionesDescargaV = function() {\n fetch(baseUrl + 'versiones.php')\n .then(r => r.json())\r\n .then(data => {\r\n if (data.error) {\r\n alert(data.error);\r\n return;\r\n }\r\n\r\n const versions = data.versions || [];\n const items = versions.map(v => ({\n file_id: v.file_id || '',\n app: v.app || block.contenido.app || 'DMC Studio',\n version: v.version || '',\n channel: v.channel || 'stable',\n notes: v.notes || '',\n required: !!v.required,\n enlace: v.download_url || '',\r\n download_url: v.download_url || '',\n size: v.size || '',\n released_at: v.released_at || '',\n descargas: v.descargas || v.downloads || 0,\n nombre_original: v.original_name || '',\n extension: v.extension || '',\n sha256: v.sha256 || ''\n }));\r\n\r\n updateBlock('contenido.items', items);\r\n alert('Versiones cargadas: ' + items.length);\r\n })\r\n .catch(err => alert('Error cargando versiones: ' + err.message));\n };\n\n window.verEstadisticasUsoDescargaV = function() {\n fetch(baseUrl + 'estadisticas_uso.php')\n .then(r => r.json())\n .then(data => {\n if (!data.ok) {\n alert(data.error || 'No se pudieron cargar las estadisticas');\n return;\n }\n\n function safeHtml(value) {\n if (value === null || value === undefined) return '';\n return String(value).replace(/[&<>\"]/g, function(m) {\n return {'&':'&','<':'<','>':'>','\"':'"'}[m];\n });\n }\n\n const apps = data.apps || {};\n const rows = Object.keys(apps).sort().map(appName => {\n const item = apps[appName] || {};\n const byVersion = item.by_version || {};\n const byDay = item.by_day || {};\n const byCountry = item.by_country || {};\n const versions = Object.keys(byVersion).sort().map(v => `${v}: ${byVersion[v]}`).join(', ') || 'Sin datos';\n const days = Object.keys(byDay).sort().slice(-7).map(d => `${d}: ${byDay[d]}`).join('
') || 'Sin datos';\n const countries = Object.keys(byCountry).sort().map(c => `${c}: ${byCountry[c]}`).join(', ') || 'Sin datos';\n\n return `\n \n ${safeHtml(appName)}\n ${item.unique_installations || 0}\n ${item.total_events || 0}\n ${safeHtml(countries)}\n ${safeHtml(versions)}\n ${days}\n \n `;\n }).join('');\n\n const installations = data.installations || [];\n const installationRows = installations.map(item => {\n const byVersion = item.by_version || {};\n const versions = Object.keys(byVersion).sort().map(v => `${v}: ${byVersion[v]}`).join(', ') || safeHtml(item.version || '');\n return `\n \n ${safeHtml(item.id || '')}\n ${safeHtml(item.app || '')}\n ${safeHtml(item.country || '--')}\n ${safeHtml(item.platform || '')}\n ${item.events || 0}\n ${safeHtml(versions)}\n ${safeHtml(item.first_seen || '')}\n ${safeHtml(item.last_seen || '')}\n \n `;\n }).join('');\n\n const old = document.getElementById('modalUsoDescargaV');\n if (old) old.remove();\n\n const modal = document.createElement('div');\n modal.id = 'modalUsoDescargaV';\n modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(15,23,42,.75);z-index:100000;display:flex;align-items:center;justify-content:center;';\n modal.innerHTML = `\n
\n
\n
\n

Estadisticas de uso

\n
Instalaciones anonimas y aperturas por programa.
\n
\n \n
\n
\n
\n
Instalaciones unicas
\n
${data.unique_installations || 0}
\n
\n
\n
Eventos totales
\n
${data.total_events || 0}
\n
\n
\n

Resumen por programa

\n \n \n \n \n \n \n \n \n \n \n \n ${rows || ''}\n
ProgramaInstalacionesAperturasPaisesVersionesUltimos dias
Sin datos todavia.
\n

Instalaciones anonimas

\n
El ID es un hash anonimo; no guarda nombre, correo, coordenadas ni IP completa.
\n \n \n \n \n \n \n \n \n \n \n \n \n \n ${installationRows || ''}\n
IDProgramaPaisSistemaAperturasVersionesPrimera vezUltima vez
Sin instalaciones registradas todavia.
\n
\n `;\n document.body.appendChild(modal);\n document.getElementById('cerrarUsoDescargaV').onclick = () => modal.remove();\n modal.onclick = e => { if (e.target === modal) modal.remove(); };\n })\n .catch(err => alert('Error cargando estadisticas: ' + err.message));\n };\n\n window.abrirBibliotecaDescargaV = function() {\n const old = document.getElementById('modalDescargaV');\r\n if (old) old.remove();\r\n\r\n const modal = document.createElement('div');\r\n modal.id = 'modalDescargaV';\r\n modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(15,23,42,.75);z-index:100000;display:flex;align-items:center;justify-content:center;';\r\n modal.innerHTML = `\r\n
\r\n
\r\n
\r\n

Descarga V - control de version

\r\n
Sube el instalador y registralo como version publicada.
\r\n
\r\n \r\n
\r\n\r\n
\r\n \r\n \r\n \r\n \r\n
\r\n\r\n
\r\n
\r\n
\r\n
\r\n
0%
\r\n
\r\n\r\n \r\n
Cargando...
\r\n
\r\n `;\r\n\r\n document.body.appendChild(modal);\r\n\r\n document.getElementById('cerrarDescargaV').onclick = () => modal.remove();\r\n modal.onclick = e => { if (e.target === modal) modal.remove(); };\r\n\r\n function safeJs(value) {\r\n return String(value || '').replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\").replace(/\\n/g, '\\\\n');\r\n }\r\n\r\n function cargarArchivos() {\r\n fetch(baseUrl + 'listar.php')\r\n .then(r => r.json())\r\n .then(files => {\r\n const box = document.getElementById('listaArchivosDescargaV');\r\n if (!box) return;\r\n\r\n if (files.error) {\r\n box.innerHTML = '
' + files.error + '
';\r\n return;\r\n }\r\n\r\n const filtro = String(document.getElementById('filtroDescargaV').value || '').toLowerCase();\r\n const filtrados = files.filter(f => {\r\n return String(f.nombre_original || '').toLowerCase().includes(filtro)\r\n || String(f.name || '').toLowerCase().includes(filtro)\r\n || String(f.extension || '').toLowerCase().includes(filtro);\r\n });\r\n\r\n if (!filtrados.length) {\r\n box.innerHTML = '
No hay archivos.
';\r\n return;\r\n }\r\n\r\n box.innerHTML = filtrados.map(f => `\r\n
\r\n
${f.nombre_original || f.name}
\r\n
ID ${f.id} | ${f.size} | .${f.extension}
\r\n
SHA256: ${f.sha256 || ''}
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n `).join('');\r\n })\r\n .catch(err => {\r\n const box = document.getElementById('listaArchivosDescargaV');\r\n if (box) box.innerHTML = '
Error: ' + err.message + '
';\r\n });\r\n }\r\n\r\n window.agregarArchivoDescargaV = function(id, name, url, size, extension, sha256) {\r\n const version = prompt('Version del programa:', '1.0.0') || '';\r\n if (!version) return;\r\n\r\n const items = block.contenido.items || [];\r\n items.unshift({\r\n app: block.contenido.app || 'DMC Studio',\r\n version: version,\r\n channel: block.contenido.channel || 'stable',\r\n nombre_original: name,\r\n enlace: url,\r\n download_url: url,\r\n size: size,\r\n extension: extension,\r\n sha256: sha256,\r\n notes: '',\r\n required: false,\r\n released_at: new Date().toISOString()\r\n });\r\n updateBlock('contenido.items', items);\r\n };\r\n\r\n window.registrarVersionDescargaV = function(id, name, size, extension, sha256) {\r\n const app = prompt('Programa:', block.contenido.app || 'DMC Studio') || '';\r\n if (!app) return;\r\n\r\n const version = prompt('Version:', '1.0.0') || '';\r\n if (!version) return;\r\n\r\n const channel = prompt('Canal: stable, beta o dev', block.contenido.channel || 'stable') || 'stable';\r\n const requiredText = prompt('Obligatoria? escribe si o no', 'no') || 'no';\r\n const notes = prompt('Notas de la version:', '') || '';\r\n\r\n const body = new URLSearchParams();\r\n body.set('id', id);\r\n body.set('app', app);\r\n body.set('version', version);\r\n body.set('channel', channel);\r\n body.set('title', app + ' ' + version);\r\n body.set('notes', notes);\r\n body.set('required', requiredText.toLowerCase() === 'si' || requiredText.toLowerCase() === 'yes' ? '1' : '0');\r\n body.set('active', '1');\r\n\r\n fetch(baseUrl + 'guardar_version.php', {\r\n method: 'POST',\r\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\r\n body: body.toString()\r\n })\r\n .then(r => r.json())\r\n .then(data => {\r\n if (!data.success && !data.ok) {\r\n alert(data.error || 'No se pudo registrar la version');\r\n return;\r\n }\r\n\r\n const items = block.contenido.items || [];\r\n const v = data.version;\r\n items.unshift({\r\n app: v.app,\r\n version: v.version,\r\n channel: v.channel,\r\n nombre_original: v.original_name,\r\n enlace: v.download_url,\r\n download_url: v.download_url,\r\n size: v.size,\r\n extension: v.extension,\r\n sha256: v.sha256,\r\n notes: v.notes,\r\n required: !!v.required,\r\n released_at: v.released_at\r\n });\r\n updateBlock('contenido.items', items);\r\n alert('Version registrada correctamente.');\r\n })\r\n .catch(err => alert('Error registrando version: ' + err.message));\r\n };\r\n\r\n window.eliminarArchivoDescargaV = function(id, name) {\r\n if (!confirm('Eliminar archivo ' + name + '?')) return;\r\n fetch(baseUrl + 'delete.php', {\r\n method: 'POST',\r\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\r\n body: 'id=' + encodeURIComponent(id)\r\n })\r\n .then(r => r.json())\r\n .then(data => {\r\n if (!data.success && !data.ok) {\r\n alert(data.error || 'No se pudo eliminar');\r\n return;\r\n }\r\n cargarArchivos();\r\n })\r\n .catch(err => alert('Error eliminando: ' + err.message));\r\n };\r\n\r\n document.getElementById('recargarDescargaV').onclick = cargarArchivos;\r\n document.getElementById('filtroDescargaV').oninput = cargarArchivos;\r\n\r\n document.getElementById('subirDescargaV').onclick = function() {\r\n const input = document.getElementById('archivoDescargaV');\r\n const estado = document.getElementById('estadoDescargaV');\r\n const wrap = document.getElementById('progresoDescargaVWrap');\r\n const bar = document.getElementById('progresoDescargaV');\r\n const txt = document.getElementById('progresoDescargaVTexto');\r\n\r\n if (!input.files.length) {\r\n alert('Selecciona un archivo.');\r\n return;\r\n }\r\n\r\n const fd = new FormData();\r\n fd.append('archivo_subir', input.files[0]);\r\n wrap.style.display = 'block';\r\n bar.style.width = '0%';\r\n txt.innerText = '0%';\r\n estado.innerText = 'Subiendo...';\r\n\r\n const xhr = new XMLHttpRequest();\r\n xhr.open('POST', baseUrl + 'subir.php', true);\r\n xhr.upload.onprogress = function(e) {\r\n if (e.lengthComputable) {\r\n const p = Math.round((e.loaded / e.total) * 100);\r\n bar.style.width = p + '%';\r\n txt.innerText = p + '%';\r\n }\r\n };\r\n xhr.onload = function() {\r\n let data = null;\r\n try { data = JSON.parse(xhr.responseText); } catch(e) {}\r\n\r\n if (xhr.status >= 200 && xhr.status < 300 && data && (data.success || data.ok)) {\r\n estado.innerText = 'Archivo subido.';\r\n input.value = '';\r\n cargarArchivos();\r\n } else {\r\n estado.innerText = data && data.error ? data.error : 'Error al subir.';\r\n }\r\n };\r\n xhr.onerror = function() {\r\n estado.innerText = 'Error de conexion.';\r\n };\r\n xhr.send(fd);\r\n };\r\n\r\n cargarArchivos();\r\n };\r\n}"},"espacio":{"nombre":"Espacio H","contenido":[],"estilos":{"width":"100%","height":"50px","background":"transparent","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n return `\n
\n
\n
\n `;\n}"},"espacio_vertical":{"nombre":"Espacio V","contenido":[],"estilos":{"width":"100%","height":"50px","background":"transparent","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n return `\n
\n
\n
\n `;\n}"},"formulario":{"nombre":"Formulario","contenido":{"destinatario":"admin@ejemplo.com","texto_boton":"Enviar"},"estilos":{"width":"50%","background":"#f9f9f9","padding":"20px","btn_bg":"#2c3e66","btn_text":"#fff","btn_hover":"#1e2a44","input_border":"#ccc","label_color":"#000","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n return `\n
\n
\n
\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n `;\n}"},"imagen":{"nombre":"Imagen","contenido":{"src":"","downloadLink":"","linkTarget":"_blank"},"estilos":{"width":"100%","height":"auto","background":"#ffffff","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n const target = block.contenido.linkTarget || '_blank';\n // Escapar comillas\n const srcVal = (block.contenido.src || '').replace(/\"/g, '"');\n const linkVal = (block.contenido.downloadLink || '').replace(/\"/g, '"');\n // Crear un ID único para el input file basado en el id del bloque\n const inputId = 'fileInput_' + block.id.replace(/[^a-zA-Z0-9]/g, '_');\n return `\n
\n
\n
\n
\n \n
\n
\n \n
\n
\n \n \n
\n
\n \n
\n
\n \n
\n `;\n}"},"lista_iconos":{"nombre":"Lista iconos","contenido":{"items":[{"icono":"⭐","texto":"Elemento 1"},{"icono":"✅","texto":"Elemento 2"},{"icono":"🔥","texto":"Elemento 3"}]},"estilos":{"width":"100%","background":"#ffffff","color":"#000000","icon_color":"#007bff","font_size":"16","gap":"8","horizontal_align":"izquierda"},"render_js":"function(block) {\n\n const align = block.estilos.horizontal_align || 'izquierda';\n\n let justify = 'flex-start';\n if (align === 'centro') justify = 'center';\n if (align === 'derecha') justify = 'flex-end';\n\n const items = block.contenido.items || [];\n\n return `\n
\n ${items.map(i => `\n
\n \n ${i.icono}\n \n ${i.texto}\n
\n `).join('')}\n
\n `;\n}","editor_js":"function(block) {\n\n const items = block.contenido.items || [];\n\n return `\n
\n \n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n
\n\n
\n ${items.map((item, i) => `\n
\n \n \n \n
\n `).join('')}\n \n
\n `;\n}"},"mapa":{"nombre":"Mapa","contenido":{"lat":18.432608,"lng":-69.133209,"zoom":12,"marker":true},"estilos":{"width":"100%","height":"400px","background":"#e9ecef","horizontal_align":"izquierda"},"render_js":"function(block, isBuilder) {\n const contenido = block.contenido || {};\n const estilos = block.estilos || {};\n\n const lat = contenido.lat || 18.432608;\n const lng = contenido.lng || -69.133209;\n const zoom = contenido.zoom || 12;\n const marker = contenido.marker !== false;\n const height = estilos.height || '400px';\n const bg = estilos.background || '#e9ecef';\n\n const mapId = 'mapa2_' + String(block.id).replace(/[^a-zA-Z0-9]/g, '_') + '_' + Math.random().toString(36).substr(2, 8);\n\n setTimeout(function() {\n const container = document.getElementById(mapId);\n if (!container) return;\n\n function iniciarMapa2() {\n if (typeof L === 'undefined') {\n setTimeout(iniciarMapa2, 200);\n return;\n }\n\n if (container._mapa2_iniciado) return;\n\n const mapLat = parseFloat(container.getAttribute('data-lat'));\n const mapLng = parseFloat(container.getAttribute('data-lng'));\n const mapZoom = parseInt(container.getAttribute('data-zoom') || '12');\n const mapMarker = container.getAttribute('data-marker') !== 'false';\n\n if (isNaN(mapLat) || isNaN(mapLng)) return;\n\n const map = L.map(mapId).setView([mapLat, mapLng], mapZoom);\n\n L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap'\n }).addTo(map);\n\n if (mapMarker) {\n L.marker([mapLat, mapLng]).addTo(map).bindPopup('Ubicación guardada');\n }\n\n container._mapa2_iniciado = true;\n container._map = map;\n\n setTimeout(function() {\n map.invalidateSize();\n }, 300);\n }\n\n iniciarMapa2();\n }, 300);\n\n return `\n \n `;\n}","editor_js":"function(block) {\n const editorMapId = 'mapa2_editor_' + String(block.id).replace(/[^a-zA-Z0-9]/g, '_');\n\n setTimeout(() => {\n const container = document.getElementById(editorMapId);\n if (!container) return;\n\n function iniciarEditorMapa2() {\n if (typeof L === 'undefined') {\n setTimeout(iniciarEditorMapa2, 200);\n return;\n }\n\n const lat = parseFloat(block.contenido.lat || 18.432608);\n const lng = parseFloat(block.contenido.lng || -69.133209);\n const zoom = parseInt(block.contenido.zoom || 12);\n\n if (container._map) {\n container._map.setView([lat, lng], zoom);\n container._map.invalidateSize();\n\n if (container._marker) {\n container._marker.setLatLng([lat, lng]);\n }\n\n return;\n }\n\n const map = L.map(editorMapId).setView([lat, lng], zoom);\n\n L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {\n attribution: '© OpenStreetMap'\n }).addTo(map);\n\n const marker = L.marker([lat, lng], {\n draggable: true\n }).addTo(map);\n\n marker.on('dragend', function(e) {\n const pos = e.target.getLatLng();\n\n const latInput = document.getElementById('lat_' + block.id);\n const lngInput = document.getElementById('lng_' + block.id);\n\n if (latInput) latInput.value = pos.lat;\n if (lngInput) lngInput.value = pos.lng;\n\n updateSelectedContent('lat', pos.lat);\n updateSelectedContent('lng', pos.lng);\n });\n\n map.on('click', function(e) {\n const lat = e.latlng.lat;\n const lng = e.latlng.lng;\n\n marker.setLatLng([lat, lng]);\n\n const latInput = document.getElementById('lat_' + block.id);\n const lngInput = document.getElementById('lng_' + block.id);\n\n if (latInput) latInput.value = lat;\n if (lngInput) lngInput.value = lng;\n\n updateSelectedContent('lat', lat);\n updateSelectedContent('lng', lng);\n });\n\n container._map = map;\n container._marker = marker;\n\n setTimeout(() => map.invalidateSize(), 300);\n }\n\n iniciarEditorMapa2();\n }, 300);\n\n return `\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n
\n
\n `;\n}"},"pwa_instalar":{"nombre":"PWA Descarga","contenido":{"nombre":"Aplicación Web","descripcion":"Instala esta aplicación en tu dispositivo para acceder más rápido.","tipo":"PWA","tamano":"Instalación ligera","descargas":0,"texto_boton":"Instalar","texto_instalada":"Instalada","texto_no_disponible":"No disponible","texto_activando":"Preparando..."},"estilos":{"width":"100%","background":"#ffffff","color":"#000000","link_color":"#2c3e66","border_radius":"8","font_size":"14","table_border":"1px solid #e0e0e0","header_bg":"#f5f5f5","row_hover":"#f9f9f9","btn_bg":"#2c3e66","btn_color":"#ffffff","btn_hover":"#1e293b","horizontal_align":"centro"},"render_js":"function(block, isBuilder) {\n const contenido = block.contenido || {};\n const estilos = block.estilos || {};\n\n const nombre = contenido.nombre || 'Aplicación Web';\n const descripcion = contenido.descripcion || 'Instala esta aplicación en tu dispositivo para acceder más rápido.';\n const tipo = contenido.tipo || 'PWA';\n const tamano = contenido.tamano || 'Instalación ligera';\n const descargas = contenido.descargas || 0;\n const textoBoton = contenido.texto_boton || 'Instalar';\n const textoInstalada = contenido.texto_instalada || 'Instalada';\n const textoNoDisponible = contenido.texto_no_disponible || 'No disponible';\n const textoActivando = contenido.texto_activando || 'Preparando...';\n\n const width = estilos.width || '100%';\n const bg = estilos.background || '#ffffff';\n const color = estilos.color || '#000000';\n const linkColor = estilos.link_color || '#2c3e66';\n const radius = estilos.border_radius || '8';\n const fontSize = estilos.font_size || '14';\n const border = estilos.table_border || '1px solid #e0e0e0';\n const headerBg = estilos.header_bg || '#f5f5f5';\n const rowHover = estilos.row_hover || '#f9f9f9';\n const btnBg = estilos.btn_bg || '#2c3e66';\n const btnColor = estilos.btn_color || '#ffffff';\n const btnHover = estilos.btn_hover || '#1e293b';\n\n const align = estilos.horizontal_align || 'centro';\n\n let justifyContent = 'center';\n if (align === 'izquierda') justifyContent = 'flex-start';\n if (align === 'derecha') justifyContent = 'flex-end';\n\n const id = 'pwa_descarga_' + String(block.id).replace(/[^a-zA-Z0-9]/g, '_') + '_' + Math.random().toString(36).substr(2, 8);\n const btnId = id + '_btn';\n const countId = id + '_count';\n const statusId = id + '_status';\n\n function escapeHtml(str) {\n if (!str) return '';\n\n return String(str).replace(/[&<>\"]/g, function(m) {\n if (m === '&') return '&';\n if (m === '<') return '<';\n if (m === '>') return '>';\n if (m === '\"') return '"';\n return m;\n });\n }\n\n if (isBuilder) {\n return `\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
AplicaciónDescripciónTipoDescargas
\n \n ${escapeHtml(nombre)}\n ${escapeHtml(descripcion)}${escapeHtml(tipo)} / ${escapeHtml(tamano)}${descargas}\n \n
\n
\n
\n `;\n }\n\n setTimeout(function() {\n const wrapper = document.getElementById(id);\n const btn = document.getElementById(btnId);\n const contador = document.getElementById(countId);\n const status = document.getElementById(statusId);\n\n if (!wrapper || !btn) return;\n\n let deferredPrompt = null;\n let contadorActual = parseInt(contador ? contador.innerText : descargas) || 0;\n\n function escribirEstado(txt) {\n if (status) status.innerText = txt || '';\n }\n\n function estaInstalada() {\n return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;\n }\n\n if (estaInstalada()) {\n btn.innerText = textoInstalada;\n btn.disabled = true;\n btn.style.opacity = '0.75';\n btn.style.cursor = 'default';\n escribirEstado('');\n return;\n }\n\n window.addEventListener('beforeinstallprompt', function(e) {\n e.preventDefault();\n\n deferredPrompt = e;\n\n btn.disabled = false;\n btn.innerText = textoBoton;\n btn.style.opacity = '1';\n btn.style.cursor = 'pointer';\n escribirEstado('Disponible para instalar');\n });\n\n // =====================================================\n // REGISTRAR SERVICE WORKER DESDE EL MISMO ELEMENTO\n // =====================================================\n if ('serviceWorker' in navigator) {\n btn.innerText = textoActivando;\n btn.disabled = true;\n btn.style.opacity = '0.75';\n escribirEstado('Activando instalación...');\n\n navigator.serviceWorker.register('/dmcc/builder/elementos/pwa_descarga.php?sw=1', {\n scope: '/'\n })\n .then(function(reg) {\n console.log('Service Worker registrado desde pwa_descarga:', reg.scope);\n\n btn.disabled = false;\n btn.style.opacity = '1';\n btn.innerText = textoBoton;\n\n if (!navigator.serviceWorker.controller) {\n escribirEstado('Preparando instalación...');\n\n if (!sessionStorage.getItem('pwa_sw_recargado')) {\n sessionStorage.setItem('pwa_sw_recargado', '1');\n\n setTimeout(function() {\n window.location.reload();\n }, 900);\n\n } else {\n escribirEstado('Listo. Si no instala, revisa icono y manifest.');\n }\n\n } else {\n escribirEstado('Listo para instalar');\n }\n })\n .catch(function(err) {\n console.log('Error registrando Service Worker desde pwa_descarga:', err);\n\n escribirEstado('No se pudo activar instalación.');\n btn.disabled = false;\n btn.style.opacity = '1';\n btn.innerText = textoNoDisponible;\n });\n\n } else {\n escribirEstado('Este navegador no soporta instalación PWA.');\n btn.disabled = false;\n btn.style.opacity = '1';\n btn.innerText = textoNoDisponible;\n }\n\n btn.addEventListener('click', async function() {\n if (!deferredPrompt) {\n btn.innerText = textoNoDisponible;\n escribirEstado('La instalación aún no está disponible. Espera unos segundos o recarga la página.');\n\n setTimeout(function() {\n btn.innerText = textoBoton;\n }, 2500);\n\n return;\n }\n\n deferredPrompt.prompt();\n\n const choiceResult = await deferredPrompt.userChoice;\n\n if (choiceResult.outcome === 'accepted') {\n contadorActual++;\n\n if (contador) {\n contador.innerText = contadorActual;\n }\n\n btn.innerText = textoInstalada;\n btn.disabled = true;\n btn.style.opacity = '0.75';\n btn.style.cursor = 'default';\n escribirEstado('');\n }\n\n deferredPrompt = null;\n });\n\n window.addEventListener('appinstalled', function() {\n contadorActual++;\n\n if (contador) {\n contador.innerText = contadorActual;\n }\n\n btn.innerText = textoInstalada;\n btn.disabled = true;\n btn.style.opacity = '0.75';\n btn.style.cursor = 'default';\n escribirEstado('');\n });\n\n }, 300);\n\n return `\n \n\n
\n
\n \n \n \n \n \n \n \n \n \n \n\n \n \n \n\n \n\n \n\n \n\n \n \n \n
AplicaciónDescripciónTipoDescargas
\n \n ${escapeHtml(nombre)}\n \n ${escapeHtml(descripcion)}\n \n ${escapeHtml(tipo)} / ${escapeHtml(tamano)}\n \n ${descargas}\n \n \n \n
\n
\n
\n `;\n}","editor_js":"function(block) {\n const contenido = block.contenido || {};\n const estilos = block.estilos || {};\n\n return `\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n \n \n
\n\n
\n Este elemento incluye su propio Service Worker interno usando el mismo archivo PHP. No necesitas crear sw.js aparte.\n
\n `;\n}"},"tabla":{"nombre":"Tabla","contenido":{"titulo":"Tabla","columnas":["NOMBRE","CIUDAD","VER"],"filas":[{"nombre":"Ejemplo","ciudad":"Ciudad","link":"#"}]},"estilos":{"width":"100%","height":"150px","background":"#ffffff","title_bg":"#2c3e66","title_color":"#ffffff","header_bg":"#f2f2f2","header_color":"#000","button_bg":"#3498db","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n const cols = block.contenido.columnas || ['NOMBRE','CIUDAD','VER'];\n const filas = block.contenido.filas || [];\n let filasHtml = '';\n filas.forEach((f, idx) => {\n filasHtml += `
`;\n });\n return `\n
\n
\n
\n
\n \n
\n
\n
\n
${filasHtml}
\n
\n
\n
\n
\n
\n `;\n}"},"texto":{"nombre":"Texto","contenido":{"html":"

Nuevo texto

"},"estilos":{"width":"100%","height":"auto","background":"#ffffff","color":"#000000","padding":"10px","horizontal_align":"izquierda"},"render_js":null,"editor_js":"function(block) {\n return `\n
\n
\n
\n
\n \n
\n
\n
\n
\n \n \n \n \n \n \n \n \n
\n
\n `;\n}"},"video":{"nombre":"Video","contenido":{"url":"https://www.youtube.com/watch?v=9bZkp7q19f0"},"estilos":{"width":"100%","height":"315px","background":"#000","horizontal_align":"izquierda"},"render_js":"function(block, isBuilder) {\n const url = block.contenido.url || '';\n const width = block.estilos.width || '100%';\n const height = block.estilos.height || '315px';\n const bg = block.estilos.background || '#000';\n const align = block.estilos.horizontal_align || 'izquierda';\n\n // Extraer ID de YouTube\n let videoId = null;\n if (url) {\n const regExp = /^.*(youtu.be\\/|v\\/|u\\/\\w\\/|embed\\/|watch\\?v=|\\&v=)([^#\\&\\?]*).*/;\n const match = url.match(regExp);\n videoId = (match && match[2].length === 11) ? match[2] : null;\n }\n\n let justifyContent = 'flex-start';\n if (align === 'centro') justifyContent = 'center';\n if (align === 'derecha') justifyContent = 'flex-end';\n\n let inner = '';\n if (!videoId) {\n inner = '
URL inválida o no soportada
';\n } else {\n const embedUrl = `https://www.youtube.com/embed/${videoId}`;\n // En el builder, deshabilitamos la interacción con el iframe (pointer-events: none)\n // para que se pueda seleccionar el elemento.\n const iframeStyle = isBuilder ? 'pointer-events: none;' : '';\n inner = ``;\n }\n\n // Contenedor con alineación y overlay para capturar clics en el builder\n // En el builder, agregamos un div transparente encima (overlay) que captura el clic\n // y evita que el iframe lo reciba, así se puede seleccionar el bloque.\n let overlayHtml = '';\n if (isBuilder && videoId) {\n overlayHtml = `
`;\n }\n\n return `\n
\n
\n ${inner}\n ${overlayHtml}\n
\n
\n `;\n}","editor_js":"function(block) {\n const id = block.id.replace(/[^a-zA-Z0-9]/g, '_');\n return `\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
Ejemplo: https://www.youtube.com/watch?v=9bZkp7q19f0
\n `;\n}","init_js":"function(block, container, updateBlock) {\n const id = block.id.replace(/[^a-zA-Z0-9]/g, '_');\n\n const widthInput = container.querySelector(`#width_${id}`);\n const heightInput = container.querySelector(`#height_${id}`);\n const bgInput = container.querySelector(`#bg_${id}`);\n const alignSelect = container.querySelector(`#align_${id}`);\n const urlInput = container.querySelector(`#url_${id}`);\n\n widthInput?.addEventListener('input', e => updateBlock('estilos.width', e.target.value));\n heightInput?.addEventListener('input', e => updateBlock('estilos.height', e.target.value));\n bgInput?.addEventListener('input', e => updateBlock('estilos.background', e.target.value));\n alignSelect?.addEventListener('change', e => updateBlock('estilos.horizontal_align', e.target.value));\n urlInput?.addEventListener('input', e => updateBlock('contenido.url', e.target.value));\n}"}}; const BLOQUES = [{"id":4692,"tipo":"contenedor","contenido":{"columnas":1,"children":[[{"id":4693,"tipo":"espacio","contenido":[],"estilos":{"width":"100%","height":"70px","background":"#ffffff"}}]]},"estilos":{"width":"100%","background":"#ffffff","padding":"0px","border":"0px dashed #aaa"}},{"id":4694,"tipo":"contenedor","contenido":{"columnas":1,"children":[[{"id":4695,"tipo":"imagen","contenido":{"src":"/uploads/uploads/1778948553_6a0899c9cc501.PNG","downloadLink":""},"estilos":{"width":"30%","height":"120px","background":"#ffffff","horizontal_align":"centro"}}]]},"estilos":{"width":"100%","background":"#ffffff","padding":"0px","border":"0px dashed #aaa"}},{"id":4696,"tipo":"contenedor","contenido":{"columnas":1,"children":[[{"id":4697,"tipo":"espacio","contenido":[],"estilos":{"width":"100%","height":"5px","background":"#ffffff"}}]]},"estilos":{"width":"100%","background":"#ffffff","padding":"0px","border":"0px dashed #aaa"}},{"id":4698,"tipo":"contenedor","contenido":{"columnas":1,"children":[[{"id":4699,"tipo":"tabla","contenido":{"titulo":"CORS De La Jurisdicción Inmobiliaria","columnas":["NOMBRE","CIUDAD","VER"],"filas":[{"nombre":"SPED","ciudad":"San Pedro De Macoris","link":"http://monitor.use-snip.com/map?streamName0=SPED&latitude0=18.46&longitude0=-69.31&length=1"},{"nombre":"LVEG","ciudad":"La Vega","link":"http://monitor.use-snip.com/map?streamName0=LVEG&latitude0=19.22&longitude0=-70.53&length=1"},{"nombre":"SROD","ciudad":"Santiago Rodriguez","link":"http://monitor.use-snip.com/map?streamName0=SROD&latitude0=19.48&longitude0=-71.34&length=1"},{"nombre":"BARA","ciudad":"Barahona","link":"http://monitor.use-snip.com/map?streamName0=BARA&latitude0=18.21&longitude0=-71.10&length=1"},{"nombre":"STGO","ciudad":"Santiago","link":"http://monitor.use-snip.com/map?streamName0=STGO&latitude0=19.47&longitude0=-70.71&length=1"},{"nombre":"HGUY","ciudad":"Higuey","link":"http://monitor.use-snip.com/map?streamName0=HGUY&latitude0=18.63&longitude0=-68.74&length=1"},{"nombre":"SAMN","ciudad":"Samana","link":"http://monitor.use-snip.com/map?streamName0=SAMN&latitude0=19.20&longitude0=-69.34&length=1"},{"nombre":"PEVA","ciudad":"Peravia","link":"http://monitor.use-snip.com/map?streamName0=PEVA&latitude0=18.28&longitude0=-70.34&length=1"},{"nombre":"SJUM","ciudad":"San Juan","link":"http://monitor.use-snip.com/map?streamName0=SJUM&latitude0=18.81&longitude0=-71.23&length=1"}]},"estilos":{"width":"100%","height":"400px","background":"#ffffff","title_bg":"#3bba4a","title_color":"#ffffff","header_bg":"#f2f2f2","header_color":"#000","button_bg":"#15a84d","horizontal_align":"izquierda"}}]]},"estilos":{"width":"100%","background":"#f9f9f9","padding":"10px","border":"2px dashed #aaa"}},{"id":4700,"tipo":"contenedor","contenido":{"columnas":1,"children":[[{"id":4701,"tipo":"espacio","contenido":[],"estilos":{"width":"100%","height":"5px","background":"#ffffff"}}]]},"estilos":{"width":"100%","background":"#ffffff","padding":"0px","border":"0px dashed #aaa"}}]; // ===== RENDERIZADO DE ELEMENTOS FALLBACK ===== function renderElementoFallback(block) { const tipo = block.tipo; const contenido = block.contenido || {}; const estilos = block.estilos || {}; const align = estilos.horizontal_align || 'izquierda'; let justifyContent = 'flex-start'; if (align === 'centro') justifyContent = 'center'; if (align === 'derecha') justifyContent = 'flex-end'; let inner = ''; if (tipo === 'texto') { inner = `
${contenido.html || ''}
`; } else if (tipo === 'imagen') { const src = contenido.src || ''; const link = contenido.downloadLink || contenido.link || ''; const target = contenido.linkTarget || '_blank'; const img = ``; inner = link ? `${img}` : img; if (!src) { inner = '
Sin imagen
'; } } else if (tipo === 'boton') { const txt = contenido.texto || 'Botón'; const link = contenido.link || '#'; const bg = estilos.btn_bg || '#333'; const color = estilos.btn_text || '#fff'; const border = estilos.btn_border || bg; const radius = estilos.btn_radius || '0'; const size = estilos.btn_size || '15'; const py = estilos.btn_padding_y || '12'; const px = estilos.btn_padding_x || '18'; const hover = estilos.btn_hover || '#111'; inner = `${window.escapeHtml(txt)}`; } else if (tipo === 'espacio' || tipo === 'espacio_vertical') { inner = '
'; } else if (tipo === 'acordeon') { inner = `
${window.escapeHtml(contenido.titulo || 'Acordeón')}
${contenido.html || ''}
`; } else if (tipo === 'carrusel') { const imgs = contenido.imgs || []; if (imgs.length === 0) { inner = '
Carrusel vacío
'; } else { inner = ``; } } else if (tipo === 'tabla') { const titulo = contenido.titulo || 'Tabla'; const cols = contenido.columnas || ['NOMBRE', 'CIUDAD', 'VER']; const filas = contenido.filas || []; const s = estilos; let html = `
${window.escapeHtml(titulo)}
`; cols.forEach(col => { html += ``; }); html += ``; filas.forEach(f => { html += ``; }); html += `
${window.escapeHtml(col)}
${window.escapeHtml(f.nombre || '')} ${window.escapeHtml(f.ciudad || '')} Mapa
`; inner = html; } else if (tipo === 'video') { const url = contenido.url || ''; const videoId = window.getYoutubeId(url); inner = videoId ? `` : '
URL inválida
'; } else if (tipo === 'formulario') { const s = estilos; inner = `
`; } else if (tipo === 'lista_iconos') { const items = contenido.items || []; const gap = estilos.gap || '15px'; const fontSize = estilos.font_size || '16px'; let html = `
`; items.forEach(item => { html += `
${window.escapeHtml(item.icono || '📌')} ${window.escapeHtml(item.texto || '')}
`; }); html += `
`; inner = html; } else if (tipo === 'descarga') { const items = contenido.items || []; if (items.length === 0) { inner = '
No hay archivos para descargar.
'; } else { const est = estilos; const bg = est.background || '#fff'; const color = est.color || '#000'; const link_color = est.link_color || '#2c3e66'; const radius = est.border_radius || '8px'; const font_size = est.font_size || '14px'; const table_border = est.table_border || '1px solid #e0e0e0'; const header_bg = est.header_bg || '#f5f5f5'; const row_hover = est.row_hover || '#f9f9f9'; const btn_bg = est.btn_bg || '#2c3e66'; const btn_color = est.btn_color || '#fff'; const iconMap = { pdf: 'fa-file-pdf', zip: 'fa-file-archive', rar: 'fa-file-archive', doc: 'fa-file-word', docx: 'fa-file-word', xls: 'fa-file-excel', xlsx: 'fa-file-excel', jpg: 'fa-file-image', jpeg: 'fa-file-image', png: 'fa-file-image', gif: 'fa-file-image', mp3: 'fa-file-audio', mp4: 'fa-file-video', txt: 'fa-file-alt' }; const contadoresReales = (typeof window !== 'undefined' && window.CONTADORES) ? window.CONTADORES : {}; let html = `
`; items.forEach((item) => { const nombre = window.escapeHtml(item.nombre_visible || item.nombre || ''); const desc = window.escapeHtml(item.descripcion || ''); const tamano = window.escapeHtml(item.tamaño || item.tamano || ''); const enlaceGuardado = item.enlace || '#'; const extension = String(item.extension || '').toLowerCase(); const icono = iconMap[extension] || 'fa-file-alt'; let idArchivo = String(enlaceGuardado).replace(/^\//, ''); let contador = 0; if (idArchivo && /^\d+$/.test(idArchivo) && contadoresReales[idArchivo] !== undefined) { contador = contadoresReales[idArchivo]; } else { contador = item.descargas || 0; } const enlaceDescarga = enlaceGuardado; html += ``; }); html += `
Archivo Descripción Tamaño Descargas
${item.mostrar_boton ? `${nombre}` : `${nombre}`} ${desc} ${tamano} ${contador} ${item.mostrar_boton ? `Descargar` : ''}
`; inner = html; } } else if (tipo === 'mapa') { const lat = contenido.lat || 18.4861; const lng = contenido.lng || -69.9312; const zoom = contenido.zoom || 12; const marker = contenido.marker !== false; const height = estilos.height || '300px'; const mapId = 'map_' + block.id + '_' + Math.random().toString(36).substr(2, 8); inner = `
`; } else { inner = `
Elemento no implementado: ${window.escapeHtml(tipo)}
`; } return `
${inner}
`; } // ===== FUNCIÓN RENDERBLOCK CORREGIDA ===== function renderBlock(block) { const cfg = ELEMENTOS[block.tipo]; if (cfg && cfg.render_js) { try { const renderFn = eval('(' + cfg.render_js + ')'); const resultado = renderFn(block, false); if (resultado && typeof resultado === 'string') { return resultado; } } catch(e) { console.error('Error en render_js de', block.tipo, e); } } // Si no tiene render_js, usa el render interno del index. return renderElementoFallback(block); } function renderContenedor(container) { const div = document.createElement('div'); div.className = 'contenedor'; const est = container.estilos || {}; div.style.width = est.width || '100%'; div.style.background = est.background || '#f9f9f9'; div.style.padding = est.padding || '10px'; div.style.border = 'none'; div.style.margin = est.margin || '0'; const colsDiv = document.createElement('div'); colsDiv.className = 'contenedor-columnas'; const columns = container.contenido.children || []; const totalColumnas = parseInt(container.contenido.columnas || 1); for (let i = 0; i < totalColumnas; i++) { const col = document.createElement('div'); col.className = 'columna'; col.style.flex = '1'; const elementos = columns[i] || []; elementos.forEach(elem => { const elemDiv = document.createElement('div'); elemDiv.className = 'elemento'; const elemEst = elem.estilos || {}; elemDiv.style.width = elemEst.width || '100%'; elemDiv.style.height = elemEst.height || 'auto'; elemDiv.style.background = elemEst.background || '#fff'; const align = elemEst.horizontal_align || 'izquierda'; if (align === 'centro') { elemDiv.style.marginLeft = 'auto'; elemDiv.style.marginRight = 'auto'; } else if (align === 'derecha') { elemDiv.style.marginLeft = 'auto'; elemDiv.style.marginRight = '0'; } else { elemDiv.style.marginLeft = '0'; elemDiv.style.marginRight = 'auto'; } elemDiv.innerHTML = renderBlock(elem); col.appendChild(elemDiv); }); colsDiv.appendChild(col); } div.appendChild(colsDiv); return div; } function renderPagina() { const contenedor = document.getElementById('contenido-principal'); if (!contenedor) return; contenedor.innerHTML = ''; BLOQUES.forEach(block => { if (block.tipo === 'contenedor') { contenedor.appendChild(renderContenedor(block)); } else { const elemDiv = document.createElement('div'); elemDiv.className = 'elemento'; elemDiv.innerHTML = renderBlock(block); contenedor.appendChild(elemDiv); } }); } document.addEventListener('DOMContentLoaded', () => { renderPagina(); setTimeout(() => { document.querySelectorAll('.carousel-view').forEach(wrap => { const slides = wrap.querySelectorAll('img'); if (slides.length <= 1) return; if (wrap.dataset.started === '1') return; wrap.dataset.started = '1'; let i = 0; setInterval(() => { slides.forEach(img => img.style.opacity = '0'); i = (i + 1) % slides.length; slides[i].style.opacity = '1'; }, 3000); }); document.querySelectorAll('.form-contacto-front').forEach(form => { if (form.dataset.listener) return; form.dataset.listener = '1'; form.addEventListener('submit', async (e) => { e.preventDefault(); const fd = new FormData(form); const dest = form.dataset.destinatario; fd.append('destinatario', dest); const resp = await fetch(window.location.href, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' }, body: fd }); const res = await resp.json(); const msgDiv = form.parentNode.querySelector('.form-mensaje'); if (res.ok) { msgDiv.style.display = 'block'; msgDiv.style.color = 'green'; msgDiv.innerHTML = 'Mensaje enviado correctamente.'; form.reset(); setTimeout(() => msgDiv.style.display = 'none', 5000); } else { msgDiv.style.display = 'block'; msgDiv.style.color = 'red'; msgDiv.innerHTML = 'Error: ' + (res.error || 'No se pudo enviar'); } }); }); function initMaps() { document.querySelectorAll('.mapa-contenedor').forEach(container => { if (container._map) return; const lat = parseFloat(container.getAttribute('data-lat')); const lng = parseFloat(container.getAttribute('data-lng')); const zoom = parseInt(container.getAttribute('data-zoom') || '12'); const markerEnabled = container.getAttribute('data-marker') !== 'false'; if (isNaN(lat) || isNaN(lng)) return; if (typeof L !== 'undefined') { const map = L.map(container.id).setView([lat, lng], zoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM' }).addTo(map); if (markerEnabled) { L.marker([lat, lng]).addTo(map).bindPopup('Ubicación guardada').openPopup(); } container._map = map; setTimeout(() => map.invalidateSize(), 100); } }); } initMaps(); }, 100); });