Audio Player siempre visible. Guarda por donde vas
This commit is contained in:
parent
7efaa919b6
commit
7211b678fd
|
|
@ -17,6 +17,7 @@
|
||||||
--warning: #d7ba7d;
|
--warning: #d7ba7d;
|
||||||
--sidebar-w: 290px;
|
--sidebar-w: 290px;
|
||||||
--topbar-h: 52px;
|
--topbar-h: 52px;
|
||||||
|
--audio-bar-h: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
@ -589,6 +590,63 @@ a:hover { text-decoration: underline; }
|
||||||
accent-color: var(--accent);
|
accent-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
AUDIO BAR – barra fija inferior
|
||||||
|
============================================================ */
|
||||||
|
.audio-bar-fixed {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
min-height: var(--audio-bar-h);
|
||||||
|
background: var(--bg-alt);
|
||||||
|
border-top: 3px solid var(--accent);
|
||||||
|
box-shadow: 0 -2px 12px rgba(0,0,0,.4);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .6rem 1rem;
|
||||||
|
padding: .6rem 1.5rem;
|
||||||
|
transition: transform .3s ease;
|
||||||
|
}
|
||||||
|
.audio-bar-fixed.audio-bar-hidden {
|
||||||
|
transform: translateY(110%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.audio-bar-icon {
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.audio-bar-title {
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.audio-bar-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.audio-bar-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
.audio-bar-fixed audio {
|
||||||
|
height: 32px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
body.has-audio-bar {
|
||||||
|
padding-bottom: var(--audio-bar-h);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
CUESTIONARIOS
|
CUESTIONARIOS
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
|
||||||
|
|
@ -115,20 +115,6 @@ function renderMarkdown(md, tema, bloque, bloqueId, temaNum) {
|
||||||
|
|
||||||
const html = marked.parse(md);
|
const html = marked.parse(md);
|
||||||
|
|
||||||
const audioHtml = (tema.audios && tema.audios.length)
|
|
||||||
? `<div class="tema-audio-bar">
|
|
||||||
<i class="fas fa-headphones"></i>
|
|
||||||
<span class="tema-audio-label">Escuchar este tema</span>
|
|
||||||
${tema.audios.map(a => `
|
|
||||||
<div class="tema-audio-item">
|
|
||||||
${tema.audios.length > 1 ? `<span class="tema-audio-tag">${a.label}</span>` : ''}
|
|
||||||
<audio controls preload="none">
|
|
||||||
<source src="${a.src}" type="audio/mpeg">
|
|
||||||
</audio>
|
|
||||||
</div>`).join('')}
|
|
||||||
</div>`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
const leyesHtml = (tema.leyes && tema.leyes.length)
|
const leyesHtml = (tema.leyes && tema.leyes.length)
|
||||||
? `<div class="tema-leyes-bar">
|
? `<div class="tema-leyes-bar">
|
||||||
<i class="fas fa-landmark"></i>
|
<i class="fas fa-landmark"></i>
|
||||||
|
|
@ -142,7 +128,6 @@ function renderMarkdown(md, tema, bloque, bloqueId, temaNum) {
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
document.getElementById('lesson-content').innerHTML = `
|
document.getElementById('lesson-content').innerHTML = `
|
||||||
${audioHtml}
|
|
||||||
${leyesHtml}
|
${leyesHtml}
|
||||||
<div class="md-body">${html}</div>
|
<div class="md-body">${html}</div>
|
||||||
<nav class="lesson-nav" aria-label="Navegación entre temas">
|
<nav class="lesson-nav" aria-label="Navegación entre temas">
|
||||||
|
|
@ -159,6 +144,65 @@ function renderMarkdown(md, tema, bloque, bloqueId, temaNum) {
|
||||||
|
|
||||||
// Actualizar progress en topbar
|
// Actualizar progress en topbar
|
||||||
updateTopbarProgress(pos, total);
|
updateTopbarProgress(pos, total);
|
||||||
|
|
||||||
|
// Barra de audio fija
|
||||||
|
updateAudioBar(tema, bloqueId, temaNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Audio bar fija (bottom) ───────────────────────────────────
|
||||||
|
function updateAudioBar(tema, bloqueId, temaNum) {
|
||||||
|
const bar = document.getElementById('audio-bar');
|
||||||
|
const titleEl = document.getElementById('audio-bar-title');
|
||||||
|
const controlsEl = document.getElementById('audio-bar-controls');
|
||||||
|
if (!bar || !titleEl || !controlsEl) return;
|
||||||
|
|
||||||
|
// Parar cualquier audio en curso
|
||||||
|
bar.querySelectorAll('audio').forEach(a => a.pause());
|
||||||
|
|
||||||
|
if (!tema.audios || !tema.audios.length) {
|
||||||
|
bar.classList.add('audio-bar-hidden');
|
||||||
|
document.body.classList.remove('has-audio-bar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
titleEl.textContent = `B${toRoman(bloqueId)} · T${temaNum}`;
|
||||||
|
|
||||||
|
controlsEl.innerHTML = tema.audios.map(a => `
|
||||||
|
<div class="audio-bar-item">
|
||||||
|
${tema.audios.length > 1 ? `<span class="tema-audio-tag">${a.label}</span>` : ''}
|
||||||
|
<audio controls preload="none" data-key="audioPos:${a.src}">
|
||||||
|
<source src="${a.src}" type="audio/mpeg">
|
||||||
|
</audio>
|
||||||
|
</div>`).join('');
|
||||||
|
|
||||||
|
// Restaurar y guardar posición por audio
|
||||||
|
controlsEl.querySelectorAll('audio').forEach(audioEl => {
|
||||||
|
const key = audioEl.dataset.key;
|
||||||
|
audioEl.addEventListener('loadedmetadata', () => {
|
||||||
|
const saved = parseFloat(localStorage.getItem(key) || '0');
|
||||||
|
if (saved > 1 && saved < audioEl.duration - 2) {
|
||||||
|
audioEl.currentTime = saved;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let _saveTimer = null;
|
||||||
|
audioEl.addEventListener('play', () => {
|
||||||
|
clearInterval(_saveTimer);
|
||||||
|
_saveTimer = setInterval(() => {
|
||||||
|
localStorage.setItem(key, audioEl.currentTime.toString());
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
audioEl.addEventListener('pause', () => {
|
||||||
|
clearInterval(_saveTimer);
|
||||||
|
localStorage.setItem(key, audioEl.currentTime.toString());
|
||||||
|
});
|
||||||
|
audioEl.addEventListener('ended', () => {
|
||||||
|
clearInterval(_saveTimer);
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bar.classList.remove('audio-bar-hidden');
|
||||||
|
document.body.classList.add('has-audio-bar');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
|
|
|
||||||
|
|
@ -322,9 +322,7 @@ Mayor seguridad frente a ataques.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Gestión de usuarios
|
# 8. Gestión de usuarios
|
||||||
|
|
||||||
### 8.1. Introducción
|
|
||||||
|
|
||||||
La gestión de usuarios consiste en administrar:
|
La gestión de usuarios consiste en administrar:
|
||||||
|
|
||||||
|
|
@ -388,7 +386,6 @@ Cada usuario debe tener:
|
||||||
|
|
||||||
- Solo los permisos necesarios.
|
- Solo los permisos necesarios.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tipos de permisos
|
## Tipos de permisos
|
||||||
|
|
||||||
|
|
@ -408,6 +405,15 @@ Ejecutar programas.
|
||||||
|
|
||||||
Administración completa.
|
Administración completa.
|
||||||
|
|
||||||
|
## Linux/Unix
|
||||||
|
- rwx (lectura, escritura, ejecución)
|
||||||
|
- comandos: chmod, chown, chgrp
|
||||||
|
## Windows
|
||||||
|
- R (lectura)
|
||||||
|
- W (escritura)
|
||||||
|
- X (ejecución)
|
||||||
|
- gestión mediante ACLs (Access Control Lists)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 11. Políticas de seguridad
|
# 11. Políticas de seguridad
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,13 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Audio bar (fija, inferior) ──────────────────────── -->
|
||||||
|
<div id="audio-bar" class="audio-bar-fixed audio-bar-hidden" aria-label="Reproductor de audio">
|
||||||
|
<i class="fas fa-headphones audio-bar-icon"></i>
|
||||||
|
<span class="audio-bar-title" id="audio-bar-title"></span>
|
||||||
|
<div class="audio-bar-controls" id="audio-bar-controls"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── Scripts ────────────────────────────────────────── -->
|
<!-- ── Scripts ────────────────────────────────────────── -->
|
||||||
<!-- marked.js para renderizar Markdown en el cliente -->
|
<!-- marked.js para renderizar Markdown en el cliente -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue