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 += `
- Auto - - - -
`; - - 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 += `
- ${label} - - - -
`; - }); - 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 += `
- Off - - - -
`; - - // 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 += `
- ${speed}x - - - -
`; + // 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 = ` +
+ + ${title} +
+
+ ${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 += `
- Auto - - - -
`; - - 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 += `
- ${label} - - - -
`; - }); - 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 += `
- Off - - - -
`; - - // 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 += `
- ${speed}x - - - -
`; + // 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 = ` +
+ + ${title} +
+
+ ${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; }