From f78708753123e5384341fcd67f893890d4f58a2e Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sat, 11 Oct 2025 01:48:26 +0300 Subject: [PATCH] Improve mobile responsiveness for settings and chapters Adds mobile-first responsive CSS for the custom settings menu and chapters overlay, including adaptive sizing, touch-friendly interactions, and prevention of body scroll when overlays are open. Updates CustomSettingsMenu.js to detect mobile devices, handle touch events for menu items, and manage scroll locking and haptic feedback for a better mobile user experience. --- .../controls/CustomChaptersOverlay.css | 20 ++ .../controls/CustomSettingsMenu.css | 310 ++++++++++++++++++ .../components/controls/CustomSettingsMenu.js | 179 ++++++++++ 3 files changed, 509 insertions(+) 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();