Player premium

/** * ═════════════════════════════════════════════════════════════ * 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 = `

cover

Cola

0:00
0:00
`; 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 += `
`; 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 => `` ).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 = ""; } } })();

0 Reviews:

Publicar un comentario

My Instagram