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