From 8eb196bf74cd7615bc7f8c0b263f908db912fe41 Mon Sep 17 00:00:00 2001 From: Yiannis Christodoulou Date: Sat, 11 Oct 2025 01:35:38 +0300 Subject: [PATCH] Improve mobile UX for chapters overlay and add chapters Enhanced the CustomChaptersOverlay component and CSS for a more responsive, touch-friendly mobile experience, including haptic feedback, scroll optimizations, and body scroll locking. Updated SubtitlesButton indicator for better alignment. Added multiple new chapters to the sample video in VideoJSPlayer.jsx for richer navigation. --- .../controls/CustomChaptersOverlay.css | 327 ++++++++++++++++-- .../controls/CustomChaptersOverlay.js | 227 ++++++++++-- .../components/controls/SubtitlesButton.css | 4 +- .../components/video-player/VideoJSPlayer.jsx | 75 ++++ 4 files changed, 562 insertions(+), 71 deletions(-) diff --git a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css index 7c03f6b9..30f9cf73 100644 --- a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css +++ b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.css @@ -249,67 +249,332 @@ background: transparent; } -/* Improve touch scrolling on mobile */ +/* Mobile-first responsive design */ @media (max-width: 767px) { + .custom-chapters-overlay { + background: rgba(0, 0, 0, 0.5) !important; + } + + .video-chapter { + right: 4px !important; + left: 4px !important; + width: calc(100% - 8px) !important; + max-width: none !important; + height: calc(100% - 50px) !important; + bottom: 45px !important; + border-radius: 10px !important; + } + .chapter-body { -webkit-overflow-scrolling: touch; touch-action: pan-y; overscroll-behavior: contain; - height: calc(100% - 70px); + height: calc(100% - 55px); + scroll-behavior: smooth; } .chapter-body::-webkit-scrollbar { width: 0px; - } - - div.chapter-close button { - width: 30px; - height: 30px; - } - - .video-js-root-main .video-js.video-js-rounded-corners .custom-chapters-overlay { - border-bottom-left-radius: 12px !important; - border-bottom-right-radius: 12px !important; - } - - .custom-chapters-overlay .video-chapter { - right: 10px; - left: auto; - width: 100%; - max-width: calc(300px - 20px); - height: calc(100% - 40px); - max-height: calc(100% - 40px); - overflow: hidden; - bottom: 40px; + background: transparent; } .chapter-head { - padding: 10px 15px; + padding: 8px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + } + + .chapter-close button { + width: 32px; + height: 32px; + border-radius: 6px; + transition: background-color 0.2s ease; + } + + .chapter-close button:active { + background: rgba(255, 255, 255, 0.2); + transform: scale(0.95); } .chapter-title h3 a { - font-size: 15px !important; - line-height: 20px !important; - height: 20px !important; + font-size: 14px !important; + line-height: 18px !important; + height: auto !important; + font-weight: 600 !important; } .chapter-title p { font-size: 11px !important; line-height: 14px !important; + margin-top: 1px !important; + opacity: 0.8; + } + + .playlist-items { + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + } + + .playlist-items:last-child { + border-bottom: none; } .playlist-items a { - padding: 10px 16px !important; - min-height: 58px !important; + padding: 10px 12px !important; + min-height: 52px !important; + gap: 10px !important; + transition: background-color 0.2s ease; + border-radius: 0; + } + + .playlist-items a:active { + background: rgba(255, 255, 255, 0.12) !important; + transform: scale(0.98); + } + + .playlist-items.selected a { + background: rgba(255, 255, 255, 0.16) !important; + } + + .playlist-drag-handle { + width: 24px; + font-size: 12px; + font-weight: 600; + color: #fff; + } + + .thumbnail-meta { + flex: 1; + min-width: 0; } .thumbnail-meta h4 { font-size: 13px !important; - line-height: 18px !important; + line-height: 17px !important; + font-weight: 500 !important; + margin-bottom: 3px !important; + -webkit-line-clamp: 2; + line-clamp: 2; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + max-height: 34px; + } + + .thumbnail-meta .meta-sub { + gap: 4px; } .thumbnail-meta .meta-sub .meta-dynamic { font-size: 11px !important; - line-height: 16px !important; + line-height: 14px !important; + color: #bdbdbd; + font-weight: 400; + } + + .thumbnail-action { + display: none; /* Hide action buttons on mobile for cleaner look */ + } +} + +/* Extra small screens (phones in portrait) - Ultra compact */ +@media (max-width: 480px) { + .video-chapter { + right: 2px !important; + left: 2px !important; + width: calc(100% - 4px) !important; + height: calc(100% - 40px) !important; + bottom: 35px !important; + border-radius: 8px !important; + } + + .chapter-head { + padding: 6px 10px; + } + + .chapter-body { + height: calc(100% - 45px); + } + + .chapter-close button { + width: 28px; + height: 28px; + } + + .chapter-title h3 a { + font-size: 13px !important; + line-height: 16px !important; + } + + .chapter-title p { + font-size: 10px !important; + line-height: 13px !important; + } + + .playlist-items a { + padding: 8px 10px !important; + min-height: 44px !important; + gap: 8px !important; + } + + .playlist-drag-handle { + width: 20px; + font-size: 11px; + } + + .thumbnail-meta h4 { + font-size: 12px !important; + line-height: 15px !important; + margin-bottom: 2px !important; + max-height: 30px; + } + + .thumbnail-meta .meta-sub { + gap: 3px; + } + + .thumbnail-meta .meta-sub .meta-dynamic { + font-size: 10px !important; + line-height: 13px !important; + } +} + +/* Very small screens (< 360px) - Maximum compactness */ +@media (max-width: 360px) { + .video-chapter { + right: 1px !important; + left: 1px !important; + width: calc(100% - 2px) !important; + height: calc(100% - 35px) !important; + bottom: 30px !important; + border-radius: 6px !important; + } + + .chapter-head { + padding: 5px 8px; + } + + .chapter-body { + height: calc(100% - 40px); + } + + .chapter-close button { + width: 26px; + height: 26px; + } + + .chapter-title h3 a { + font-size: 12px !important; + line-height: 15px !important; + } + + .chapter-title p { + font-size: 9px !important; + line-height: 12px !important; + } + + .playlist-items a { + padding: 6px 8px !important; + min-height: 40px !important; + gap: 6px !important; + } + + .playlist-drag-handle { + width: 18px; + font-size: 10px; + } + + .thumbnail-meta h4 { + font-size: 11px !important; + line-height: 14px !important; + margin-bottom: 1px !important; + max-height: 28px; + -webkit-line-clamp: 2; + } + + .thumbnail-meta .meta-sub { + gap: 2px; + } + + .thumbnail-meta .meta-sub .meta-dynamic { + font-size: 9px !important; + line-height: 12px !important; + } +} + +/* Landscape orientation on mobile - Compact for limited height */ +@media (max-width: 767px) and (orientation: landscape) { + .video-chapter { + height: calc(100% - 30px) !important; + bottom: 25px !important; + max-height: 350px; + right: 2px !important; + left: 2px !important; + width: calc(100% - 4px) !important; + } + + .chapter-body { + height: calc(100% - 45px); + } + + .chapter-head { + padding: 6px 12px; + } + + .chapter-close button { + width: 28px; + height: 28px; + } + + .chapter-title h3 a { + font-size: 13px !important; + line-height: 16px !important; + } + + .chapter-title p { + font-size: 10px !important; + line-height: 13px !important; + } + + .playlist-items a { + padding: 7px 12px !important; + min-height: 42px !important; + gap: 8px !important; + } + + .thumbnail-meta h4 { + font-size: 12px !important; + line-height: 15px !important; + margin-bottom: 2px !important; + max-height: 30px; + } + + .thumbnail-meta .meta-sub .meta-dynamic { + font-size: 10px !important; + line-height: 13px !important; + } + + .playlist-drag-handle { + width: 20px; + font-size: 11px; + } +} + +/* Touch-friendly improvements for all mobile devices */ +@media (hover: none) and (pointer: coarse) { + .playlist-items a { + -webkit-tap-highlight-color: transparent; + user-select: none; + -webkit-user-select: none; + } + + .chapter-close button { + -webkit-tap-highlight-color: transparent; + } + + /* Ensure smooth scrolling on touch devices */ + .chapter-body { + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + overscroll-behavior-y: contain; } } diff --git a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js index 9f73cbb2..31aad2df 100644 --- a/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js +++ b/frontend-tools/video-js/src/components/controls/CustomChaptersOverlay.js @@ -16,6 +16,10 @@ class CustomChaptersOverlay extends Component { this.channelName = options.channelName || ''; this.thumbnail = options.thumbnail || ''; this.isScrolling = false; + this.isMobile = this.detectMobile(); + this.touchStartTime = 0; + this.touchThreshold = 150; // ms for tap vs scroll detection + this.isSmallScreen = window.innerWidth <= 480; // Bind methods this.createOverlay = this.createOverlay.bind(this); @@ -23,14 +27,60 @@ class CustomChaptersOverlay extends Component { this.toggleOverlay = this.toggleOverlay.bind(this); this.formatTime = this.formatTime.bind(this); this.getChapterTimeRange = this.getChapterTimeRange.bind(this); + this.detectMobile = this.detectMobile.bind(this); + this.handleMobileInteraction = this.handleMobileInteraction.bind(this); + this.setupResizeListener = this.setupResizeListener.bind(this); + this.handleResize = this.handleResize.bind(this); // Initialize after player is ready this.player().ready(() => { this.createOverlay(); this.setupChaptersButton(); + 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(event, chapter, index) { + if (!this.isMobile) return; + + event.preventDefault(); + + // Add haptic feedback if available + if (navigator.vibrate) { + navigator.vibrate(50); + } + + // Seek to chapter and close overlay + this.player().currentTime(chapter.startTime); + this.overlay.style.display = 'none'; + this.updateActiveItem(index); + + const el = this.player().el(); + if (el) el.classList.remove('chapters-open'); + } + + setupResizeListener() { + this.handleResize = () => { + this.isSmallScreen = window.innerWidth <= 480; + }; + + window.addEventListener('resize', this.handleResize); + window.addEventListener('orientationchange', this.handleResize); + } + + handleResize() { + // Update small screen detection on resize/orientation change + this.isSmallScreen = window.innerWidth <= 480; + } + formatTime(seconds) { const totalSec = Math.max(0, Math.floor(seconds)); const hh = Math.floor(totalSec / 3600); @@ -125,7 +175,25 @@ class CustomChaptersOverlay extends Component { -webkit-overflow-scrolling: touch; touch-action: pan-y; overscroll-behavior: contain; + scroll-behavior: smooth; `; + + // Add mobile-specific scroll optimization + if (this.isMobile) { + body.style.cssText += ` + scroll-snap-type: y proximity; + overscroll-behavior-y: contain; + `; + + // For very small screens, add momentum scrolling optimization + if (this.isSmallScreen) { + body.style.cssText += ` + scroll-padding-top: 5px; + scroll-padding-bottom: 5px; + `; + } + } + container.appendChild(body); const list = document.createElement('ul'); @@ -180,46 +248,81 @@ class CustomChaptersOverlay extends Component { `; action.appendChild(btn); - // Handle click and touch events properly - const seekFn = (e) => { - // Prevent default only for navigation, not scrolling - if (e.type === 'click' || (e.type === 'touchend' && !this.isScrolling)) { + // Enhanced mobile touch handling + if (this.isMobile) { + let touchStartY = 0; + let touchStartTime = 0; + let touchMoved = false; + + item.addEventListener( + 'touchstart', + (e) => { + touchStartY = e.touches[0].clientY; + touchStartTime = Date.now(); + touchMoved = false; + this.isScrolling = false; + + // Add visual feedback + item.style.transform = 'scale(0.98)'; + item.style.transition = 'transform 0.1s ease'; + }, + { passive: true } + ); + + item.addEventListener( + 'touchmove', + (e) => { + const touchMoveY = e.touches[0].clientY; + const deltaY = Math.abs(touchMoveY - touchStartY); + // Use smaller threshold for very small screens to be more sensitive + const scrollThreshold = this.isSmallScreen ? 5 : 8; + + if (deltaY > scrollThreshold) { + touchMoved = true; + this.isScrolling = true; + // Remove visual feedback when scrolling + item.style.transform = ''; + } + }, + { passive: true } + ); + + item.addEventListener( + 'touchend', + (e) => { + const touchEndTime = Date.now(); + const touchDuration = touchEndTime - touchStartTime; + + // Reset visual feedback + item.style.transform = ''; + + // Only trigger if it's a quick tap (not a scroll) + // Use shorter threshold for small screens to feel more responsive + const tapThreshold = this.isSmallScreen ? 120 : this.touchThreshold; + if (!touchMoved && touchDuration < tapThreshold) { + this.handleMobileInteraction(e, chapter, index); + } + }, + { passive: false } + ); + + item.addEventListener( + 'touchcancel', + () => { + // Reset visual feedback on cancel + item.style.transform = ''; + }, + { passive: true } + ); + } else { + // Desktop click handling + item.addEventListener('click', (e) => { e.preventDefault(); this.player().currentTime(chapter.startTime); this.overlay.style.display = 'none'; this.updateActiveItem(index); - } - }; - - // Track scrolling state for touch devices - let touchStartY = 0; - let touchStartTime = 0; - - item.addEventListener( - 'touchstart', - (e) => { - touchStartY = e.touches[0].clientY; - touchStartTime = Date.now(); - this.isScrolling = false; - }, - { passive: true } - ); - - item.addEventListener( - 'touchmove', - (e) => { - const touchMoveY = e.touches[0].clientY; - const deltaY = Math.abs(touchMoveY - touchStartY); - // If user moved more than 10px vertically, consider it scrolling - if (deltaY > 10) { - this.isScrolling = true; - } - }, - { passive: true } - ); - - item.addEventListener('touchend', seekFn, { passive: false }); - item.addEventListener('click', seekFn); + }); + } anchor.appendChild(drag); anchor.appendChild(meta); @@ -238,8 +341,17 @@ class CustomChaptersOverlay extends Component { const chaptersButton = this.player().getChild('controlBar').getChild('chaptersButton'); if (chaptersButton) { chaptersButton.off('click'); - chaptersButton.on('click', this.toggleOverlay); - chaptersButton.on('touchstart', this.toggleOverlay); // mobile support + chaptersButton.off('touchstart'); + + if (this.isMobile) { + // Enhanced mobile button handling + chaptersButton.on('touchstart', (e) => { + e.preventDefault(); + this.toggleOverlay(); + }); + } else { + chaptersButton.on('click', this.toggleOverlay); + } } } @@ -252,6 +364,24 @@ class CustomChaptersOverlay extends Component { this.overlay.style.display = isHidden ? 'block' : 'none'; if (el) el.classList.toggle('chapters-open', isHidden); + // Add haptic feedback on mobile when opening + if (this.isMobile && isHidden && navigator.vibrate) { + navigator.vibrate(30); + } + + // Prevent body scroll on mobile when overlay is open + if (this.isMobile) { + if (isHidden) { + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + } else { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } + } + try { this.player() .el() @@ -336,6 +466,13 @@ class CustomChaptersOverlay extends Component { this.overlay.style.display = 'none'; const el = this.player().el(); if (el) el.classList.remove('chapters-open'); + + // Restore body scroll on mobile + if (this.isMobile) { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } } } @@ -345,6 +482,20 @@ class CustomChaptersOverlay extends Component { } const el = this.player().el(); if (el) el.classList.remove('chapters-open'); + + // Restore body scroll on mobile when disposing + if (this.isMobile) { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + } + + // Clean up event listeners + if (this.handleResize) { + window.removeEventListener('resize', this.handleResize); + window.removeEventListener('orientationchange', this.handleResize); + } + super.dispose(); } } diff --git a/frontend-tools/video-js/src/components/controls/SubtitlesButton.css b/frontend-tools/video-js/src/components/controls/SubtitlesButton.css index 0868a4e7..982c5e6d 100644 --- a/frontend-tools/video-js/src/components/controls/SubtitlesButton.css +++ b/frontend-tools/video-js/src/components/controls/SubtitlesButton.css @@ -63,7 +63,7 @@ position: absolute; left: 50%; transform: translateX(-50%); - bottom: 6px; + bottom: 3px; height: 3px; background: #e1002d; border-radius: 2px; @@ -77,7 +77,7 @@ } .video-js .vjs-subs-active button.vjs-subtitles-button::before { - width: 24px; + width: 20px; transition: none !important; animation: none !important; -webkit-transition: none !important; diff --git a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx index b03836ee..004a3fe5 100644 --- a/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx +++ b/frontend-tools/video-js/src/components/video-player/VideoJSPlayer.jsx @@ -265,6 +265,81 @@ function VideoJSPlayer({ videoId = 'default-video' }) { endTime: '00:00:06.885', chapterTitle: 'A3 Reef Ecosystems', }, + { + startTime: '00:00:06.885', + endTime: '00:00:09.180', + chapterTitle: 'A4 Coral Formation and Growth', + }, + { + startTime: '00:00:09.180', + endTime: '00:00:11.475', + chapterTitle: 'A5 Tropical Fish Species', + }, + { + startTime: '00:00:11.475', + endTime: '00:00:13.770', + chapterTitle: 'A6 Ocean Current Patterns', + }, + { + startTime: '00:00:13.770', + endTime: '00:00:16.065', + chapterTitle: 'A7 Deep Sea Exploration', + }, + { + startTime: '00:00:16.065', + endTime: '00:00:18.360', + chapterTitle: 'A8 Marine Conservation Efforts', + }, + { + startTime: '00:00:18.360', + endTime: '00:00:20.655', + chapterTitle: 'A9 Underwater Photography Techniques', + }, + { + startTime: '00:00:20.655', + endTime: '00:00:22.950', + chapterTitle: 'A10 Plankton and Microscopic Life', + }, + { + startTime: '00:00:22.950', + endTime: '00:00:25.245', + chapterTitle: 'A11 Whale Migration Routes', + }, + { + startTime: '00:00:25.245', + endTime: '00:00:27.540', + chapterTitle: 'A12 Tidal Pool Ecosystems', + }, + { + startTime: '00:00:27.540', + endTime: '00:00:29.835', + chapterTitle: 'A13 Submarine Technology', + }, + { + startTime: '00:00:29.835', + endTime: '00:00:32.130', + chapterTitle: 'A14 Ocean Pollution Impact', + }, + { + startTime: '00:00:32.130', + endTime: '00:00:34.425', + chapterTitle: 'A15 Bioluminescent Creatures', + }, + { + startTime: '00:00:34.425', + endTime: '00:00:36.720', + chapterTitle: 'A16 Seaweed and Kelp Forests', + }, + { + startTime: '00:00:36.720', + endTime: '00:00:39.015', + chapterTitle: 'A17 Marine Food Chain Dynamics', + }, + { + startTime: '00:00:39.015', + endTime: '00:00:41.310', + chapterTitle: 'A18 Coastal Erosion and Climate Change', + }, ], related_media: [ {