diff --git a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css index 30f9cf73..c459c6f6 100644 --- a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css +++ b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css @@ -290,6 +290,11 @@ transition: background-color 0.2s ease; } + .chapter-close button svg { + width: 18px !important; + height: 18px !important; + } + .chapter-close button:active { background: rgba(255, 255, 255, 0.2); transform: scale(0.95); @@ -400,6 +405,11 @@ height: 28px; } + .chapter-close button svg { + width: 16px !important; + height: 16px !important; + } + .chapter-title h3 a { font-size: 13px !important; line-height: 16px !important; @@ -462,6 +472,11 @@ height: 26px; } + .chapter-close button svg { + width: 14px !important; + height: 14px !important; + } + .chapter-title h3 a { font-size: 12px !important; line-height: 15px !important; @@ -525,6 +540,11 @@ height: 28px; } + .chapter-close button svg { + width: 16px !important; + height: 16px !important; + } + .chapter-title h3 a { font-size: 13px !important; line-height: 16px !important; diff --git a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.css b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.css index 34421528..ad06e7de 100644 --- a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.css +++ b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.css @@ -213,3 +213,313 @@ sup.hd-badge { padding: 1px 4px; border-radius: 3px; } + +/* ===== MOBILE RESPONSIVE DESIGN ===== */ + +/* Mobile-first responsive design for tablets and large phones */ +@media (max-width: 767px) { + .custom-settings-overlay { + right: 8px !important; + left: auto !important; + width: 260px !important; + max-width: calc(100vw - 16px) !important; + height: auto !important; + max-height: calc(100vh - 100px) !important; + bottom: 45px !important; + border-radius: 10px !important; + font-size: 13px !important; + } + + .settings-header { + padding: 10px 12px; + font-size: 14px; + font-weight: 600; + } + + .settings-close-btn { + width: 32px !important; + height: 32px !important; + padding: 0 !important; + } + + .settings-close-btn svg { + width: 18px !important; + height: 18px !important; + } + + .settings-item { + padding: 10px 12px; + gap: 8px; + min-height: 48px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + } + + .settings-item:active { + background: rgba(255, 255, 255, 0.12) !important; + transform: scale(0.98); + } + + .settings-left { + gap: 6px; + font-size: 13px; + } + + .settings-right { + font-size: 12px; + opacity: 0.9; + } + + .submenu-header { + padding: 10px 12px; + font-size: 14px; + min-height: 48px; + } + + .submenu-header:active { + background: rgba(255, 255, 255, 0.12) !important; + transform: scale(0.98); + } + + .speed-option, + .quality-option, + .subtitle-option { + padding: 10px 12px; + min-height: 44px; + font-size: 13px; + } + + .speed-option:active, + .quality-option:active, + .subtitle-option:active { + background: rgba(255, 255, 255, 0.12) !important; + transform: scale(0.98); + } + + /* Hide hover effects on touch devices */ + .settings-item:hover, + .speed-option:hover, + .quality-option:hover, + .subtitle-option:hover, + .submenu-header:hover { + background: transparent; + } +} + +/* Small phones (portrait) - Ultra compact */ +@media (max-width: 480px) { + .custom-settings-overlay { + right: 6px !important; + left: auto !important; + width: 240px !important; + max-width: calc(100vw - 12px) !important; + height: auto !important; + max-height: calc(100vh - 80px) !important; + bottom: 35px !important; + border-radius: 8px !important; + font-size: 12px !important; + } + + .settings-header { + padding: 8px 10px; + font-size: 13px; + } + + .settings-close-btn { + width: 28px !important; + height: 28px !important; + } + + .settings-close-btn svg { + width: 16px !important; + height: 16px !important; + } + + .settings-item { + padding: 8px 10px; + gap: 6px; + min-height: 44px; + } + + .settings-left { + gap: 5px; + font-size: 12px; + } + + .settings-right { + font-size: 11px; + } + + .submenu-header { + padding: 8px 10px; + font-size: 13px; + min-height: 44px; + } + + .speed-option, + .quality-option, + .subtitle-option { + padding: 8px 10px; + min-height: 40px; + font-size: 12px; + } + + /* Smaller icons on mobile */ + .settings-item-svg svg, + .submenu-header svg { + width: 20px !important; + height: 20px !important; + } +} + +/* Very small screens (< 360px) - Maximum compactness */ +@media (max-width: 360px) { + .custom-settings-overlay { + right: 4px !important; + left: auto !important; + width: 220px !important; + max-width: calc(100vw - 8px) !important; + height: auto !important; + max-height: calc(100vh - 70px) !important; + bottom: 30px !important; + border-radius: 6px !important; + font-size: 11px !important; + } + + .settings-header { + padding: 6px 8px; + font-size: 12px; + } + + .settings-close-btn { + width: 26px !important; + height: 26px !important; + } + + .settings-close-btn svg { + width: 14px !important; + height: 14px !important; + } + + .settings-item { + padding: 6px 8px; + gap: 4px; + min-height: 40px; + } + + .settings-left { + gap: 4px; + font-size: 11px; + } + + .settings-right { + font-size: 10px; + } + + .submenu-header { + padding: 6px 8px; + font-size: 12px; + min-height: 40px; + } + + .speed-option, + .quality-option, + .subtitle-option { + padding: 6px 8px; + min-height: 36px; + font-size: 11px; + } + + /* Even smaller icons for very small screens */ + .settings-item-svg svg, + .submenu-header svg { + width: 18px !important; + height: 18px !important; + } + + sup.hd-badge { + font-size: 8px; + padding: 0px 3px; + margin-left: 4px; + } +} + +/* Landscape orientation on mobile - Compact for limited height */ +@media (max-width: 767px) and (orientation: landscape) { + .custom-settings-overlay { + height: auto !important; + max-height: calc(100vh - 60px) !important; + bottom: 25px !important; + right: 6px !important; + left: auto !important; + width: 250px !important; + max-width: calc(100vw - 12px) !important; + } + + .settings-header { + padding: 6px 10px; + font-size: 12px; + } + + .settings-close-btn { + width: 28px !important; + height: 28px !important; + } + + .settings-close-btn svg { + width: 16px !important; + height: 16px !important; + } + + .settings-item { + padding: 7px 10px; + min-height: 38px; + } + + .submenu-header { + padding: 7px 10px; + min-height: 38px; + } + + .speed-option, + .quality-option, + .subtitle-option { + padding: 6px 10px; + min-height: 36px; + font-size: 11px; + } +} + +/* Touch-friendly improvements for all mobile devices */ +@media (hover: none) and (pointer: coarse) { + .settings-item, + .speed-option, + .quality-option, + .subtitle-option, + .submenu-header { + -webkit-tap-highlight-color: transparent; + user-select: none; + -webkit-user-select: none; + transition: + background-color 0.2s ease, + transform 0.1s ease; + } + + /* Ensure smooth scrolling on touch devices */ + .custom-settings-overlay, + .speed-submenu, + .quality-submenu, + .subtitles-submenu { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + overscroll-behavior-y: contain; + } + + /* Remove hover states on touch devices */ + .settings-item:hover, + .speed-option:hover, + .quality-option:hover, + .subtitle-option:hover, + .submenu-header:hover { + background: transparent !important; + } +} diff --git a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js index 80999617..409bf0e5 100644 --- a/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js +++ b/frontend-tools/video-js/src/components/controls/CustomSettingsMenu.js @@ -22,6 +22,9 @@ class CustomSettingsMenu extends Component { // Touch scroll detection (mobile) this.isTouchScrolling = false; this.touchStartY = 0; + this.isMobile = this.detectMobile(); + this.isSmallScreen = window.innerWidth <= 480; + this.touchThreshold = 150; // ms for tap vs scroll detection // Bind methods this.createSettingsButton = this.createSettingsButton.bind(this); @@ -35,6 +38,9 @@ class CustomSettingsMenu extends Component { this.refreshSubtitlesSubmenu = this.refreshSubtitlesSubmenu.bind(this); this.handleSubtitleChange = this.handleSubtitleChange.bind(this); this.handleClickOutside = this.handleClickOutside.bind(this); + this.detectMobile = this.detectMobile.bind(this); + this.handleMobileInteraction = this.handleMobileInteraction.bind(this); + this.setupResizeListener = this.setupResizeListener.bind(this); // Initialize after player is ready this.player().ready(() => { @@ -42,9 +48,101 @@ class CustomSettingsMenu extends Component { this.createSettingsOverlay(); this.setupEventListeners(); this.restoreSubtitlePreference(); + this.setupResizeListener(); }); } + detectMobile() { + return ( + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || + (navigator.maxTouchPoints && navigator.maxTouchPoints > 0) || + window.matchMedia('(hover: none) and (pointer: coarse)').matches + ); + } + + handleMobileInteraction(element, callback) { + if (!this.isMobile) return; + + let touchStartTime = 0; + let touchMoved = false; + let touchStartY = 0; + + element.addEventListener( + 'touchstart', + (e) => { + touchStartTime = Date.now(); + touchMoved = false; + touchStartY = e.touches[0].clientY; + + // Add visual feedback + element.style.transform = 'scale(0.98)'; + element.style.transition = 'transform 0.1s ease'; + + // Add haptic feedback if available + if (navigator.vibrate) { + navigator.vibrate(30); + } + }, + { passive: true } + ); + + element.addEventListener( + 'touchmove', + (e) => { + const touchMoveY = e.touches[0].clientY; + const deltaY = Math.abs(touchMoveY - touchStartY); + const scrollThreshold = this.isSmallScreen ? 5 : 8; + + if (deltaY > scrollThreshold) { + touchMoved = true; + // Remove visual feedback when scrolling + element.style.transform = ''; + } + }, + { passive: true } + ); + + element.addEventListener( + 'touchend', + (e) => { + const touchEndTime = Date.now(); + const touchDuration = touchEndTime - touchStartTime; + + // Reset visual feedback + element.style.transform = ''; + + // Only trigger if it's a quick tap (not a scroll) + const tapThreshold = this.isSmallScreen ? 120 : this.touchThreshold; + if (!touchMoved && touchDuration < tapThreshold) { + e.preventDefault(); + callback(e); + } + }, + { passive: false } + ); + + element.addEventListener( + 'touchcancel', + () => { + // Reset visual feedback on cancel + element.style.transform = ''; + }, + { passive: true } + ); + } + + setupResizeListener() { + const handleResize = () => { + this.isSmallScreen = window.innerWidth <= 480; + }; + + window.addEventListener('resize', handleResize); + window.addEventListener('orientationchange', handleResize); + + // Store reference for cleanup + this.resizeHandler = handleResize; + } + createSettingsButton() { const controlBar = this.player().getChild('controlBar'); @@ -236,8 +334,57 @@ class CustomSettingsMenu extends Component { // Create subtitles submenu (YouTube-like) this.createSubtitlesSubmenu(); + // Add mobile-specific optimizations + if (this.isMobile) { + this.settingsOverlay.style.cssText += ` + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + overscroll-behavior-y: contain; + touch-action: pan-y; + `; + + // Prevent body scroll when overlay is open on mobile + this.settingsOverlay.addEventListener( + 'touchstart', + (e) => { + e.stopPropagation(); + }, + { passive: true } + ); + } + // Add to control bar this.player().el().appendChild(this.settingsOverlay); + + // Add mobile touch handling to settings items + if (this.isMobile) { + const settingsItems = this.settingsOverlay.querySelectorAll('.settings-item'); + settingsItems.forEach((item) => { + this.handleMobileInteraction(item, (e) => { + // Handle the same logic as click events + if (e.target.closest('[data-setting="playback-speed"]')) { + this.speedSubmenu.style.display = 'flex'; + this.qualitySubmenu.style.display = 'none'; + if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none'; + } + + if (e.target.closest('[data-setting="quality"]')) { + this.qualitySubmenu.style.display = 'flex'; + this.speedSubmenu.style.display = 'none'; + if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none'; + } + + if (e.target.closest('[data-setting="subtitles"]')) { + this.refreshSubtitlesSubmenu(); + if (this.subtitlesSubmenu) { + this.subtitlesSubmenu.style.display = 'flex'; + this.speedSubmenu.style.display = 'none'; + this.qualitySubmenu.style.display = 'none'; + } + } + }); + }); + } } createSpeedSubmenu() { @@ -812,9 +959,28 @@ class CustomSettingsMenu extends Component { if (isVisible) { this.settingsOverlay.classList.remove('show'); this.settingsOverlay.style.display = 'none'; + + // Restore body scroll on mobile when closing + if (this.isMobile) { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } } else { this.settingsOverlay.classList.add('show'); this.settingsOverlay.style.display = 'block'; + + // Add haptic feedback on mobile when opening + if (this.isMobile && navigator.vibrate) { + navigator.vibrate(30); + } + + // Prevent body scroll on mobile when overlay is open + if (this.isMobile) { + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + } } this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles @@ -1252,6 +1418,19 @@ class CustomSettingsMenu extends Component { // Remove event listeners document.removeEventListener('click', this.handleClickOutside); + // Clean up resize listener + if (this.resizeHandler) { + window.removeEventListener('resize', this.resizeHandler); + window.removeEventListener('orientationchange', this.resizeHandler); + } + + // Restore body scroll on mobile when disposing + if (this.isMobile) { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } + // Remove DOM elements if (this.settingsOverlay) { this.settingsOverlay.remove();