/**
* ═════════════════════════════════════════════════════════════
* REPRODUCTOR MULTIMEDIA — Estilo YouTube Music + Spotify
* v7.0 - Controles de pantalla completa + Temporizador de fin de episodio con cuenta regresiva
* ═════════════════════════════════════════════════════════════
*/
(función () {
"usar estricto";
/* ── Estado ─────────────────────────────────────────────── */
constante S = {
jugando: falso,
expandido: falso,
modo: "audio",
mediaUrl: "",
mediaVideo: "",
coverUrl: "",
coverInfo: "",
título: "",
detailUrl: "",
autor: "",
cola: [],
índice de cola: -1,
texto: "",
URL de subtítulos: "",
bgColor: "#111",
permitirDescargar: falso,
subtítulos activados: falso,
SubtítulosPistas: [],
velocidad: 1,
temporizador de sueño: nulo,
minutos de sueño: 0,
sleepEndTime: null,
panelOpen: null,
duración: 0,
Hora actual: 0,
almacenado en búfer: 0,
volumen: 1,
silenciado: falso,
seekDragging: falso,
repetir: falso,
barajar: falso,
Me gustó: falso,
episodeId: null,
// Timer fin de episodio
finDeEpisodioTimer: falso,
endOfEpisodeCallback: null,
// Controles de pantalla completa
fsControlsVisible: falso,
fsControlsTimeout: null,
// Estado del botón flotante de vídeo
videoFloatVisible: falso,
videoFloatTimeout: null,
};
const STORAGE_KEY = "mp_player_state_v7";
/* ── Ayudantes ───────────────────────────────────────────── */
const $ = (sel, ctx) => (ctx || document).querySelector(sel);
const $$ = (sel, ctx) => [...(ctx || document).querySelectorAll(sel)];
const ce = (tag, cls, html) => {
const el = document.createElement(tag);
si (cls) el.className = cls;
si (html) el.innerHTML = html;
devolver el;
};
const fmt = (s) => {
if (!s || !isFinite(s)) return "0:00";
const m = Math.floor(s / 60);
const sec = Math.floor(s % 60);
devolver m + ":" + (sec < 10 ? "0" : "") + sec;
};
const fmtLong = (s) => {
if (!s || !isFinite(s)) return "0:00";
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = Math.floor(s % 60);
if (h > 0) return `${h}:${m < 10 ? "0" : ""}${m}:${sec < 10 ? "0" : ""}${sec}`;
devolver `${m}:${sec < 10 ? "0" : ""}${sec}`;
};
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const hexToRgb = (h) => {
h = h.replace("#", "");
si (h.length === 3) h = h[0]+h[0]+h[1]+h[1]+h[2]+h[2];
devolver [parseInt(h.substr(0,2),16), parseInt(h.substr(2,2),16), parseInt(h.substr(4,2),16)];
};
const oscurecer = (hex, f) => {
const [r,g,b] = hexToRgb(hex);
return `rgb(${Math.round(r*f)},${Math.round(g*f)},${Math.round(b*f)})`;
};
const lighten = (hex, f) => {
const [r,g,b] = hexToRgb(hex);
return `rgb(${Math.min(255, Math.round(r*f))},${Math.min(255, Math.round(g*f))},${Math.min(255, Math.round(b*f))})`;
};
const luminancia = (hex) => {
const [r,g,b] = hexToRgb(hex);
devolver (0,299*r + 0,587*g + 0,114*b) / 255;
};
const textColor = (hex) => luminance(hex) > 0.55 ? "#111" : "#fff";
/* ── Iconos (SVG en línea) ────────────────────────────────── */
const ICO = {
reproducir: `
`,
pausa: `
`,
siguiente: `
`,
anterior: `
`,
vol: `
`,
volMute: `
`,
expandir: `
`,
colapso: `
`,
cola: `
`,
velocidad: `
`,
temporizador: `
`,
compartir: `
`,
subtítulo: `
`,
cerrar: `
`,
descarga: `
`,
videoIcon: `
`,
audioIcon: `
`,
repetir: `
`,
shuffle: `
`,
rewind15: `
15 `,
forward15: `
15 `,
como: `
`,
Me gustó: `
`,
pantalla completa: `
`,
exitFullscreen: `
`,
openEpisode: `
`,
};
const icon = (name, size) => `
${ICO[name]||""} `;
/* ── CSS actualizado (con mejoras para controles y temporizador) ── */
const CSS = `
/* Reiniciar ámbito */
#mp-root,.mp-root *{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
#mp-root{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;position:fixed;bottom:0;left:0;right:0;z-index:999999;pointer-events:none}
#mp-root *{pointer-events:auto}
.mp-ico{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
.mp-ico svg{ancho:100%;alto:100%}
/* Minibar */
.mp-mini{display:none;background:#181818;color:#fff;position:fixed;bottom:0;left:0;right:0;z-index:1000000;border-top:1px solid rgba(255,255,255,.08);transition:background .4s}
.mp-mini.visible{display:block}
.mp-mini-progress{height:3px;background:rgba(255,255,255,.15);cursor:pointer;position:relative}
.mp-mini-progress-fill{height:100%;background:#fff;border-radius:0 2px 2px 0;transition:width .1s linear;position:relative}
.mp-mini-progress-fill::after{content:'';position:absolute;right:-4px;top:50%;transform:translateY(-50%);width:10px;height:10px;background:#fff;border-radius:50%;opacity:0;transition:opacity .15s}
.mp-mini-progress:hover .mp-mini-progress-fill::after{opacity:1}
.mp-mini-progress-buf{posición:absoluta;superior:0;izquierda:0;altura:100%;fondo:rgba(255,255,255,.15);eventos-puntero:ninguno}
.mp-mini-content{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;height:80px;gap:20px}
.mp-mini-left{display:flex;align-items:center;gap:16px;flex:0 0 360px;min-width:0}
.mp-mini-cover{width:56px;height:56px;border-radius:8px;object-fit:cover;cursor:pointer;flex-shrink:0;box-shadow:0 4px 12px rgba(0,0,0,.3)}
.mp-mini-info{flex:1;min-width:0;cursor:pointer}
.mp-mini-title{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-mini-author{font-size:12px;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-mini-actions{display:flex;align-items:center;gap:8px;flex-shrink:0}
.mp-mini-queue,.mp-mini-subtitle,.mp-mini-detail{background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;opacity:.7;transition:all .2s;display:inline-flex;align-items:center}
.mp-mini-queue:hover,.mp-mini-subtitle:hover,.mp-mini-detail:hover{opacity:1;transform:scale(1.1)}
.mp-mini-subtitle.active{color:#1db954;opacity:1}
.mp-mini-center{display:flex;flex-direction:column;align-items:center;gap:6px;flex:1;justify-content:center}
.mp-mini-controls{display:flex;align-items:center;gap:12px;justify-content:center}
.mp-mini-btn{background:none;border:none;color:inherit;cursor:pointer;padding:8px;border-radius:50%;transition:all .15s;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}
.mp-mini-btn:hover{background:rgba(255,255,255,.1);transform:scale(1.05)}
.mp-mini-btn:active{transform:scale(.95)}
.mp-mini-play{width:44px;height:44px;background:#fff!important;color:#000!important;border-radius:50%;padding:0}
.mp-mini-play:hover{transform:scale(1.08)}
.mp-mini-time{display:flex;align-items:center;gap:12px;font-size:11px;color:rgba(255,255,255,.6)}
.mp-mini-time span{min-width:40px;font-variant-numeric:tabular-nums}
.mp-mini-time .mp-sep{flex:1;height:2px;background:rgba(255,255,255,.1);border-radius:2px;min-width:100px}
.mp-mini-right{display:flex;align-items:center;gap:16px;flex:0 0 280px;justify-content:flex-end}
.mp-mini-vol-wrap{display:flex;align-items:center;gap:8px}
.mp-mini-vol-bar{width:80px;height:4px;background:rgba(255,255,255,.2);border-radius:2px;cursor:pointer;position:relative}
.mp-mini-vol-fill{height:100%;background:#fff;border-radius:2px;pointer-events:none}
.mp-speed-badge{padding:4px 12px;background:rgba(255,255,255,.1);border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap}
.mp-speed-badge:hover{background:rgba(255,255,255,.2)}
.mp-like-btn{background:none;border:none;color:rgba(255,255,255,.7);cursor:pointer;padding:8px;border-radius:50%;transition:all .2s;display:inline-flex;align-items:center;flex-shrink:0}
.mp-like-btn:hover{transform:scale(1.1)}
.mp-like-btn.active{color:#ff4444}
.mp-download-btn{background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;opacity:.7;transition:all .2s;display:inline-flex;align-items:center}
.mp-download-btn:hover{opacity:1;transform:scale(1.1)}
/* Vista ampliada */
.mp-expanded{position:fixed;bottom:80px;left:0;right:0;top:0;display:flex;transform:translateY(100%);transition:transform .38s cubic-bezier(.16,1,.3,1);overflow:hidden;z-index:999998}
.mp-expanded.open{transform:translateY(0)}
.mp-expanded-bg{posición:absoluta;superior:0;izquierda:0;derecha:0;inferior:0;transición:fondo .5s ease;z-index:0}
.mp-exp-container{display:flex;width:100%;height:100%;transition:all .3s ease;position:relative;z-index:1}
.mp-exp-media{flex:1;display:flex;align-items:center;justify-content:center;position:relative;transition:flex .3s ease;background:transparent}
.mp-exp-media.with-panel{flex:0 0 60%}
.mp-exp-cover{max-width:80%;max-height:80%;object-fit:contain;border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.5);transition:all .3s}
.mp-exp-video{width:100%;height:100%;object-fit:contain;background:#000;cursor:pointer}
/* Subtítulos modo audio (estilo Spotify) */
.mp-lyrics-subtitles{position:absolute;bottom:20%;left:0;right:0;text-align:center;font-size:clamp(1.8rem, 8vw, 3.5rem);font-weight:bold;color:white;text-shadow:0 0 20px rgba(0,0,0,0.5);background:transparent;padding:0 20px;margin:0 auto;pointer-events:none;z-index:10;max-width:90%;word-break:break-word;transition:all 0.2s}
/* Subtítulos modo video (sobre el video) */
.mp-exp-subs{position:absolute;bottom:80px;left:0;right:0;text-align:center;font-size:1.5rem;font-weight:bold;color:white;text-shadow:0 0 10px black;background:rgba(0,0,0,0.6);padding:8px 16px;border-radius:8px;width:fit-content;margin:0 auto;pointer-events:none;z-index:10;max-width:80%;word-break:break-word}
.mp-exp-top{position:absolute;top:20px;right:20px;z-index:15;display:flex;gap:12px}
.mp-mode-switch{display:flex;align-items:center;gap:4px;background:rgba(0,0,0,.5);backdrop-filter:blur(8px);border-radius:24px;padding:4px}
.mp-mode-opt{background:none;border:none;color:#fff;padding:6px 14px;border-radius:20px;font-size:12px;font-weight:600;cursor:pointer;opacity:.6;transition:all .2s;display:flex;align-items:center;gap:4px}
.mp-mode-opt.active{background:rgba(255,255,255,.2);opacity:1}
.mp-exp-close{background:rgba(0,0,0,.5);backdrop-filter:blur(8px);border:none;color:#fff;border-radius:50%;padding:10px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:all .2s}
.mp-exp-close:hover{background:rgba(0,0,0,.7);transform:scale(1.05)}
/* Botón flotante de pantalla completa (modo vídeo normal) */
.mp-video-fs-btn-float{position:absolute;bottom:20px;right:20px;background:rgba(0,0,0,0.7);backdrop-filter:blur(8px);border:none;color:#fff;border-radius:50%;width:48px;height:48px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:25;transition:opacity 0.2s, transform 0.1s;opacity:0;pointer-events:none}
.mp-video-fs-btn-float.visible{opacity:1;pointer-events:auto}
.mp-video-fs-btn-float:hover{background:rgba(255,255,255,0.3);transform:scale(1.05)}
/* Controles en pantalla completa (superposición sobre el video) */
.mp-fs-overlay{posición:fija;superior:0;izquierda:0;derecha:0;inferior:0;fondo:transparente;z-index:1000001;visualización:flexible;flex-direction:columna;justificar-contenido:espacio-entre;eventos-puntero:ninguno}
.mp-fs-overlay.visible{pointer-events:auto}
.mp-fs-top{position:absolute;top:0;left:0;right:0;padding:20px;background:linear-gradient(180deg, rgba(0,0,0,0.8) 0%, transparent 100%);text-align:center}
.mp-fs-title{font-size:1.2rem;font-weight:600;color:white;text-shadow:0 1px 2px black}
.mp-fs-center{flex:1;display:flex;align-items:center;justify-content:center;gap:40px}
.mp-fs-btn{background:rgba(0,0,0,0.7);border:none;color:white;width:60px;height:60px;border-radius:50%;cursor:pointer;transition:all 0.2s;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px)}
.mp-fs-btn:hover{background:rgba(255,255,255,0.3);transform:scale(1.1)}
.mp-fs-play{ancho:80px;alto:80px}
.mp-fs-bottom{position:absolute;bottom:0;left:0;right:0;padding:20px;background:linear-gradient(0deg, rgba(0,0,0,0.8) 0%, transparent 100%)}
.mp-fs-progress-bar{height:4px;background:rgba(255,255,255,0.3);border-radius:2px;cursor:pointer;margin-bottom:12px}
.mp-fs-progress-fill{height:100%;background:#fff;border-radius:2px;width:0%}
.mp-fs-time{display:flex;justify-content:space-between;font-size:12px;color:white;margin-bottom:12px}
.mp-fs-exit{position:absolute;top:20px;right:20px;background:rgba(0,0,0,0.7);border:none;color:white;width:44px;height:44px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px)}
.mp-fs-exit:hover{background:rgba(255,255,255,0.3)}
/* Panel lateral */
.mp-exp-panel{position:fixed;right:0;top:0;bottom:0;width:40%;background:rgba(20,20,20,.98);backdrop-filter:blur(20px);transform:translateX(100%);transition:transform .3s ease;z-index:20;display:flex;flex-direction:column;border-left:1px solid rgba(255,255,255,.1)}
.mp-exp-panel.open{transform:translateX(0)}
.mp-panel-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid rgba(255,255,255,.1)}
.mp-panel-header h3{font-size:18px;font-weight:700}
.mp-panel-close{background:none;border:none;color:#fff;cursor:pointer;padding:8px;border-radius:50%;transition:background .15s;display:inline-flex;align-items:center}
.mp-panel-close:hover{background:rgba(255,255,255,.1)}
.mp-panel-body{flex:1;overflow-y:auto;padding:16px 24px}
.mp-queue-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;cursor:pointer;transition:background .15s;margin-bottom:4px}
.mp-queue-item:hover{background:rgba(255,255,255,.08)}
.mp-queue-item.active{background:rgba(255,255,255,.12)}
.mp-queue-img{width:48px;height:48px;border-radius:6px;object-fit:cover;flex-shrink:0}
.mp-queue-info{flex:1;min-width:0}
.mp-queue-title{font-size:14px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mp-queue-author{font-size:12px;opacity:.6}
.mp-speed-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.mp-speed-opt{background:rgba(255,255,255,.08);border:2px solid transparent;border-radius:12px;padding:14px;text-align:center;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s}
.mp-speed-opt:hover{background:rgba(255,255,255,.12)}
.mp-speed-opt.active{border-color:#1db954;background:rgba(29,185,84,.15)}
/* Panel de temporizador mejorado */
.mp-timer-grid{display:flex;flex-direction:column;gap:16px}
.mp-timer-presets{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:20px}
.mp-timer-opt{background:rgba(255,255,255,.08);border:2px solid transparent;border-radius:12px;padding:12px;text-align:center;font-size:14px;font-weight:600;cursor:pointer;transition:all .15s}
.mp-timer-opt:hover{background:rgba(255,255,255,.12)}
.mp-timer-opt.active{border-color:#1db954;background:rgba(29,185,84,.15)}
.mp-timer-end-episode{background:rgba(29,185,84,0.2);border-color:#1db954}
.mp-timer-countdown{text-align:center;padding:20px;background:rgba(255,255,255,0.05);border-radius:16px;margin:10px 0}
.mp-countdown-number{font-size:3rem;font-weight:bold;color:white;font-family:monospace;letter-spacing:2px}
.mp-timer-buttons{display:flex;gap:12px;justify-content:center;margin-top:20px}
.mp-timer-btn{background:rgba(255,255,255,.1);border:none;border-radius:40px;padding:12px 24px;font-size:14px;font-weight:600;color:white;cursor:pointer;transition:all .2s}
.mp-timer-btn:hover{background:rgba(255,255,255,.2);transform:scale(1.02)}
.mp-timer-btn.danger{background:rgba(255,80,80,0.3);color:#ff8888}
.mp-timer-btn.danger:hover{background:rgba(255,80,80,0.5)}
.mp-share-grid{display:flex;flex-wrap:wrap;gap:12px;justify-content:center}
.mp-share-btn{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px;border-radius:12px;background:rgba(255,255,255,.08);border:none;color:#fff;cursor:pointer;min-width:90px;font-size:12px;transition:all .15s}
.mp-share-btn:hover{background:rgba(255,255,255,.15);transform:translateY(-2px)}
@media(max-width:768px){
.mp-mini-left{flex:0 0 260px}
.mp-mini-right{flex:0 0 180px}
.mp-mini-vol-bar{width:50px}
.mp-exp-panel{width:100%}
.mp-exp-media.with-panel{flex:0 0 100%}
.mp-mini-time .mp-sep{min-width:50px}
.mp-lyrics-subtitles{font-size:1.8rem;bottom:15%}
.mp-timer-presets{grid-template-columns:repeat(3,1fr)}
.mp-countdown-number{font-size:2rem}
}
`;
/* ── Construir DOM ─────────────────────────────────────────── */
función buildUI() {
const style = document.createElement("style");
estilo.textoContenido = CSS;
documento.head.appendChild(estilo);
const raíz = ce("div");
root.id = "mp-root";
raíz.HTMLinterno = `
${icon("audioIcon",16)} Audio
${icon("videoIcon",16)} Vídeo
${icon("close",24)}
${icon("prev",32)}
${icon("play",40)}
${icon("next",32)}
${icon("exitFullscreen",24)}
${icon("openEpisode",20)}
${icon("queue",20)}
${icon("subtitle",20)}
${icon("shuffle",20)}
${icon("prev",24)}
${icon("rewind15",24)}
${icon("play",28)}
${icon("forward15",24)}
${icon("next",24)}
${icon("repeat",20)}
1.0x
${icon("timer",20)}
${icon("like",22)}
${icon("download",20)}
${icon("expand",24)}
`;
documento.cuerpo.añadirHijo(raíz);
const audio = document.createElement("audio");
audio.id = "mp-audio";
audio.preload = "auto";
audio.style.display = "ninguno";
documento.cuerpo.añadirHijo(audio);
}
/* ── Referencias ─────────────────────────────────────────── */
let els = {};
dejar audioEl, videoEl;
función refs() {
els = {
mini: $("#mp-mini"),
miniCubierta: $("#mp-mini-cubierta"),
miniTítulo: $("#mp-mini-title"),
miniAutor: $("#mp-mini-author"),
miniInfo: $("#mp-mini-info"),
miniFill: $("#mp-mini-fill"),
miniBuf: $("#mp-mini-buf"),
miniProg: $("#mp-mini-prog"),
curTime: $("#mp-cur-time"),
durTime: $("#mp-dur-time"),
playBtn: $("#mp-play-btn"),
prevBtn: $("#mp-prev-btn"),
nextBtn: $("#mp-next-btn"),
rewindBtn: $("#mp-rewind-btn"),
forwardBtn: $("#mp-forward-btn"),
shuffleBtn: $("#mp-shuffle-btn"),
repeatBtn: $("#mp-repeat-btn"),
volBtn: $("#mp-vol-btn"),
barra de volumen: $("#mp-vol-bar"),
volFill: $("#mp-vol-fill"),
speedBtn: $("#mp-speed-btn"),
timerBtn: $("#mp-timer-btn"),
likeBtn: $("#mp-like-btn"),
queueBtn: $("#mp-queue-btn"),
detailBtn: $("#mp-detail-btn"),
Botón de subtítulos: $("#mp-subtitle-btn"),
Botón de descarga: $("#mp-download-btn"),
expandBtn: $("#mp-expand-btn"),
exp: $("#mp-exp"),
expBg: $("#mp-exp-bg"),
expContainer: $("#mp-exp-container"),
expMedia: $("#mp-exp-media"),
expCover: $("#mp-exp-cover"),
expVideo: $("#mp-exp-video"),
lyricsSubs: $("#mp-lyrics-subs"),
videoSubs: $("#mp-exp-subs"),
modeSwitch: $("#mp-mode-switch"),
expClose: $("#mp-exp-close"),
panel lateral: $("#mp-panel lateral"),
Título del panel: $("#mp-panel-title"),
panelBody: $("#mp-panel-body"),
panelClose: $("#mp-panel-close"),
videoFsFloat: $("#mp-video-fs-float"),
fsOverlay: $("#mp-fs-overlay"),
fsTitle: $("#mp-fs-title"),
fsPrev: $("#mp-fs-prev"),
fsPlay: $("#mp-fs-play"),
fsNext: $("#mp-fs-next"),
fsProgress: $("#mp-fs-progress"),
fsProgressFill: $("#mp-fs-progress-fill"),
fsCurrent: $("#mp-fs-current"),
fsDuration: $("#mp-fs-duration"),
fsExit: $("#mp-fs-exit"),
};
audioEl = $("#mp-audio");
videoEl = els.expVideo;
}
función activeMedia() {
devolver S.mode === "vídeo" && S.mediaVideo? videoEl : audioEl;
}
/* ── Almacenamiento ────────────────────────────────────────────── */
función saveState() {
si (!S.episodeId) regresar;
const estado = {
episodeId: S.episodeId,
currentTime: S.currentTime,
jugando: S.playing,
volumen: S.volumen,
silenciado: S.silencioso,
velocidad: Velocidad S,
modo: S.modo,
subtítulosActivados: S.subtítulosActivados,
mediaUrl: S.mediaUrl,
mediaVideo: S.mediaVideo,
coverUrl: S.coverUrl,
título: S.título,
autor: S.autor,
detailUrl: S.detailUrl,
cola: S.queue,
índice de cola: S.índice de cola,
bgColor: S.bgColor,
permitirDescargar: S.permitirDescargar,
URL de subtítulos: S.subtitlesUrl,
Me gusta: S.Me gusta,
repetir: S.repetir,
barajar: S.shuffle,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
función restaurarEstado() {
const saved = localStorage.getItem(STORAGE_KEY);
Si (!guardado) devolver falso;
intentar {
const estado = JSON.parse(guardado);
cargarEpisodio(
estado.mediaUrl, estado.mediaVideo, estado.modo,
estado.coverUrl, "", estado.title, estado.detailUrl, estado.author,
estado.cola, "", estado.url.subtítulos, estado.color.fondo,
estado.permitirDescarga, estado.identificadorEpisodio
);
S.tiempoActual = estado.tiempoActual;
Volumen S = volumen de estado;
S.muted = estado.muted;
Velocidad del estado = velocidad del estado;
S.subtítulosActivados = estado.subtítulosActivados;
S.queueIndex = estado.queueIndex;
S.liked = estado.liked || falso;
S.repeat = estado.repeat || falso;
S.shuffle = state.shuffle || false;
establecerVolumen(S.volumen);
const media = activeMedia();
si (medios) {
media.playbackRate = S.speed;
media.currentTime = S.currentTime;
}
actualizarSpeedUI();
actualizarLikeUI();
actualizarRepeatUI();
actualizarShuffleUI();
actualizarSubtítuloUI();
si (estado.playing) reproducirMedia();
de lo contrario pauseMedia();
devolver verdadero;
} catch(e) { return false; }
}
/* ── Sesión con los medios ─────────────────────────────────────── */
función updateMediaSession() {
si (!navigator.mediaSession) regresar;
navigator.mediaSession.metadata = new MediaMetadata({
título: S.título || "Sin título",
artista: S.author || "Balta Media",
álbum: S.title || "",
obra de arte: S.coverUrl ? [{ src: S.coverUrl, sizes: "512x512", type: "image/png" }] : []
});
navigator.mediaSession.setActionHandler("play", () => togglePlay());
navigator.mediaSession.setActionHandler("pause", () => togglePlay());
navigator.mediaSession.setActionHandler("previoustrack", () => prevTrack());
navigator.mediaSession.setActionHandler("nexttrack", () => nextTrack());
navigator.mediaSession.setActionHandler("seekbackward", (details) => skip(-(details.seekOffset || 15)));
navigator.mediaSession.setActionHandler("seekforward", (details) => skip(details.seekOffset || 15));
Si ('setPositionState' en navigator.mediaSession) {
navegador.mediaSession.setPositionState({
duración: S.duración,
posición: S.currentTime,
Velocidad de reproducción: Velocidad
});
}
}
/* ── Integración de almacenamiento de usuario ──────────────────────────── */
función syncLikedFromStorage() {
Si (window.userStorage && window.userStorage.liked && S.episodeId) {
S.liked = window.userStorage.liked.has(S.episodeId);
actualizarLikeUI();
}
}
función toggleLiked() {
Si (window.userStorage && window.userStorage.liked && S.episodeId) {
window.userStorage.liked.toggle(S.episodeId);
S.liked = window.userStorage.liked.has(S.episodeId);
actualizarLikeUI();
guardarEstado();
} demás {
S.liked = !S.liked;
actualizarLikeUI();
guardarEstado();
}
}
función addToPlaylist() {
Si (window.userStorage && window.userStorage.playlist && S.episodeId) {
const episodio = {
id: S.episodeId,
título: S.título,
autor: S.autor,
coverUrl: S.coverUrl,
detailUrl: S.detailUrl,
mediaUrl: S.mediaUrl,
mediaVideo: S.mediaVideo,
initialMode: S.mode,
bgColor: S.bgColor,
permitirDescargar: S.permitirDescargar,
URL de subtítulos: S.subtitlesUrl,
};
ventana.userStorage.playlist.add(episodio);
const btn = els.queueBtn;
btn.style.transform = "scale(0.9)";
setTimeout(() => btn.style.transform = "", 150);
}
}
/* ── Ayudantes de actualización de la interfaz de usuario ─────────────────────────────────── */
función updateBg() {
const c = S.bgColor || "#111";
els.mini.style.background = `linear-gradient(90deg, ${darken(c,.35)} 0%, ${darken(c,.2)} 100%)`;
els.expBg.style.background = `linear-gradient(135deg, ${lighten(c,1.2)} 0%, ${darken(c,.6)} 100%)`;
const tc = textColor(c);
els.mini.style.color = tc;
}
función updateMiniInfo() {
els.miniCover.src = S.coverUrl || "";
els.miniTitle.textContent = S.título || "";
els.miniAuthor.textContent = S.autor || "";
els.expCover.src = S.coverUrl || "";
if (els.fsTitle) els.fsTitle.textContent = S.title || "";
}
función updatePlayBtn() {
const ic = S.playing ? ICO.pause : ICO.play;
els.playBtn.innerHTML = `
${ic} `;
if (els.fsPlay) els.fsPlay.innerHTML = `
${ic} `;
}
función updateProgress() {
const pct = S.duration ? (S.currentTime / S.duration) * 100 : 0;
els.miniFill.style.width = pct + "%";
els.curTime.textContent = fmt(S.currentTime);
els.durTime.textContent = fmt(S.duración);
si (els.fsProgressFill) {
els.fsProgressFill.style.width = pct + "%";
els.fsCurrent.textContent = fmt(S.currentTime);
els.fsDuration.textContent = fmt(S.duration);
}
const media = activeMedia();
Si (media && media.buffered && media.buffered.length > 0) {
const buf = (media.buffered.end(media.buffered.length - 1) / (S.duration || 1)) * 100;
els.miniBuf.style.width = buf + "%";
}
comprobarTemporizadorDeSueño();
checkEndOfEpisodeTimer();
// Actualizar panel de temporizador si está abierto
if (S.panelOpen === "timer") updateTimerPanelContent();
Si (navigator.mediaSession && 'setPositionState' en navigator.mediaSession) {
navegador.mediaSession.setPositionState({
duración: S.duración,
posición: S.currentTime,
Velocidad de reproducción: Velocidad
});
}
}
/* ── Temporizador fin de episodio (con cuenta regresiva) ──────── */
función obtenerTiempoRestante() {
Si (!S.endOfEpisodeTimer || S.duration <= 0) devuelve 0;
devolver Math.max(0, S.duration - S.currentTime);
}
función checkEndOfEpisodeTimer() {
Si (S.endOfEpisodeTimer && getRemainingTime() <= 0.5) {
pauseMedia();
clearEndOfEpisodeTimer();
if (S.panelOpen === "timer") updateTimerPanelContent();
}
}
función clearEndOfEpisodeTimer() {
S.endOfEpisodeTimer = false;
si (S.endOfEpisodeCallback) {
clearInterval(S.endOfEpisodeCallback);
S.endOfEpisodeCallback = null;
}
}
función setEndOfEpisodeTimer() {
clearSleepTimer();
clearEndOfEpisodeTimer();
S.endOfEpisodeTimer = verdadero;
// No necesitamos intervalos, la comprobación se hace en updateProgress
}
función cancelEndOfEpisodeTimer() {
clearEndOfEpisodeTimer();
if (S.panelOpen === "timer") updateTimerPanelContent();
}
/* ── Temporizador de sueño (minutos) ───────────────────── */
función checkSleepTimer() {
Si (S.sleepTimer && S.sleepEndTime) {
const remaining = S.sleepEndTime - Date.now();
si (restante <= 0) {
pauseMedia();
clearSleepTimer();
if (S.panelOpen === "timer") updateTimerPanelContent();
guardarEstado();
}
}
}
función clearSleepTimer() {
si (S.sleepTimer) {
clearInterval(S.sleepTimer);
S.sleepTimer = null;
}
S.sleepMinutes = 0;
S.sleepEndTime = null;
}
función obtenerSueñoResto() {
if (S.sleepEndTime) return Math.max(0, (S.sleepEndTime - Date.now()) / 1000);
devolver 0;
}
/* ── Subtítulos ───────────────────── ───────────────────── */
función parseTime(str) {
str = str.replace(",", ".");
const partes = str.split(":");
if (parts.length === 3) return parseFloat(parts[0]) * 3600 + parseFloat(parts[1]) * 60 + parseFloat(parts[2]);
if (parts.length === 2) return parseFloat(parts[0]) * 60 + parseFloat(parts[1]);
devolver parseFloat(str);
}
función cargarSubtítulos(url) {
S.subtitlesCues = [];
fetch(url).then(r => r.ok ? r.text() : "").then(txt => {
si (!txt) regresar;
txt = txt.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
const blocks = txt.split(/\n\s*\n/);
para (bloque constante de bloques) {
const líneas = bloque.trim().split("\n");
sea timeLineIndex = -1;
para (sea i = 0; i < lines.length; i++) {
si (lines[i].match(/\d{2}:\d{2}:\d{2}[.,]\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}[.,]\d{3}/)) {
índiceLíneaDeTiempo = i;
romper;
}
}
si (timeLineIndex === -1) continuar;
const match = lines[timeLineIndex].match(/(\d{2}:\d{2}:\d{2}[.,]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[.,]\d{3})/);
si (coincidencia) {
const start = parseTime(match[1]);
const end = parseTime(match[2]);
const texto = líneas.slice(timeLineIndex + 1).join(" ").replace(/<[^>]+>/g, "").trim();
if (text) S.subtitlesCues.push({ start, end, text });
}
}
}).catch(() => console.warn("Error al cargar subtítulos"));
}
función obtenerCuentaActual(tiempo) {
para (const cue de S.subtitlesCues) {
Si (tiempo >= cue.start && tiempo <= cue.end) devuelve cue.text;
}
devolver "";
}
función actualizarSubtítulos() {
Si (!S.subtitlesOn || !S.subtitlesCues.length) {
if (els.lyricsSubs) els.lyricsSubs.style.display = "none";
if (els.videoSubs) els.videoSubs.style.display = "none";
devolver;
}
const cue = getCurrentCue(S.currentTime);
si (!cue) {
if (els.lyricsSubs) els.lyricsSubs.style.display = "none";
if (els.videoSubs) els.videoSubs.style.display = "none";
devolver;
}
Si (S.mode === "audio" && S.expanded) {
els.lyricsSubs.textContent = cue;
els.lyricsSubs.style.display = "block";
if (els.videoSubs) els.videoSubs.style.display = "none";
} else if (S.mode === "video") {
els.videoSubs.textContent = señal;
els.videoSubs.style.display = "block";
if (els.lyricsSubs) els.lyricsSubs.style.display = "none";
} demás {
if (els.lyricsSubs) els.lyricsSubs.style.display = "none";
if (els.videoSubs) els.videoSubs.style.display = "none";
}
}
función alternar subtítulos() {
S.subtitlesOn = !S.subtitlesOn;
actualizarSubtítuloUI();
actualizarSubtítulos();
guardarEstado();
}
función updateSubtitleUI() {
els.subtitleBtn.classList.toggle("active", S.subtitlesOn);
}
/* ── Modo y pantalla completa (con mejoras de hover/clic) ─────── */
función updateMode() {
const hasAudio = !!S.mediaUrl;
const hasVideo = !!S.mediaVideo;
const showModeSwitch = hasAudio && hasVideo;
els.modeSwitch.style.display = showModeSwitch ? "flex" : "none";
si (showModeSwitch) {
$$(".mp-mode-opt", els.modeSwitch).forEach(btn => {
btn.classList.toggle("active", btn.dataset.mode === S.mode);
});
}
Si (S.mode === "video" && hasVideo) {
els.expCover.style.display = "ninguno";
videoEl.style.display = "block";
els.videoFsFloat.style.display = "flex";
// Asegurar eventos de hover y clic para el botón flotante
bindVideoFloatEvents();
} demás {
els.expCover.style.display = "block";
videoEl.style.display = "ninguno";
els.videoFsFloat.style.display = "none";
Si (document.fullscreenElement) document.exitFullscreen();
}
if (!S.expanded && els.lyricsSubs) els.lyricsSubs.style.display = "none";
actualizarSubtítulos();
}
// Manejo del botón flotante de pantalla completa en modo video normal
dejar videoFloatTimeout;
función showFsFloat() {
Si (S.mode !== "video" || document.fullscreenElement) regresar;
els.videoFsFloat.classList.add("visible");
Si (videoFloatTimeout) borrarTimeout(videoFloatTimeout);
videoFloatTimeout = setTimeout(() => {
if (!S.videoFloatVisible) else.videoFsFloat.classList.remove("visible");
}, 5000);
}
función hideFsFloat() {
si (!S.videoFloatVisible) {
els.videoFsFloat.classList.remove("visible");
Si (videoFloatTimeout) borrarTimeout(videoFloatTimeout);
}
}
función toggleFsFloatPermanent() {
if (els.videoFsFloat.classList.contains("visible")) {
S.videoFloatVisible = falso;
ocultarFsFloat();
} demás {
S.videoFloatVisible = verdadero;
els.videoFsFloat.classList.add("visible");
Si (videoFloatTimeout) borrarTimeout(videoFloatTimeout);
}
}
función bindVideoFloatEvents() {
si (!videoEl) regresar;
// Pasar el cursor: mostrar al entrar, ocultar al salir (si no está fijado por clic)
videoEl.addEventListener("mouseenter", () => {
Si (!S.videoFloatVisible) mostrarFsFloat();
});
videoEl.addEventListener("mouseleave", () => {
Si (!S.videoFloatVisible) ocultarFsFloat();
});
// Haz clic en el vídeo: fijar/desfijar el botón
videoEl.addEventListener("hacer clic", (e) => {
si (!document.fullscreenElement) {
alternarFsFloatPermanent();
e.stopPropagation();
}
});
els.videoFsFloat.addEventListener("click", (e) => {
e.stopPropagation();
entrarEnPantallaCompleta();
});
}
// Pantalla completa y controles
función enterFullscreen() {
contenedor constante = els.expMedia;
si (!contenedor) regresar;
contenedor.requestFullscreen().catch(err => console.warn(err));
}
función exitFullscreen() {
Si (document.fullscreenElement) document.exitFullscreen();
}
let fsOverlayTimeout;
función showFsOverlay() {
si (!document.fullscreenElement) regresar;
els.fsOverlay.style.display = "flex";
els.fsOverlay.classList.add("visible");
Si (fsOverlayTimeout) borrarTimeout(fsOverlayTimeout);
fsOverlayTimeout = setTimeout(() => {
if (els.fsOverlay) els.fsOverlay.classList.remove("visible");
}, 3000);
}
función hideFsOverlay() {
if (els.fsOverlay) els.fsOverlay.classList.remove("visible");
}
función toggleFsOverlay() {
si (!document.fullscreenElement) regresar;
if (els.fsOverlay.classList.contains("visible")) hideFsOverlay();
de lo contrario, muestraFsOverlay();
}
función bindFullscreenEvents() {
els.videoFsFloat.onclick = (e) => {
e.stopPropagation();
entrarEnPantallaCompleta();
};
document.addEventListener("fullscreenchange", () => {
si (document.fullscreenElement) {
// Entró a pantalla completa
els.videoFsFloat.classList.remove("visible");
S.videoFloatVisible = falso;
els.fsOverlay.style.display = "flex";
mostrarFsOverlay();
document.addEventListener("mousemove", showFsOverlay);
document.addEventListener("click", toggleFsOverlay);
videoEl.style.objectFit = "contener";
} demás {
// Salió de fullscreen
els.fsOverlay.style.display = "ninguno";
els.fsOverlay.classList.remove("visible");
document.removeEventListener("mousemove", showFsOverlay);
document.removeEventListener("click", toggleFsOverlay);
Si (fsOverlayTimeout) borrarTimeout(fsOverlayTimeout);
}
});
// Controles del overlay
els.fsPlay.onclick = (e) => { e.stopPropagation(); togglePlay(); showFsOverlay(); };
els.fsPrev.onclick = (e) => { e.stopPropagation(); prevTrack(); showFsOverlay(); };
els.fsNext.onclick = (e) => { e.stopPropagation(); siguientePista(); mostrarFsOverlay(); };
els.fsProgress.onclick = (e) => {
e.stopPropagation();
const rect = els.fsProgress.getBoundingClientRect();
const pct = (e.clientX - rect.left) / rect.width;
buscarA(pct);
mostrarFsOverlay();
};
els.fsExit.onclick = (e) => { e.stopPropagation(); salir de pantalla completa(); };
els.fsOverlay.addEventListener("click", (e) => e.stopPropagation());
}
/* ── Paneles ────────────────────── ─────────────────────── */
let currentPanelType = null;
función openPanelWithExpand(type) {
si (!S.expandido) {
S.pendingPanel = tipo;
expandir();
} demás {
abrirPanel(tipo);
}
}
función openPanel(tipo) {
const títulos = {
cola: "A continuación",
velocidad: "Velocidad de reproducción",
temporizador: "Temporizador",
compartir: "Compartir"
};
if (currentPanelType === type && els.sidePanel.classList.contains("open")) {
cerrarPanel();
devolver;
}
els.panelTitle.textContent = títulos[tipo] || "Panel";
currentPanelType = tipo;
if (type === "queue") buildQueuePanel();
Si (tipo === "velocidad") construirPanelVelocidad();
if (type === "timer") buildTimerPanel();
if (type === "share") buildSharePanel();
els.expMedia.classList.add("with-panel");
els.sidePanel.classList.add("open");
S.panelOpen = tipo;
}
función closePanel() {
els.expMedia.classList.remove("with-panel");
els.sidePanel.classList.remove("open");
currentPanelType = null;
S.panelOpen = null;
}
función buildSpeedPanel() {
const velocidades = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3];
els.panelBody.innerHTML = '
' + speeds.map(s =>
`
${s}x
`
).join("") + '
';
$$(".mp-speed-opt", els.panelBody).forEach(el => {
el.onclick = () => {
S.speed = parseFloat(el.dataset.speed);
activeMedia().playbackRate = S.speed;
actualizarSpeedUI();
construirPanelVelocidad();
guardarEstado();
};
});
}
función buildTimerPanel() {
const presets = [5, 10, 15, 30, 45, 60];
let html = `
` + presets.map(m =>
`
${m} min
`
).join("") + `
Fin del episodio
`;
html += `
`;
html += `
Desactivar temporizador
Añadir 5 minutos
`;
els.panelBody.innerHTML = html;
// Botones
$$(".mp-timer-opt", els.panelBody).forEach(el => {
el.onclick = () => {
si (el.dataset.end === "episodio") {
clearSleepTimer();
establecerTemporizadorDeFinDeEpisodio();
construirPanelTemporizador();
guardarEstado();
} demás {
const m = parseInt(el.dataset.min);
clearSleepTimer();
cancelarFinDeEpisodioTimer();
S.sleepMinutes = m;
si (m > 0) {
S.sleepEndTime = Date.now() + (m * 60 * 1000);
S.sleepTimer = setInterval(() => {
comprobarTemporizadorDeSueño();
if (S.panelOpen === "timer") updateTimerPanelContent();
}, 1000);
}
construirPanelTemporizador();
guardarEstado();
}
};
});
const cancelBtn = $("#mp-timer-cancel", els.panelBody);
if (cancelBtn) cancelBtn.onclick = () => { clearSleepTimer(); cancelEndOfEpisodeTimer(); buildTimerPanel(); saveState(); };
const add5Btn = $("#mp-timer-add5", els.panelBody);
si (add5Btn) {
add5Btn.onclick = () => {
si (S.sleepEndTime) {
S.sleepEndTime += 5 * 60 * 1000;
Si (S.sleepTimer) borrarIntervalo(S.sleepTimer);
S.sleepTimer = setInterval(() => {
comprobarTemporizadorDeSueño();
if (S.panelOpen === "timer") updateTimerPanelContent();
}, 1000);
construirPanelTemporizador();
guardarEstado();
} else if (S.endOfEpisodeTimer) {
// No hacer nada para fin de episodio
} else if (S.sleepMinutes === 0 && !S.endOfEpisodeTimer) {
// Si no hay temporizador, añadir 5 minutos
S.sleepMinutes = 5;
S.sleepEndTime = Date.now() + (5 * 60 * 1000);
S.sleepTimer = setInterval(() => {
comprobarTemporizadorDeSueño();
if (S.panelOpen === "timer") updateTimerPanelContent();
}, 1000);
construirPanelTemporizador();
guardarEstado();
}
};
}
actualizarContenidoDelPanelDelTemporizador();
}
función updateTimerPanelContent() {
const countdownDiv = $("#mp-timer-countdown", els.panelBody);
si (!countdownDiv) regresar;
sea restante = 0;
sea isEndEpisode = falso;
si (S.endOfEpisodeTimer) {
restante = obtenerTiempoRestante();
esFinDeEpisodio = verdadero;
} else if (S.sleepEndTime) {
restante = obtenerSueñoRestante();
}
si (restante > 0) {
countdownDiv.innerHTML = `
${fmtLong(remaining)}
${isEndEpisode ? "restante del episodio" : "restantes"}
`;
} else if (S.endOfEpisodeTimer) {
countdownDiv.innerHTML = `
0:00
El episodio finalizará pronto
`;
} demás {
countdownDiv.innerHTML = `
Sin temporizador activo
`;
}
}
función buildSharePanel() {
const url = S.detailUrl ? window.location.origin + S.detailUrl : window.location.href;
const t = encodeURIComponent(S.title + " — " + S.author);
const u = encodeURIComponent(url);
acciones constantes = [
{ nombre: "WhatsApp", url: `https://wa.me/?text=${t}%20${u}`, icono: "💬" },
{ nombre: "Twitter", url: `https://twitter.com/intent/tweet?text=${t}&url=${u}`, icono: "🐦" },
{ nombre: "Facebook", url: `https://www.facebook.com/sharer/sharer.php?u=${u}`, icono: "👍" },
{ nombre: "Telegram", url: `https://t.me/share/url?url=${u}&text=${t}`, icono: "📱" },
{ nombre: "Copiar", url: null, icono: "📋" },
];
els.panelBody.innerHTML = '
' + shares.map(s =>
`${s.icon} ${s.name} `
).join("") + '
';
$$(".mp-share-btn", els.panelBody).forEach(btn => {
btn.onclick = () => {
if (btn.dataset.name === "Copiar") {
navegador.clipboard.writeText(url).then(() => {
btn.querySelector("span:last-child").textContent = ¡Copiado!";
setTimeout(() => btn.querySelector("span:last-child").textContent = "Copiar", 2000);
});
} demás {
window.open(btn.dataset.url, "_blank", "width=600,height=400");
}
};
});
}
función buildQueuePanel() {
si (!S.queue || !S.queue.length) {
els.panelBody.innerHTML = '
No hay episodios en cola.
';
devolver;
}
els.panelBody.innerHTML = S.queue.map((ep, i) =>
`
${escapeHtml(ep.title||"")}
${escapeHtml(ep.author||"")}
`
).unirse("");
$$(".mp-queue-item", els.panelBody).forEach(el => {
el.onclick = () => {
const idx = parseInt(el.dataset.qi);
playQueueItem(idx);
};
});
}
función escapeHtml(str) {
si (!str) devolver "";
return str.replace(/[&<>]/g, function(m) {
si (m === "&") devolver "&";
si (m === "<") devolver "<";
si (m === ">") devolver ">";
devolver m;
});
}
función playQueueItem(idx) {
si (!S.queue[idx]) regresar;
const ep = S.queue[idx];
S.queueIndex = idx;
cancelarFinDeEpisodioTimer();
cargarEpisodio(ep.mediaUrl, ep.mediaVideo, ep.initialMode || "audio", ep.coverUrl, ep.coverInfo, ep.title, ep.detailUrl, ep.author, S.queue, ep.text, ep.subtitlesUrl, ep.bgColor, ep.allowDownload, ep.id);
reproducirMedios();
construirPanelCola();
guardarEstado();
}
/* ── Control de medios ─────────────────────────────────────── */
función loadEpisode(mediaUrl, mediaVideo, initialMode, coverUrl, coverInfo, title, detailUrl, author, queue, text, subtitlesUrl, bgColor, allowDownload, episodeId) {
pauseMedia();
cancelarFinDeEpisodioTimer();
clearSleepTimer();
S.mediaUrl = mediaUrl || "";
S.mediaVideo = mediaVideo || "";
S.coverUrl = coverUrl || coverInfo || "";
S.coverInfo = coverInfo || coverUrl || "";
S.título = título || "";
S.detailUrl = detailUrl || "";
S.autor = autor || "";
S.cola = cola || [];
S.texto = texto || "";
S.subtitlesUrl = subtítulosUrl || "";
S.bgColor = bgColor || "#111";
S.allowDownload = allowDownload === true || allowDownload === "true";
S.episodeId = episodeId || detailUrl;
S.currentTime = 0;
S.duration = 0;
S.subtitlesOn = false;
actualizarSubtítuloUI();
const hasAudio = !!S.mediaUrl;
const hasVideo = !!S.mediaVideo;
Si (initialMode === "video" && hasVideo) S.mode = "video";
else if (hasAudio) S.mode = "audio";
else if (hasVideo) S.mode = "video";
de lo contrario S.mode = "audio";
if (hasAudio) audioEl.src = S.mediaUrl;
if (hasVideo) videoEl.src = S.mediaVideo;
audioEl.playbackRate = S.speed;
videoEl.playbackRate = S.speed;
actualizarBg();
actualizarMiniInfo();
actualizarPlayBtn();
actualizarModo();
actualizarProgreso();
actualizarSpeedUI();
actualizarMediaSesión();
syncLikedFromStorage();
els.downloadBtn.style.display = S.allowDownload ? "inline-flex" : "none";
els.mini.classList.add("visible");
Si (S.subtitlesUrl) cargarSubtítulos(S.subtitlesUrl);
guardarEstado();
}
función playMedia() {
const media = activeMedia();
si (!media || !media.src) regresar;
media.play().catch(() => {});
S.playing = verdadero;
actualizarPlayBtn();
syncMediaStreams();
if (navigator.mediaSession) navigator.mediaSession.playbackState = "playing";
guardarEstado();
}
función pauseMedia() {
audioEl.pause();
videoEl.pause();
S.playing = falso;
actualizarPlayBtn();
if (navigator.mediaSession) navigator.mediaSession.playbackState = "paused";
guardarEstado();
}
función alternar reproducción() {
S.playing ? pauseMedia() : playMedia();
}
función syncMediaStreams() {
si (S.mode === "video" && S.mediaVideo) {
audioEl.pause();
audioEl.muted = verdadero;
videoEl.muted = S.muted;
videoEl.volume = S.volume;
if (S.playing) videoEl.play().catch(() => {});
} demás {
videoEl.pause();
videoEl.muted = verdadero;
audioEl.muted = S.muted;
audioEl.volume = S.volume;
if (S.playing) audioEl.play().catch(() => {});
}
}
función seekTo(pct) {
const media = activeMedia();
si (media && S.duration) {
media.currentTime = pct * S.duration;
S.currentTime = media.currentTime;
actualizarProgreso();
}
}
función skip(offset) {
const media = activeMedia();
si (media && S.duration) {
media.currentTime = Math.min(S.duration, Math.max(0, media.currentTime + offset));
}
}
función nextTrack() {
Si (!S.queue || !S.queue.length) regresar;
let next = S.queueIndex + 1;
if (next >= S.queue.length) next = S.shuffle ? Math.floor(Math.random() * S.queue.length) : 0;
Si (S.queue[next]) playQueueItem(next);
}
función prevTrack() {
Si (!S.queue || !S.queue.length) regresar;
let prev = S.queueIndex - 1;
si (prev < 0) prev = S.queue.length - 1;
si (S.queue[prev]) playQueueItem(prev);
}
función establecerVolumen(v) {
S.volume = clamp(v, 0, 1);
S.muted = S.volume === 0;
activeMedia().volume = S.volume;
activeMedia().muted = S.muted;
els.volFill.style.width = (S.volume * 100) + "%";
els.volBtn.innerHTML = icon(S.muted ? "volMute" : "vol", 20);
guardarEstado();
}
/* ── Expandir / Contraer ─────────────────────────────────── */
función expandir() {
S.expandido = verdadero;
els.exp.classList.add("open");
document.body.style.overflow = "oculto";
actualizarModo();
si (S.pendingPanel) {
setTimeout(() => {
openPanel(S.pendingPanel);
S.pendingPanel = null;
}, 400);
}
}
función colapso() {
S.expanded = falso;
els.exp.classList.remove("open");
cerrarPanel();
document.body.style.overflow = "";
S.pendingPanel = null;
if (els.lyricsSubs) els.lyricsSubs.style.display = "none";
}
función toggleExpand() {
si (S.expanded) collapse();
de lo contrario expand();
}
/* ── Navegación SPA (con botón dedicado) ──────────────── */
función navigateToDetail() {
si (!S.detailUrl) regresar;
si (window.router && typeof window.router === "function") {
window.history.pushState(null, null, S.detailUrl);
ventana.enrutador();
} demás {
console.warn("El enrutador no está disponible, no se navega");
}
}
/* ── Eventos ────────────────────── ─────────────────────── */
función bindEvents() {
els.playBtn.onclick = alternarReproducción;
els.prevBtn.onclick = prevTrack;
els.nextBtn.onclick = nextTrack;
els.rewindBtn.onclick = () => skip(-15);
els.forwardBtn.onclick = () => saltar(15);
els.repeatBtn.onclick = () => { S.repeat = !S.repeat; updateRepeatUI(); saveState(); };
els.shuffleBtn.onclick = () => { S.shuffle = !S.shuffle; updateShuffleUI(); saveState(); };
els.likeBtn.onclick = toggleLiked;
els.queueBtn.onclick = () => openPanelWithExpand("queue");
els.detailBtn.onclick = navigateToDetail;
els.subtitleBtn.onclick = alternarSubtítulos;
els.downloadBtn.onclick = () => {
url constante = S.mode === "vídeo" && S.mediaVideo? S.mediaVideo: S.mediaUrl;
si (url) {
const a = document.createElement("a");
a.href = url;
a.descargar = S.título || "descargar";
a.target = "_blank";
documento.cuerpo.añadirHijo(a);
a.click();
documento.cuerpo.eliminarHijo(a);
}
};
els.speedBtn.onclick = () => openPanelWithExpand("speed");
els.timerBtn.onclick = () => openPanelWithExpand("timer");
els.expandBtn.onclick = toggleExpand;
els.panelClose.onclick = cerrarPanel;
els.expClose.onclick = colapsar;
$$(".mp-mode-opt", els.modeSwitch).forEach(btn => {
btn.onclick = () => {
si (btn.dataset.mode === S.mode) regresar;
S.mode = btn.dataset.mode;
const media = activeMedia();
Si (media.src) media.currentTime = S.currentTime;
media.playbackRate = S.speed;
syncMediaStreams();
actualizarModo();
guardarEstado();
};
});
els.miniProg.onclick = (e) => {
const r = els.miniProg.getBoundingClientRect();
seekTo((e.clientX - r.left) / r.width);
guardarEstado();
};
els.volBtn.onclick = () => { setVolume(S.muted ? (S.volume || 1) : 0); };
els.volBar.onclick = (e) => {
const r = els.volBar.getBoundingClientRect();
establecerVolumen((e.clientX - r.left) / r.width);
};
// También el clic en portada/título navega
els.miniInfo.onclick = navegarToDetail;
els.miniCover.onclick = navigateToDetail;
función onTimeUpdate() {
si (S.seekDragging) regresar;
const m = activeMedia();
S.tiempoActual = m.tiempoActual;
S.duration = m.duration || 0;
actualizarProgreso();
Si (S.subtitlesUrl) actualizarSubtítulos();
}
función onEnded() {
si (S.repetir) {
activeMedia().currentTime = 0;
reproducirMedios();
} demás {
siguientePista();
}
guardarEstado();
}
audioEl.addEventListener("timeupdate", onTimeUpdate);
videoEl.addEventListener("timeupdate", onTimeUpdate);
audioEl.addEventListener("loadedmetadata", () => { S.duration = audioEl.duration; updateProgress(); });
videoEl.addEventListener("loadedmetadata", () => { S.duration = videoEl.duration; updateProgress(); });
audioEl.addEventListener("ended", onEnded);
videoEl.addEventListener("finalizado", onEnded);
document.addEventListener("keydown", (e) => {
if (!els.mini.classList.contains("visible")) return;
Si (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") regresar;
if (e.code === "Espacio") { e.preventDefault(); togglePlay(); }
if (e.code === "ArrowRight") skip(10);
if (e.code === "ArrowLeft") skip(-10);
if (e.code === "ArrowUp") { e.preventDefault(); setVolume(S.volume + 0.1); }
if (e.code === "ArrowDown") { e.preventDefault(); setVolume(S.volume - 0.1); }
if (e.code === "KeyF") { e.preventDefault(); entrar en pantalla completa(); }
});
bindFullscreenEvents();
}
function updateRepeatUI() { els.repeatBtn.classList.toggle("active", S.repeat); }
function updateShuffleUI() { els.shuffleBtn.classList.toggle("active", S.shuffle); }
function updateSpeedUI() { els.speedBtn.textContent = S.speed + "x"; }
función updateLikeUI() {
els.likeBtn.innerHTML = icon(S.liked ? "liked" : "like", 22);
if (S.liked) else.likeBtn.classList.add("active");
else els.likeBtn.classList.remove("active");
}
/* ── API pública ────────────────────────────────────────── */
window.playEpisodeExpanded = function (mediaUrl, mediaVideo, initialMode, coverUrl, coverInfo, title, detailUrl, author, queue, text, subtitlesUrl, bgColor, allowDownload, episodeId) {
if (!els.mini) { buildUI(); refs(); bindEvents(); }
cargarEpisodio(mediaUrl, mediaVideo, initialMode, coverUrl, coverInfo, title, detailUrl, author, queue, text, subtitlesUrl, bgColor, allowDownload, episodeId);
reproducirMedios();
expandir();
};
window.playEpisodeMini = function (mediaUrl, mediaVideo, initialMode, coverUrl, coverInfo, title, detailUrl, author, queue, text, subtitlesUrl, bgColor, allowDownload, episodeId) {
if (!els.mini) { buildUI(); refs(); bindEvents(); }
cargarEpisodio(mediaUrl, mediaVideo, initialMode, coverUrl, coverInfo, title, detailUrl, author, queue, text, subtitlesUrl, bgColor, allowDownload, episodeId);
reproducirMedios();
};
// Inicialización
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
buildUI(); refs(); bindEvents();
els.mini.classList.add("visible");
si (!restoreState()) {
S.expanded = falso;
els.miniTitle.textContent = "Sin reproducción";
els.miniAuthor.textContent = "Selecciona un episodio";
els.miniCover.src = "";
}
});
} demás {
buildUI(); refs(); bindEvents();
els.mini.classList.add("visible");
si (!restoreState()) {
S.expanded = falso;
els.miniTitle.textContent = "Sin reproducción";
els.miniAuthor.textContent = "Selecciona un episodio";
els.miniCover.src = "";
}
}
})();