diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js
index a55c79d..d57c92d 100644
--- a/desktop/src/scripts/anime/player.js
+++ b/desktop/src/scripts/anime/player.js
@@ -20,6 +20,7 @@ const AnimePlayer = (function() {
let subtitleRenderer = null;
let cursorTimeout = null;
let settingsPanelActive = false;
+ let _settingsView = 'main';
const els = {
wrapper: null,
@@ -204,9 +205,16 @@ const AnimePlayer = (function() {
// Settings
if(els.settingsBtn) {
- els.settingsBtn.onclick = () => {
+ els.settingsBtn.onclick = (e) => {
+ e.stopPropagation();
settingsPanelActive = !settingsPanelActive;
- els.settingsPanel?.classList.toggle('active', settingsPanelActive);
+ if (settingsPanelActive) {
+ _settingsView = 'main';
+ buildSettingsPanel();
+ els.settingsPanel?.classList.add('active');
+ } else {
+ els.settingsPanel?.classList.remove('active');
+ }
};
}
@@ -534,143 +542,233 @@ const AnimePlayer = (function() {
return `${m}:${s.toString().padStart(2, '0')}`;
}
- // Settings Panel
+ const Icons = {
+ back: ` `,
+ check: ` `,
+ chevron: ` `,
+ quality: ` `,
+ audio: ` `,
+ subs: ` `,
+ speed: ` `
+ };
+
function buildSettingsPanel() {
if (!els.settingsPanel) return;
- let html = '';
+ els.settingsPanel.innerHTML = '';
- // 1. Quality settings (for HLS)
+ if (_settingsView === 'main') {
+ buildMainMenu();
+ } else {
+ buildSubMenu(_settingsView);
+ }
+ }
+
+ function buildMainMenu() {
+ let html = `
`;
+
+ // 1. Quality
if (hlsInstance && hlsInstance.levels && hlsInstance.levels.length > 1) {
- html += '
';
- html += '
Calidad
';
-
- html += `
`;
-
- hlsInstance.levels.forEach((level, i) => {
- const active = hlsInstance.currentLevel === i;
- html += `
-
${level.height}p
-
-
-
-
`;
- });
- html += '
';
+ const currentLevel = hlsInstance.currentLevel;
+ const label = currentLevel === -1 ? 'Auto' : (hlsInstance.levels[currentLevel]?.height + 'p');
+ html += createMenuItem('quality', 'Quality', label, Icons.quality);
}
- // 2. Audio tracks
+ // 2. Audio
if (hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 1) {
- html += '
';
- html += '
Audio
';
- hlsInstance.audioTracks.forEach((track, i) => {
- const active = hlsInstance.audioTrack === i;
- const label = track.name || track.lang || `Audio ${i + 1}`;
- html += `
`;
- });
- html += '
';
+ const currentAudio = hlsInstance.audioTrack;
+ const track = hlsInstance.audioTracks[currentAudio];
+ const label = track ? (track.name || track.lang || `Track ${currentAudio + 1}`) : 'Default';
+ html += createMenuItem('audio', 'Audio', label, Icons.audio);
}
- // 3. Subtitles (ESTO FALTABA)
+ // 3. Subtitles
if (_currentSubtitles && _currentSubtitles.length > 0) {
- html += '
';
- html += '
Subtítulos
';
-
- // Opción para desactivar
- const isOff = els.video.textTracks && Array.from(els.video.textTracks).every(t => t.mode === 'hidden' || t.mode === 'disabled');
-
- html += `
`;
-
- // Lista de subtítulos
- _currentSubtitles.forEach((sub, i) => {
- // Verificamos si este track está activo en el elemento de video
- let isActive = false;
- if (els.video.textTracks && els.video.textTracks[i]) {
- isActive = els.video.textTracks[i].mode === 'showing';
- }
-
- html += `
-
${sub.label || sub.language || 'Desconocido'}
-
-
-
-
`;
- });
- html += '
';
+ let label = 'Off';
+ const activeIndex = getActiveSubtitleIndex();
+ if (activeIndex !== -1 && _currentSubtitles[activeIndex]) {
+ label = _currentSubtitles[activeIndex].label || _currentSubtitles[activeIndex].language;
+ }
+ html += createMenuItem('subtitle', 'Subtitles', label, Icons.subs);
}
- // 4. Playback speed
- html += '
';
- html += '
Velocidad
';
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
- speeds.forEach(speed => {
- const active = els.video && Math.abs(els.video.playbackRate - speed) < 0.01;
- html += `
`;
+ // 4. Playback Speed
+ if (els.video) {
+ const label = els.video.playbackRate === 1 ? 'Normal' : `${els.video.playbackRate}x`;
+ html += createMenuItem('speed', 'Playback Speed', label, Icons.speed);
+ }
+
+ html += `
`;
+ els.settingsPanel.innerHTML = html;
+
+ // Listeners del menú principal
+ els.settingsPanel.querySelectorAll('.settings-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.stopPropagation();
+ _settingsView = item.dataset.target;
+ buildSettingsPanel();
+ });
});
- html += '
';
+ }
+
+ function createMenuItem(target, title, value, icon) {
+ return `
+
+
+ ${icon}
+ ${title}
+
+
+ ${value}
+ ${Icons.chevron}
+
+
+ `;
+ }
+
+ function buildSubMenu(type) {
+ let title = '';
+ let content = '';
+
+ if (type === 'quality') {
+ title = 'Quality';
+ content = renderQualityOptions();
+ } else if (type === 'audio') {
+ title = 'Audio Track';
+ content = renderAudioOptions();
+ } else if (type === 'subtitle') {
+ title = 'Subtitles';
+ content = renderSubtitleOptions();
+ } else if (type === 'speed') {
+ title = 'Playback Speed';
+ content = renderSpeedOptions();
+ }
+
+ const html = `
+
+
+ ${content}
+
+ `;
els.settingsPanel.innerHTML = html;
- // Add click handlers
- els.settingsPanel.querySelectorAll('.settings-option').forEach(opt => {
- opt.addEventListener('click', () => {
- const action = opt.dataset.action;
- const value = opt.dataset.value;
+ // Listener para volver atrás
+ els.settingsPanel.querySelector('.settings-back-btn').addEventListener('click', (e) => {
+ e.stopPropagation();
+ _settingsView = 'main';
+ buildSettingsPanel();
+ });
- if (action === 'quality') {
- if (hlsInstance) {
- hlsInstance.currentLevel = parseInt(value);
- buildSettingsPanel();
- }
- } else if (action === 'audio') {
- if (hlsInstance) {
- hlsInstance.audioTrack = parseInt(value);
- buildSettingsPanel();
- }
- } else if (action === 'subtitle') {
- // Lógica para cambiar subtítulos
- const idx = parseInt(value);
- if (els.video && els.video.textTracks) {
- Array.from(els.video.textTracks).forEach((track, i) => {
- // Activamos si el índice coincide, desactivamos si es -1 u otro
- track.mode = (i === idx) ? 'showing' : 'hidden';
- });
- }
-
- // Si usas SubtitlesOctopus (Canvas) para ASS, aquí podrías necesitar lógica extra,
- // pero para la mayoría de los casos web (VTT), cambiar el modo del track es suficiente.
- buildSettingsPanel();
-
- } else if (action === 'speed') {
- if (els.video) {
- els.video.playbackRate = parseFloat(value);
- buildSettingsPanel();
- }
- }
+ // Listeners para opciones CORREGIDO
+ els.settingsPanel.querySelectorAll('.settings-item-option').forEach(opt => {
+ opt.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const val = opt.dataset.value;
+ applySetting(type, val);
});
});
}
+ // Funciones de renderizado de opciones
+ function renderQualityOptions() {
+ if (!hlsInstance) return '';
+ let html = '';
+
+ // Auto option
+ const isAuto = hlsInstance.currentLevel === -1;
+ html += `
+ Auto ${isAuto ? Icons.check : ''}
+
`;
+
+ // Levels desc
+ hlsInstance.levels.forEach((level, i) => {
+ const isSelected = hlsInstance.currentLevel === i;
+ html += `
+ ${level.height}p ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderAudioOptions() {
+ if (!hlsInstance) return '';
+ let html = '';
+ hlsInstance.audioTracks.forEach((track, i) => {
+ const isSelected = hlsInstance.audioTrack === i;
+ const label = track.name || track.lang || `Audio ${i + 1}`;
+ html += `
+ ${label} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderSubtitleOptions() {
+ let html = '';
+ const activeIdx = getActiveSubtitleIndex();
+
+ // Off
+ html += `
+ Off ${activeIdx === -1 ? Icons.check : ''}
+
`;
+
+ _currentSubtitles.forEach((sub, i) => {
+ const isSelected = activeIdx === i;
+ html += `
+ ${sub.label || sub.language} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderSpeedOptions() {
+ const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
+ let html = '';
+ const currentRate = els.video ? els.video.playbackRate : 1;
+
+ speeds.forEach(speed => {
+ const isSelected = Math.abs(currentRate - speed) < 0.1;
+ html += `
+ ${speed === 1 ? 'Normal' : speed + 'x'} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ // Aplicar configuración
+ function applySetting(type, value) {
+ if (type === 'quality') {
+ if (hlsInstance) hlsInstance.currentLevel = parseInt(value);
+ } else if (type === 'audio') {
+ if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
+ } else if (type === 'subtitle') {
+ const idx = parseInt(value);
+ if (els.video && els.video.textTracks) {
+ Array.from(els.video.textTracks).forEach((track, i) => {
+ track.mode = (i === idx) ? 'showing' : 'hidden';
+ });
+ }
+ } else if (type === 'speed') {
+ if (els.video) els.video.playbackRate = parseFloat(value);
+ }
+
+ // Volvemos al menú principal para confirmar visualmente (opcional, estilo YouTube)
+ _settingsView = 'main';
+ buildSettingsPanel();
+ }
+
+ function getActiveSubtitleIndex() {
+ if (!els.video || !els.video.textTracks) return -1;
+ for (let i = 0; i < els.video.textTracks.length; i++) {
+ if (els.video.textTracks[i].mode === 'showing') return i;
+ }
+ return -1;
+ }
+
// Subtitle renderer with libass
async function initSubtitleRenderer() {
if (!window.SubtitlesOctopus || !els.video || !els.subtitlesCanvas) return;
@@ -1246,20 +1344,35 @@ const AnimePlayer = (function() {
function renderSkipMarkers(intervals) {
if (!els.progressContainer || !els.video.duration) return;
- // Remove existing markers
- els.progressContainer.querySelectorAll('.skip-marker').forEach(e => e.remove());
+ els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove());
+
+ const duration = els.video.duration;
intervals.forEach(skip => {
- const el = document.createElement('div');
- el.className = `skip-marker ${skip.type}`;
- const startPct = (skip.startTime / els.video.duration) * 100;
- const endPct = (skip.endTime / els.video.duration) * 100;
- el.style.left = `${startPct}%`;
- el.style.width = `${endPct - startPct}%`;
- els.progressContainer.appendChild(el);
+ const startPct = (skip.startTime / duration) * 100;
+ const endPct = (skip.endTime / duration) * 100;
+
+ const range = document.createElement('div');
+ range.className = `skip-range ${skip.type}`; // 'op' o 'ed'
+ range.style.left = `${startPct}%`;
+ range.style.width = `${endPct - startPct}%`;
+ els.progressContainer.appendChild(range);
+
+ createCut(startPct);
+
+ createCut(endPct);
});
}
+ function createCut(percent) {
+ if (percent < 0.5 || percent > 99.5) return;
+
+ const cut = document.createElement('div');
+ cut.className = 'skip-cut';
+ cut.style.left = `${percent}%`;
+ els.progressContainer.appendChild(cut);
+ }
+
function monitorSkipButton(intervals) {
if (!_skipBtn || !els.video) return;
diff --git a/desktop/views/css/anime/player.css b/desktop/views/css/anime/player.css
index 42183e0..81736a3 100644
--- a/desktop/views/css/anime/player.css
+++ b/desktop/views/css/anime/player.css
@@ -1,14 +1,15 @@
:root {
--brand-color: #8b5cf6;
--brand-color-light: #a78bfa;
- --op-color: #fbbf24;
- --ed-color: #38bdf8;
+ --brand-gradient: linear-gradient(90deg, #7c3aed 0%, #a78bfa 100%);
+ --op-color: rgba(251, 191, 36, 0.3); /* Ámbar sutil */
+ --ed-color: rgba(56, 189, 248, 0.3); /* Azul cielo sutil */
+
+ --glass-bg: rgba(10, 10, 10, 0.65);
+ --glass-border: rgba(255, 255, 255, 0.08);
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ --glass-blur: blur(20px);
--player-bg: #000;
- --control-bg: rgba(0, 0, 0, 0.8);
- --control-hover: rgba(255, 255, 255, 0.1);
- --slider-bg: rgba(255, 255, 255, 0.3);
- --slider-buffer: rgba(255, 255, 255, 0.5);
- --slider-played: var(--brand-color);
}
body.stop-scrolling {
@@ -278,6 +279,14 @@ body.stop-scrolling {
pointer-events: none;
}
+.glass-panel {
+ background: rgba(20, 20, 20, 0.75);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
/* Skip Overlay Button */
#skip-overlay-btn {
position: absolute;
@@ -328,11 +337,15 @@ body.stop-scrolling {
bottom: 0;
left: 0;
width: 100%;
- padding: 0 20px 20px;
+ padding: 0;
z-index: 60;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
+ background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
}
.player-container.show-cursor .custom-controls {
@@ -341,88 +354,102 @@ body.stop-scrolling {
}
.controls-gradient {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 150px;
- background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, transparent 100%);
- z-index: -1;
+ display: none;
}
/* Progress Bar */
.progress-container {
- width: 100%;
- height: 6px;
- background: var(--slider-bg);
- border-radius: 3px;
+ width: calc(100% - 48px);
+ height: 4px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 2px;
cursor: pointer;
- margin-bottom: 12px;
+ margin: 0 auto 10px auto;
position: relative;
- transition: height 0.2s;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ z-index: 70;
}
.progress-container:hover {
- height: 8px;
+ height: 6px;
+ transform: scaleY(1);
}
.progress-buffer {
position: absolute;
height: 100%;
- background: var(--slider-buffer);
- border-radius: 3px;
+ background: rgba(255, 255, 255, 0.25);
+ border-radius: 10px;
pointer-events: none;
+ transition: width 0.2s linear;
}
.progress-played {
+ background: var(--brand-color);
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.6);
position: absolute;
height: 100%;
- background: var(--slider-played);
- border-radius: 3px;
- pointer-events: none;
+ border-radius: 2px;
}
.progress-handle {
+ width: 14px;
+ height: 14px;
+ background: #fff;
+ border-radius: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%) scale(0);
- width: 16px;
- height: 16px;
- background: white;
- border-radius: 50%;
- pointer-events: none;
- transition: transform 0.2s;
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+ transition: transform 0.15s ease;
+ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.2);
+ z-index: 71;
}
.progress-container:hover .progress-handle {
transform: translate(-50%, -50%) scale(1);
+ box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.2);
}
/* Skip Markers on Progress */
-.skip-marker {
+.skip-range {
position: absolute;
top: 0;
height: 100%;
- background: rgba(255, 255, 255, 0.3);
- border-left: 2px solid rgba(0,0,0,0.3);
- border-right: 2px solid rgba(0,0,0,0.3);
+ z-index: 1;
pointer-events: none;
}
-.skip-marker.op {
- background: var(--op-color);
-}
+.skip-range.op { background: var(--op-color); }
+.skip-range.ed { background: var(--ed-color); }
-.skip-marker.ed {
- background: var(--ed-color);
+.skip-cut {
+ position: absolute;
+ top: -4px;
+ bottom: -4px;
+ width: 3px;
+ background-color: #000;
+ z-index: 5;
+ pointer-events: none;
+ border-radius: 2px;
+ box-shadow: 0 0 2px rgba(0,0,0,1);
}
/* Controls Row */
.controls-row {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+
+ width: 100%;
+ padding: 5px 24px 20px 24px;
+ margin: 0;
+ border-radius: 0;
+
display: flex;
align-items: center;
- gap: 12px;
+ justify-content: space-between;
}
.controls-left,
@@ -441,25 +468,30 @@ body.stop-scrolling {
.control-btn {
background: transparent;
border: none;
- color: white;
- cursor: pointer;
- padding: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- border-radius: 6px;
+ color: rgba(255, 255, 255, 0.85);
+ padding: 10px;
+ margin: 0 2px;
+ border-radius: 50%;
+ transition: all 0.2s ease;
}
.control-btn:hover {
- background: var(--control-hover);
- transform: scale(1.1);
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ transform: none;
+ box-shadow: none;
}
.control-btn svg {
- width: 24px;
- height: 24px;
+ width: 26px;
+ height: 26px;
fill: currentColor;
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
+ stroke: none;
+}
+
+.control-btn:active {
+ transform: translateY(0);
}
.control-btn.play-pause svg {
@@ -467,14 +499,13 @@ body.stop-scrolling {
height: 32px;
}
-/* Time Display */
.time-display {
+ font-family: 'Inter', monospace;
+ font-variant-numeric: tabular-nums; /* Evita que los números "bailen" */
font-size: 0.9rem;
font-weight: 500;
- color: white;
- font-variant-numeric: tabular-nums;
- min-width: 100px;
- text-align: center;
+ opacity: 0.9;
+ margin-left: 16px;
}
/* Volume Control */
@@ -487,30 +518,37 @@ body.stop-scrolling {
.volume-slider-container {
width: 0;
overflow: hidden;
- transition: width 0.3s ease;
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ margin-left: 5px;
}
.volume-control:hover .volume-slider-container {
- width: 80px;
+ width: 100px;
+ margin-left: 10px;
}
.volume-slider {
width: 100%;
height: 4px;
-webkit-appearance: none;
- background: var(--slider-bg);
- border-radius: 2px;
+ background: rgba(255,255,255,0.2);
+ border-radius: 4px;
outline: none;
- cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
- background: white;
+ background: #fff;
border-radius: 50%;
cursor: pointer;
+ box-shadow: 0 0 10px rgba(255,255,255,0.5);
+ transition: transform 0.1s;
+}
+
+.volume-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
}
.volume-slider::-moz-range-thumb {
@@ -522,29 +560,145 @@ body.stop-scrolling {
border: none;
}
-/* Settings Panel */
.settings-panel {
position: absolute;
- bottom: 60px;
+ bottom: 80px;
right: 20px;
- background: rgba(20, 20, 20, 0.98);
- border: 1px solid rgba(255,255,255,0.1);
- border-radius: 12px;
- padding: 12px;
- min-width: 240px;
- backdrop-filter: blur(20px);
+ width: 300px;
+ max-height: 400px;
+ border-radius: 16px;
+ padding: 0;
+ overflow: hidden;
opacity: 0;
- transform: translateY(10px);
+ transform: translateY(20px) scale(0.95);
pointer-events: none;
- transition: opacity 0.2s, transform 0.2s;
+ transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
+ z-index: 100;
+
+ background: rgba(15, 15, 15, 0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
}
.settings-panel.active {
opacity: 1;
- transform: translateY(0);
+ transform: translateY(0) scale(1);
pointer-events: auto;
}
+.settings-back-btn {
+ background: transparent;
+ border: none;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+ border-radius: 50%;
+ transition: background 0.2s;
+}
+
+.settings-back-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.settings-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ margin-bottom: 4px;
+}
+
+.settings-title {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #fff;
+}
+
+/* Lista de opciones */
+.settings-content {
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+/* Item del menú */
+.settings-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ cursor: pointer;
+ border-radius: 8px;
+ transition: background 0.2s;
+ color: #eee;
+ font-size: 0.9rem;
+ margin: 0 4px;
+}
+
+.settings-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* Estilos específicos para la vista principal */
+.settings-item-main {
+ justify-content: space-between;
+}
+
+.settings-label-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 500;
+}
+
+.settings-label-icon {
+ opacity: 0.7;
+}
+
+.settings-value-right {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.85rem;
+}
+
+/* Item activo (seleccionado en submenú) */
+.settings-item.selected {
+ color: var(--brand-color-light);
+}
+
+.settings-item .check-icon {
+ width: 16px;
+ height: 16px;
+ opacity: 0;
+ margin-left: 10px;
+}
+
+.settings-item.selected .check-icon {
+ opacity: 1;
+}
+
+/* Scrollbar bonito para listas largas */
+.settings-content {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.settings-content::-webkit-scrollbar {
+ width: 4px;
+}
+.settings-content::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+}
+
.settings-section {
margin-bottom: 16px;
}
diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js
index a55c79d..d57c92d 100644
--- a/docker/src/scripts/anime/player.js
+++ b/docker/src/scripts/anime/player.js
@@ -20,6 +20,7 @@ const AnimePlayer = (function() {
let subtitleRenderer = null;
let cursorTimeout = null;
let settingsPanelActive = false;
+ let _settingsView = 'main';
const els = {
wrapper: null,
@@ -204,9 +205,16 @@ const AnimePlayer = (function() {
// Settings
if(els.settingsBtn) {
- els.settingsBtn.onclick = () => {
+ els.settingsBtn.onclick = (e) => {
+ e.stopPropagation();
settingsPanelActive = !settingsPanelActive;
- els.settingsPanel?.classList.toggle('active', settingsPanelActive);
+ if (settingsPanelActive) {
+ _settingsView = 'main';
+ buildSettingsPanel();
+ els.settingsPanel?.classList.add('active');
+ } else {
+ els.settingsPanel?.classList.remove('active');
+ }
};
}
@@ -534,143 +542,233 @@ const AnimePlayer = (function() {
return `${m}:${s.toString().padStart(2, '0')}`;
}
- // Settings Panel
+ const Icons = {
+ back: ` `,
+ check: ` `,
+ chevron: ` `,
+ quality: ` `,
+ audio: ` `,
+ subs: ` `,
+ speed: ` `
+ };
+
function buildSettingsPanel() {
if (!els.settingsPanel) return;
- let html = '';
+ els.settingsPanel.innerHTML = '';
- // 1. Quality settings (for HLS)
+ if (_settingsView === 'main') {
+ buildMainMenu();
+ } else {
+ buildSubMenu(_settingsView);
+ }
+ }
+
+ function buildMainMenu() {
+ let html = ``;
+
+ // 1. Quality
if (hlsInstance && hlsInstance.levels && hlsInstance.levels.length > 1) {
- html += '
';
- html += '
Calidad
';
-
- html += `
`;
-
- hlsInstance.levels.forEach((level, i) => {
- const active = hlsInstance.currentLevel === i;
- html += `
-
${level.height}p
-
-
-
-
`;
- });
- html += '
';
+ const currentLevel = hlsInstance.currentLevel;
+ const label = currentLevel === -1 ? 'Auto' : (hlsInstance.levels[currentLevel]?.height + 'p');
+ html += createMenuItem('quality', 'Quality', label, Icons.quality);
}
- // 2. Audio tracks
+ // 2. Audio
if (hlsInstance && hlsInstance.audioTracks && hlsInstance.audioTracks.length > 1) {
- html += '
';
- html += '
Audio
';
- hlsInstance.audioTracks.forEach((track, i) => {
- const active = hlsInstance.audioTrack === i;
- const label = track.name || track.lang || `Audio ${i + 1}`;
- html += `
`;
- });
- html += '
';
+ const currentAudio = hlsInstance.audioTrack;
+ const track = hlsInstance.audioTracks[currentAudio];
+ const label = track ? (track.name || track.lang || `Track ${currentAudio + 1}`) : 'Default';
+ html += createMenuItem('audio', 'Audio', label, Icons.audio);
}
- // 3. Subtitles (ESTO FALTABA)
+ // 3. Subtitles
if (_currentSubtitles && _currentSubtitles.length > 0) {
- html += '
';
- html += '
Subtítulos
';
-
- // Opción para desactivar
- const isOff = els.video.textTracks && Array.from(els.video.textTracks).every(t => t.mode === 'hidden' || t.mode === 'disabled');
-
- html += `
`;
-
- // Lista de subtítulos
- _currentSubtitles.forEach((sub, i) => {
- // Verificamos si este track está activo en el elemento de video
- let isActive = false;
- if (els.video.textTracks && els.video.textTracks[i]) {
- isActive = els.video.textTracks[i].mode === 'showing';
- }
-
- html += `
-
${sub.label || sub.language || 'Desconocido'}
-
-
-
-
`;
- });
- html += '
';
+ let label = 'Off';
+ const activeIndex = getActiveSubtitleIndex();
+ if (activeIndex !== -1 && _currentSubtitles[activeIndex]) {
+ label = _currentSubtitles[activeIndex].label || _currentSubtitles[activeIndex].language;
+ }
+ html += createMenuItem('subtitle', 'Subtitles', label, Icons.subs);
}
- // 4. Playback speed
- html += '
';
- html += '
Velocidad
';
- const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
- speeds.forEach(speed => {
- const active = els.video && Math.abs(els.video.playbackRate - speed) < 0.01;
- html += `
`;
+ // 4. Playback Speed
+ if (els.video) {
+ const label = els.video.playbackRate === 1 ? 'Normal' : `${els.video.playbackRate}x`;
+ html += createMenuItem('speed', 'Playback Speed', label, Icons.speed);
+ }
+
+ html += `
`;
+ els.settingsPanel.innerHTML = html;
+
+ // Listeners del menú principal
+ els.settingsPanel.querySelectorAll('.settings-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.stopPropagation();
+ _settingsView = item.dataset.target;
+ buildSettingsPanel();
+ });
});
- html += '
';
+ }
+
+ function createMenuItem(target, title, value, icon) {
+ return `
+
+
+ ${icon}
+ ${title}
+
+
+ ${value}
+ ${Icons.chevron}
+
+
+ `;
+ }
+
+ function buildSubMenu(type) {
+ let title = '';
+ let content = '';
+
+ if (type === 'quality') {
+ title = 'Quality';
+ content = renderQualityOptions();
+ } else if (type === 'audio') {
+ title = 'Audio Track';
+ content = renderAudioOptions();
+ } else if (type === 'subtitle') {
+ title = 'Subtitles';
+ content = renderSubtitleOptions();
+ } else if (type === 'speed') {
+ title = 'Playback Speed';
+ content = renderSpeedOptions();
+ }
+
+ const html = `
+
+
+ ${content}
+
+ `;
els.settingsPanel.innerHTML = html;
- // Add click handlers
- els.settingsPanel.querySelectorAll('.settings-option').forEach(opt => {
- opt.addEventListener('click', () => {
- const action = opt.dataset.action;
- const value = opt.dataset.value;
+ // Listener para volver atrás
+ els.settingsPanel.querySelector('.settings-back-btn').addEventListener('click', (e) => {
+ e.stopPropagation();
+ _settingsView = 'main';
+ buildSettingsPanel();
+ });
- if (action === 'quality') {
- if (hlsInstance) {
- hlsInstance.currentLevel = parseInt(value);
- buildSettingsPanel();
- }
- } else if (action === 'audio') {
- if (hlsInstance) {
- hlsInstance.audioTrack = parseInt(value);
- buildSettingsPanel();
- }
- } else if (action === 'subtitle') {
- // Lógica para cambiar subtítulos
- const idx = parseInt(value);
- if (els.video && els.video.textTracks) {
- Array.from(els.video.textTracks).forEach((track, i) => {
- // Activamos si el índice coincide, desactivamos si es -1 u otro
- track.mode = (i === idx) ? 'showing' : 'hidden';
- });
- }
-
- // Si usas SubtitlesOctopus (Canvas) para ASS, aquí podrías necesitar lógica extra,
- // pero para la mayoría de los casos web (VTT), cambiar el modo del track es suficiente.
- buildSettingsPanel();
-
- } else if (action === 'speed') {
- if (els.video) {
- els.video.playbackRate = parseFloat(value);
- buildSettingsPanel();
- }
- }
+ // Listeners para opciones CORREGIDO
+ els.settingsPanel.querySelectorAll('.settings-item-option').forEach(opt => {
+ opt.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const val = opt.dataset.value;
+ applySetting(type, val);
});
});
}
+ // Funciones de renderizado de opciones
+ function renderQualityOptions() {
+ if (!hlsInstance) return '';
+ let html = '';
+
+ // Auto option
+ const isAuto = hlsInstance.currentLevel === -1;
+ html += `
+ Auto ${isAuto ? Icons.check : ''}
+
`;
+
+ // Levels desc
+ hlsInstance.levels.forEach((level, i) => {
+ const isSelected = hlsInstance.currentLevel === i;
+ html += `
+ ${level.height}p ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderAudioOptions() {
+ if (!hlsInstance) return '';
+ let html = '';
+ hlsInstance.audioTracks.forEach((track, i) => {
+ const isSelected = hlsInstance.audioTrack === i;
+ const label = track.name || track.lang || `Audio ${i + 1}`;
+ html += `
+ ${label} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderSubtitleOptions() {
+ let html = '';
+ const activeIdx = getActiveSubtitleIndex();
+
+ // Off
+ html += `
+ Off ${activeIdx === -1 ? Icons.check : ''}
+
`;
+
+ _currentSubtitles.forEach((sub, i) => {
+ const isSelected = activeIdx === i;
+ html += `
+ ${sub.label || sub.language} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ function renderSpeedOptions() {
+ const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
+ let html = '';
+ const currentRate = els.video ? els.video.playbackRate : 1;
+
+ speeds.forEach(speed => {
+ const isSelected = Math.abs(currentRate - speed) < 0.1;
+ html += `
+ ${speed === 1 ? 'Normal' : speed + 'x'} ${isSelected ? Icons.check : ''}
+
`;
+ });
+ return html;
+ }
+
+ // Aplicar configuración
+ function applySetting(type, value) {
+ if (type === 'quality') {
+ if (hlsInstance) hlsInstance.currentLevel = parseInt(value);
+ } else if (type === 'audio') {
+ if (hlsInstance) hlsInstance.audioTrack = parseInt(value);
+ } else if (type === 'subtitle') {
+ const idx = parseInt(value);
+ if (els.video && els.video.textTracks) {
+ Array.from(els.video.textTracks).forEach((track, i) => {
+ track.mode = (i === idx) ? 'showing' : 'hidden';
+ });
+ }
+ } else if (type === 'speed') {
+ if (els.video) els.video.playbackRate = parseFloat(value);
+ }
+
+ // Volvemos al menú principal para confirmar visualmente (opcional, estilo YouTube)
+ _settingsView = 'main';
+ buildSettingsPanel();
+ }
+
+ function getActiveSubtitleIndex() {
+ if (!els.video || !els.video.textTracks) return -1;
+ for (let i = 0; i < els.video.textTracks.length; i++) {
+ if (els.video.textTracks[i].mode === 'showing') return i;
+ }
+ return -1;
+ }
+
// Subtitle renderer with libass
async function initSubtitleRenderer() {
if (!window.SubtitlesOctopus || !els.video || !els.subtitlesCanvas) return;
@@ -1246,20 +1344,35 @@ const AnimePlayer = (function() {
function renderSkipMarkers(intervals) {
if (!els.progressContainer || !els.video.duration) return;
- // Remove existing markers
- els.progressContainer.querySelectorAll('.skip-marker').forEach(e => e.remove());
+ els.progressContainer.querySelectorAll('.skip-range, .skip-cut').forEach(e => e.remove());
+
+ const duration = els.video.duration;
intervals.forEach(skip => {
- const el = document.createElement('div');
- el.className = `skip-marker ${skip.type}`;
- const startPct = (skip.startTime / els.video.duration) * 100;
- const endPct = (skip.endTime / els.video.duration) * 100;
- el.style.left = `${startPct}%`;
- el.style.width = `${endPct - startPct}%`;
- els.progressContainer.appendChild(el);
+ const startPct = (skip.startTime / duration) * 100;
+ const endPct = (skip.endTime / duration) * 100;
+
+ const range = document.createElement('div');
+ range.className = `skip-range ${skip.type}`; // 'op' o 'ed'
+ range.style.left = `${startPct}%`;
+ range.style.width = `${endPct - startPct}%`;
+ els.progressContainer.appendChild(range);
+
+ createCut(startPct);
+
+ createCut(endPct);
});
}
+ function createCut(percent) {
+ if (percent < 0.5 || percent > 99.5) return;
+
+ const cut = document.createElement('div');
+ cut.className = 'skip-cut';
+ cut.style.left = `${percent}%`;
+ els.progressContainer.appendChild(cut);
+ }
+
function monitorSkipButton(intervals) {
if (!_skipBtn || !els.video) return;
diff --git a/docker/views/css/anime/player.css b/docker/views/css/anime/player.css
index 42183e0..81736a3 100644
--- a/docker/views/css/anime/player.css
+++ b/docker/views/css/anime/player.css
@@ -1,14 +1,15 @@
:root {
--brand-color: #8b5cf6;
--brand-color-light: #a78bfa;
- --op-color: #fbbf24;
- --ed-color: #38bdf8;
+ --brand-gradient: linear-gradient(90deg, #7c3aed 0%, #a78bfa 100%);
+ --op-color: rgba(251, 191, 36, 0.3); /* Ámbar sutil */
+ --ed-color: rgba(56, 189, 248, 0.3); /* Azul cielo sutil */
+
+ --glass-bg: rgba(10, 10, 10, 0.65);
+ --glass-border: rgba(255, 255, 255, 0.08);
+ --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ --glass-blur: blur(20px);
--player-bg: #000;
- --control-bg: rgba(0, 0, 0, 0.8);
- --control-hover: rgba(255, 255, 255, 0.1);
- --slider-bg: rgba(255, 255, 255, 0.3);
- --slider-buffer: rgba(255, 255, 255, 0.5);
- --slider-played: var(--brand-color);
}
body.stop-scrolling {
@@ -278,6 +279,14 @@ body.stop-scrolling {
pointer-events: none;
}
+.glass-panel {
+ background: rgba(20, 20, 20, 0.75);
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+}
+
/* Skip Overlay Button */
#skip-overlay-btn {
position: absolute;
@@ -328,11 +337,15 @@ body.stop-scrolling {
bottom: 0;
left: 0;
width: 100%;
- padding: 0 20px 20px;
+ padding: 0;
z-index: 60;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
+ background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
}
.player-container.show-cursor .custom-controls {
@@ -341,88 +354,102 @@ body.stop-scrolling {
}
.controls-gradient {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 150px;
- background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, transparent 100%);
- z-index: -1;
+ display: none;
}
/* Progress Bar */
.progress-container {
- width: 100%;
- height: 6px;
- background: var(--slider-bg);
- border-radius: 3px;
+ width: calc(100% - 48px);
+ height: 4px;
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 2px;
cursor: pointer;
- margin-bottom: 12px;
+ margin: 0 auto 10px auto;
position: relative;
- transition: height 0.2s;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ z-index: 70;
}
.progress-container:hover {
- height: 8px;
+ height: 6px;
+ transform: scaleY(1);
}
.progress-buffer {
position: absolute;
height: 100%;
- background: var(--slider-buffer);
- border-radius: 3px;
+ background: rgba(255, 255, 255, 0.25);
+ border-radius: 10px;
pointer-events: none;
+ transition: width 0.2s linear;
}
.progress-played {
+ background: var(--brand-color);
+ box-shadow: 0 0 20px rgba(139, 92, 246, 0.6);
position: absolute;
height: 100%;
- background: var(--slider-played);
- border-radius: 3px;
- pointer-events: none;
+ border-radius: 2px;
}
.progress-handle {
+ width: 14px;
+ height: 14px;
+ background: #fff;
+ border-radius: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%) scale(0);
- width: 16px;
- height: 16px;
- background: white;
- border-radius: 50%;
- pointer-events: none;
- transition: transform 0.2s;
- box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+ transition: transform 0.15s ease;
+ box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.2);
+ z-index: 71;
}
.progress-container:hover .progress-handle {
transform: translate(-50%, -50%) scale(1);
+ box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.2);
}
/* Skip Markers on Progress */
-.skip-marker {
+.skip-range {
position: absolute;
top: 0;
height: 100%;
- background: rgba(255, 255, 255, 0.3);
- border-left: 2px solid rgba(0,0,0,0.3);
- border-right: 2px solid rgba(0,0,0,0.3);
+ z-index: 1;
pointer-events: none;
}
-.skip-marker.op {
- background: var(--op-color);
-}
+.skip-range.op { background: var(--op-color); }
+.skip-range.ed { background: var(--ed-color); }
-.skip-marker.ed {
- background: var(--ed-color);
+.skip-cut {
+ position: absolute;
+ top: -4px;
+ bottom: -4px;
+ width: 3px;
+ background-color: #000;
+ z-index: 5;
+ pointer-events: none;
+ border-radius: 2px;
+ box-shadow: 0 0 2px rgba(0,0,0,1);
}
/* Controls Row */
.controls-row {
+ background: transparent;
+ border: none;
+ box-shadow: none;
+ backdrop-filter: none;
+ -webkit-backdrop-filter: none;
+
+ width: 100%;
+ padding: 5px 24px 20px 24px;
+ margin: 0;
+ border-radius: 0;
+
display: flex;
align-items: center;
- gap: 12px;
+ justify-content: space-between;
}
.controls-left,
@@ -441,25 +468,30 @@ body.stop-scrolling {
.control-btn {
background: transparent;
border: none;
- color: white;
- cursor: pointer;
- padding: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- border-radius: 6px;
+ color: rgba(255, 255, 255, 0.85);
+ padding: 10px;
+ margin: 0 2px;
+ border-radius: 50%;
+ transition: all 0.2s ease;
}
.control-btn:hover {
- background: var(--control-hover);
- transform: scale(1.1);
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+ transform: none;
+ box-shadow: none;
}
.control-btn svg {
- width: 24px;
- height: 24px;
+ width: 26px;
+ height: 26px;
fill: currentColor;
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));
+ stroke: none;
+}
+
+.control-btn:active {
+ transform: translateY(0);
}
.control-btn.play-pause svg {
@@ -467,14 +499,13 @@ body.stop-scrolling {
height: 32px;
}
-/* Time Display */
.time-display {
+ font-family: 'Inter', monospace;
+ font-variant-numeric: tabular-nums; /* Evita que los números "bailen" */
font-size: 0.9rem;
font-weight: 500;
- color: white;
- font-variant-numeric: tabular-nums;
- min-width: 100px;
- text-align: center;
+ opacity: 0.9;
+ margin-left: 16px;
}
/* Volume Control */
@@ -487,30 +518,37 @@ body.stop-scrolling {
.volume-slider-container {
width: 0;
overflow: hidden;
- transition: width 0.3s ease;
+ transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ margin-left: 5px;
}
.volume-control:hover .volume-slider-container {
- width: 80px;
+ width: 100px;
+ margin-left: 10px;
}
.volume-slider {
width: 100%;
height: 4px;
-webkit-appearance: none;
- background: var(--slider-bg);
- border-radius: 2px;
+ background: rgba(255,255,255,0.2);
+ border-radius: 4px;
outline: none;
- cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
- background: white;
+ background: #fff;
border-radius: 50%;
cursor: pointer;
+ box-shadow: 0 0 10px rgba(255,255,255,0.5);
+ transition: transform 0.1s;
+}
+
+.volume-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
}
.volume-slider::-moz-range-thumb {
@@ -522,29 +560,145 @@ body.stop-scrolling {
border: none;
}
-/* Settings Panel */
.settings-panel {
position: absolute;
- bottom: 60px;
+ bottom: 80px;
right: 20px;
- background: rgba(20, 20, 20, 0.98);
- border: 1px solid rgba(255,255,255,0.1);
- border-radius: 12px;
- padding: 12px;
- min-width: 240px;
- backdrop-filter: blur(20px);
+ width: 300px;
+ max-height: 400px;
+ border-radius: 16px;
+ padding: 0;
+ overflow: hidden;
opacity: 0;
- transform: translateY(10px);
+ transform: translateY(20px) scale(0.95);
pointer-events: none;
- transition: opacity 0.2s, transform 0.2s;
+ transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
+ z-index: 100;
+
+ background: rgba(15, 15, 15, 0.85);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.08);
}
.settings-panel.active {
opacity: 1;
- transform: translateY(0);
+ transform: translateY(0) scale(1);
pointer-events: auto;
}
+.settings-back-btn {
+ background: transparent;
+ border: none;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 4px;
+ border-radius: 50%;
+ transition: background 0.2s;
+}
+
+.settings-back-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.settings-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 12px 16px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ margin-bottom: 4px;
+}
+
+.settings-title {
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #fff;
+}
+
+/* Lista de opciones */
+.settings-content {
+ padding: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+/* Item del menú */
+.settings-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ cursor: pointer;
+ border-radius: 8px;
+ transition: background 0.2s;
+ color: #eee;
+ font-size: 0.9rem;
+ margin: 0 4px;
+}
+
+.settings-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* Estilos específicos para la vista principal */
+.settings-item-main {
+ justify-content: space-between;
+}
+
+.settings-label-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 500;
+}
+
+.settings-label-icon {
+ opacity: 0.7;
+}
+
+.settings-value-right {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 0.85rem;
+}
+
+/* Item activo (seleccionado en submenú) */
+.settings-item.selected {
+ color: var(--brand-color-light);
+}
+
+.settings-item .check-icon {
+ width: 16px;
+ height: 16px;
+ opacity: 0;
+ margin-left: 10px;
+}
+
+.settings-item.selected .check-icon {
+ opacity: 1;
+}
+
+/* Scrollbar bonito para listas largas */
+.settings-content {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.settings-content::-webkit-scrollbar {
+ width: 4px;
+}
+.settings-content::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 4px;
+}
+
.settings-section {
margin-bottom: 16px;
}