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: [ {