From d07c8de4521b9b5ab65fbcd8b5e710fe2d4a0d2a Mon Sep 17 00:00:00 2001 From: lenafx Date: Wed, 31 Dec 2025 16:09:44 +0100 Subject: [PATCH] better ui for new selectors on player --- desktop/src/scripts/anime/player.js | 96 +++++++++++++++++++++++------ desktop/views/css/anime/player.css | 64 +++++++++++++++++-- docker/src/scripts/anime/player.js | 96 +++++++++++++++++++++++------ docker/views/css/anime/player.css | 68 +++++++++++++++++--- 4 files changed, 274 insertions(+), 50 deletions(-) diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js index 058bbdd..f638b9b 100644 --- a/desktop/src/scripts/anime/player.js +++ b/desktop/src/scripts/anime/player.js @@ -495,6 +495,10 @@ const AnimePlayer = (function() { els.video.appendChild(track); }); } + const ICONS = { + settings: ``, + audio: `` + }; function createQualitySelector(hls) { const levels = hls.levels; @@ -502,37 +506,64 @@ const AnimePlayer = (function() { const plyrEl = els.video.closest('.plyr'); const controls = plyrEl.querySelector('.plyr__controls'); - if (!controls) return; - - if (controls.querySelector('#quality-select')) return; + if (!controls || controls.querySelector('#quality-control-wrapper')) return; + // 1. Crear el Wrapper const wrapper = document.createElement('div'); - wrapper.className = 'plyr__control'; + wrapper.className = 'plyr__controls__item plyr__custom-select-wrapper'; + wrapper.id = 'quality-control-wrapper'; + // 2. Crear el Botón Visual (Fake) + const btn = document.createElement('div'); + btn.className = 'plyr__custom-control-btn'; + // Icono + Texto Inicial + btn.innerHTML = `${ICONS.settings} Auto`; + + // 3. Crear el Select Real (Invisible) const select = document.createElement('select'); - select.id = 'quality-select'; + select.className = 'plyr__sr-only-select'; // Clase auxiliar si quieres depurar, sino usa el CSS wrapper - // AUTO - const auto = document.createElement('option'); - auto.value = -1; - auto.textContent = 'Auto'; - select.appendChild(auto); + // Opción AUTO + const autoOpt = document.createElement('option'); + autoOpt.value = -1; + autoOpt.textContent = 'Auto'; + select.appendChild(autoOpt); + // Opciones de Niveles levels.forEach((l, i) => { const opt = document.createElement('option'); opt.value = i; - opt.textContent = `${l.height}p`; + opt.textContent = `${l.height}p`; // Texto que sale en el dropdown nativo select.appendChild(opt); }); + // Sincronizar estado inicial select.value = hls.currentLevel; + updateLabel(select.value); + // Evento Change select.onchange = () => { hls.currentLevel = Number(select.value); + updateLabel(select.value); }; + function updateLabel(val) { + const index = Number(val); + let text = 'Auto'; + if (index !== -1 && levels[index]) { + // Solo el número + p (ej: 720p) + text = `${levels[index].height}p`; + } + btn.innerHTML = `${text}`; + } + wrapper.appendChild(select); - controls.insertBefore(wrapper, controls.children[4]); + wrapper.appendChild(btn); + + // Insertar en controles Plyr (antes del botón de pantalla completa o ajustes) + // Insertamos antes del 5º elemento (usualmente settings o fullscreen) + const insertIndex = controls.children.length > 4 ? 4 : controls.children.length - 1; + controls.insertBefore(wrapper, controls.children[insertIndex]); } function createAudioSelector(hls) { @@ -540,15 +571,20 @@ const AnimePlayer = (function() { const plyrEl = els.video.closest('.plyr'); const controls = plyrEl.querySelector('.plyr__controls'); - if (!controls) return; - - if (controls.querySelector('#audio-select')) return; + if (!controls || controls.querySelector('#audio-control-wrapper')) return; + // 1. Wrapper const wrapper = document.createElement('div'); - wrapper.className = 'plyr__control'; + wrapper.className = 'plyr__controls__item plyr__custom-select-wrapper'; + wrapper.id = 'audio-control-wrapper'; + // 2. Botón Visual + const btn = document.createElement('div'); + btn.className = 'plyr__custom-control-btn'; + btn.innerHTML = `Audio 1`; + + // 3. Select Invisible const select = document.createElement('select'); - select.id = 'audio-select'; hls.audioTracks.forEach((t, i) => { const opt = document.createElement('option'); @@ -558,13 +594,37 @@ const AnimePlayer = (function() { }); select.value = hls.audioTrack; + updateLabel(select.value); select.onchange = () => { hls.audioTrack = Number(select.value); + updateLabel(select.value); }; + function updateLabel(val) { + const index = Number(val); + const track = hls.audioTracks[index]; + + // Priorizamos el idioma (lang), luego el nombre + let rawText = track.lang || track.name || `A${index + 1}`; + + // Tomamos solo las 2 primeras letras y las pasamos a Mayúsculas + let shortText = rawText.substring(0, 2).toUpperCase(); + + btn.querySelector('.label-text').innerText = shortText; + } + wrapper.appendChild(select); - controls.insertBefore(wrapper, controls.children[4]); // antes del volumen + wrapper.appendChild(btn); + + // Insertar antes del selector de calidad si existe, o en la posición 4 + const qualityWrapper = controls.querySelector('#quality-control-wrapper'); + if(qualityWrapper) { + controls.insertBefore(wrapper, qualityWrapper); + } else { + const insertIndex = controls.children.length > 4 ? 4 : controls.children.length - 1; + controls.insertBefore(wrapper, controls.children[insertIndex]); + } } function initPlyr(enableAudio = false) { diff --git a/desktop/views/css/anime/player.css b/desktop/views/css/anime/player.css index 82fbe88..f8ce98d 100644 --- a/desktop/views/css/anime/player.css +++ b/desktop/views/css/anime/player.css @@ -520,7 +520,7 @@ body.stop-scrolling { cursor: pointer; transition: all 0.2s ease; backdrop-filter: blur(10px); - height: 36px; /* Para igualar la altura de los selects/toggles */ + height: 36px; } .glass-btn-mpv:hover { @@ -538,13 +538,65 @@ body.stop-scrolling { transform: scale(0.95); } -#audio-select { +.plyr__custom-select-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-left: 4px; +} + +/* El select real: invisible pero clickable */ +.plyr__custom-select-wrapper select { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 2; /* Encima del botón visual */ + appearance: none; + -webkit-appearance: none; +} + +/* El botón visual que imita a Plyr */ +.plyr__custom-control-btn { + position: relative; background: transparent; - color: white; border: none; + color: #fff; /* Plyr default white */ + padding: 7px; + border-radius: 4px; + cursor: pointer; + transition: background 0.3s ease, color 0.3s ease; + font-family: inherit; font-size: 13px; - padding: 4px; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + line-height: 1; } -#audio-select option { - color: black; + +.plyr__custom-control-btn:hover { + background: rgba(255, 255, 255, 0.15); /* Plyr hover effect */ + color: #fff; } + +.plyr__custom-control-btn svg { + width: 20px; + height: 20px; + fill: currentColor; + pointer-events: none; +} + +/* Badge pequeño para indicar calidad actual (opcional) */ +.quality-badge { + background: var(--brand-color); + padding: 2px 4px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; +} \ No newline at end of file diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js index 058bbdd..f638b9b 100644 --- a/docker/src/scripts/anime/player.js +++ b/docker/src/scripts/anime/player.js @@ -495,6 +495,10 @@ const AnimePlayer = (function() { els.video.appendChild(track); }); } + const ICONS = { + settings: ``, + audio: `` + }; function createQualitySelector(hls) { const levels = hls.levels; @@ -502,37 +506,64 @@ const AnimePlayer = (function() { const plyrEl = els.video.closest('.plyr'); const controls = plyrEl.querySelector('.plyr__controls'); - if (!controls) return; - - if (controls.querySelector('#quality-select')) return; + if (!controls || controls.querySelector('#quality-control-wrapper')) return; + // 1. Crear el Wrapper const wrapper = document.createElement('div'); - wrapper.className = 'plyr__control'; + wrapper.className = 'plyr__controls__item plyr__custom-select-wrapper'; + wrapper.id = 'quality-control-wrapper'; + // 2. Crear el Botón Visual (Fake) + const btn = document.createElement('div'); + btn.className = 'plyr__custom-control-btn'; + // Icono + Texto Inicial + btn.innerHTML = `${ICONS.settings} Auto`; + + // 3. Crear el Select Real (Invisible) const select = document.createElement('select'); - select.id = 'quality-select'; + select.className = 'plyr__sr-only-select'; // Clase auxiliar si quieres depurar, sino usa el CSS wrapper - // AUTO - const auto = document.createElement('option'); - auto.value = -1; - auto.textContent = 'Auto'; - select.appendChild(auto); + // Opción AUTO + const autoOpt = document.createElement('option'); + autoOpt.value = -1; + autoOpt.textContent = 'Auto'; + select.appendChild(autoOpt); + // Opciones de Niveles levels.forEach((l, i) => { const opt = document.createElement('option'); opt.value = i; - opt.textContent = `${l.height}p`; + opt.textContent = `${l.height}p`; // Texto que sale en el dropdown nativo select.appendChild(opt); }); + // Sincronizar estado inicial select.value = hls.currentLevel; + updateLabel(select.value); + // Evento Change select.onchange = () => { hls.currentLevel = Number(select.value); + updateLabel(select.value); }; + function updateLabel(val) { + const index = Number(val); + let text = 'Auto'; + if (index !== -1 && levels[index]) { + // Solo el número + p (ej: 720p) + text = `${levels[index].height}p`; + } + btn.innerHTML = `${text}`; + } + wrapper.appendChild(select); - controls.insertBefore(wrapper, controls.children[4]); + wrapper.appendChild(btn); + + // Insertar en controles Plyr (antes del botón de pantalla completa o ajustes) + // Insertamos antes del 5º elemento (usualmente settings o fullscreen) + const insertIndex = controls.children.length > 4 ? 4 : controls.children.length - 1; + controls.insertBefore(wrapper, controls.children[insertIndex]); } function createAudioSelector(hls) { @@ -540,15 +571,20 @@ const AnimePlayer = (function() { const plyrEl = els.video.closest('.plyr'); const controls = plyrEl.querySelector('.plyr__controls'); - if (!controls) return; - - if (controls.querySelector('#audio-select')) return; + if (!controls || controls.querySelector('#audio-control-wrapper')) return; + // 1. Wrapper const wrapper = document.createElement('div'); - wrapper.className = 'plyr__control'; + wrapper.className = 'plyr__controls__item plyr__custom-select-wrapper'; + wrapper.id = 'audio-control-wrapper'; + // 2. Botón Visual + const btn = document.createElement('div'); + btn.className = 'plyr__custom-control-btn'; + btn.innerHTML = `Audio 1`; + + // 3. Select Invisible const select = document.createElement('select'); - select.id = 'audio-select'; hls.audioTracks.forEach((t, i) => { const opt = document.createElement('option'); @@ -558,13 +594,37 @@ const AnimePlayer = (function() { }); select.value = hls.audioTrack; + updateLabel(select.value); select.onchange = () => { hls.audioTrack = Number(select.value); + updateLabel(select.value); }; + function updateLabel(val) { + const index = Number(val); + const track = hls.audioTracks[index]; + + // Priorizamos el idioma (lang), luego el nombre + let rawText = track.lang || track.name || `A${index + 1}`; + + // Tomamos solo las 2 primeras letras y las pasamos a Mayúsculas + let shortText = rawText.substring(0, 2).toUpperCase(); + + btn.querySelector('.label-text').innerText = shortText; + } + wrapper.appendChild(select); - controls.insertBefore(wrapper, controls.children[4]); // antes del volumen + wrapper.appendChild(btn); + + // Insertar antes del selector de calidad si existe, o en la posición 4 + const qualityWrapper = controls.querySelector('#quality-control-wrapper'); + if(qualityWrapper) { + controls.insertBefore(wrapper, qualityWrapper); + } else { + const insertIndex = controls.children.length > 4 ? 4 : controls.children.length - 1; + controls.insertBefore(wrapper, controls.children[insertIndex]); + } } function initPlyr(enableAudio = false) { diff --git a/docker/views/css/anime/player.css b/docker/views/css/anime/player.css index 81a519b..f8ce98d 100644 --- a/docker/views/css/anime/player.css +++ b/docker/views/css/anime/player.css @@ -538,13 +538,65 @@ body.stop-scrolling { transform: scale(0.95); } -#audio-select { - background: transparent; - color: white; - border: none; - font-size: 13px; - padding: 4px; +.plyr__custom-select-wrapper { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-left: 4px; } -#audio-select option { - color: black; + +/* El select real: invisible pero clickable */ +.plyr__custom-select-wrapper select { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + z-index: 2; /* Encima del botón visual */ + appearance: none; + -webkit-appearance: none; +} + +/* El botón visual que imita a Plyr */ +.plyr__custom-control-btn { + position: relative; + background: transparent; + border: none; + color: #fff; /* Plyr default white */ + padding: 7px; + border-radius: 4px; + cursor: pointer; + transition: background 0.3s ease, color 0.3s ease; + font-family: inherit; + font-size: 13px; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + line-height: 1; +} + +.plyr__custom-control-btn:hover { + background: rgba(255, 255, 255, 0.15); /* Plyr hover effect */ + color: #fff; +} + +.plyr__custom-control-btn svg { + width: 20px; + height: 20px; + fill: currentColor; + pointer-events: none; +} + +/* Badge pequeño para indicar calidad actual (opcional) */ +.quality-badge { + background: var(--brand-color); + padding: 2px 4px; + border-radius: 3px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; } \ No newline at end of file