diff --git a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js index 034ac015..09c84576 100644 --- a/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js +++ b/frontend-tools/video-js/src/components/overlays/EndScreenOverlay.js @@ -45,7 +45,7 @@ class EndScreenOverlay extends Component { } createGrid() { - const { columns, maxVideos, useSwiper } = this.getGridConfig(); + const { columns, maxVideos, useSwiper, itemsPerView, gridRows } = this.getGridConfig(); // Get videos to show - access directly from options during createEl const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || []; @@ -55,7 +55,7 @@ class EndScreenOverlay extends Component { : this.createSampleVideos().slice(0, maxVideos); if (useSwiper) { - return this.createSwiperGrid(videosToShow); + return this.createSwiperGrid(videosToShow, itemsPerView || 2, columns, gridRows || 1); } else { return this.createRegularGrid(columns, videosToShow); } @@ -91,14 +91,14 @@ class EndScreenOverlay extends Component { return grid; } - createSwiperGrid(videosToShow) { + createSwiperGrid(videosToShow, itemsPerView = 2, columns = 2, gridRows = 1) { const container = videojs.dom.createEl('div', { className: 'vjs-related-videos-swiper-container', }); // Container styling - ensure it stays within bounds container.style.position = 'relative'; - container.style.padding = '20px'; + container.style.padding = gridRows > 1 ? '12px' : '20px'; // Minimal padding for 2x2 grid container.style.height = '100%'; container.style.width = '100%'; container.style.display = 'flex'; @@ -111,16 +111,32 @@ class EndScreenOverlay extends Component { className: 'vjs-related-videos-swiper', }); - swiperWrapper.style.display = 'flex'; - swiperWrapper.style.overflowX = 'auto'; - swiperWrapper.style.overflowY = 'hidden'; - swiperWrapper.style.gap = '12px'; - swiperWrapper.style.paddingBottom = '10px'; - swiperWrapper.style.scrollBehavior = 'smooth'; - swiperWrapper.style.scrollSnapType = 'x mandatory'; - swiperWrapper.style.width = '100%'; - swiperWrapper.style.maxWidth = '100%'; - swiperWrapper.style.boxSizing = 'border-box'; + if (gridRows > 1) { + // Multi-row grid layout (e.g., 2x2 for landscape) + swiperWrapper.style.display = 'flex'; + swiperWrapper.style.overflowX = 'auto'; + swiperWrapper.style.overflowY = 'hidden'; + swiperWrapper.style.scrollBehavior = 'smooth'; + swiperWrapper.style.scrollSnapType = 'x mandatory'; + swiperWrapper.style.width = '100%'; + swiperWrapper.style.maxWidth = '100%'; + swiperWrapper.style.height = '100%'; + swiperWrapper.style.flex = '1'; + swiperWrapper.style.boxSizing = 'border-box'; + swiperWrapper.style.gap = '0'; // Remove gap, we'll handle it in pages + } else { + // Single row layout (original swiper) + swiperWrapper.style.display = 'flex'; + swiperWrapper.style.overflowX = 'auto'; + swiperWrapper.style.overflowY = 'hidden'; + swiperWrapper.style.gap = '12px'; + swiperWrapper.style.paddingBottom = '10px'; + swiperWrapper.style.scrollBehavior = 'smooth'; + swiperWrapper.style.scrollSnapType = 'x mandatory'; + swiperWrapper.style.width = '100%'; + swiperWrapper.style.maxWidth = '100%'; + swiperWrapper.style.boxSizing = 'border-box'; + } // Hide scrollbar and prevent scroll propagation swiperWrapper.style.scrollbarWidth = 'none'; // Firefox @@ -158,17 +174,56 @@ class EndScreenOverlay extends Component { { passive: true } ); - // Create video items for swiper (show 2 at a time, but allow scrolling through all) - videosToShow.forEach((video) => { - const videoItem = this.createVideoItem(video, true); // Pass true for swiper mode - swiperWrapper.appendChild(videoItem); - }); + if (gridRows > 1) { + // Create pages with grid layout (e.g., 2x2 grid per page) + const itemsPerPage = itemsPerView; + const totalPages = Math.ceil(videosToShow.length / itemsPerPage); + + for (let pageIndex = 0; pageIndex < totalPages; pageIndex++) { + const page = videojs.dom.createEl('div', { + className: 'vjs-swiper-page', + }); + + page.style.minWidth = '100%'; + page.style.width = '100%'; + page.style.height = '100%'; + page.style.display = 'grid'; + page.style.gridTemplateColumns = `repeat(${columns}, 1fr)`; + page.style.gridTemplateRows = `repeat(${gridRows}, 1fr)`; + page.style.gap = '8px'; + page.style.scrollSnapAlign = 'start'; + page.style.boxSizing = 'border-box'; + page.style.alignContent = 'stretch'; + page.style.justifyContent = 'stretch'; + page.style.alignItems = 'stretch'; + page.style.justifyItems = 'stretch'; + + // Get videos for this page + const startIndex = pageIndex * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, videosToShow.length); + const pageVideos = videosToShow.slice(startIndex, endIndex); + + // Create video items for this page + pageVideos.forEach((video) => { + const videoItem = this.createVideoItem(video, true, itemsPerView, true); // Pass true for grid mode + page.appendChild(videoItem); + }); + + swiperWrapper.appendChild(page); + } + } else { + // Single row - create video items directly + videosToShow.forEach((video) => { + const videoItem = this.createVideoItem(video, true, itemsPerView, false); + swiperWrapper.appendChild(videoItem); + }); + } container.appendChild(swiperWrapper); - // Add navigation indicators if there are more than 2 videos - if (videosToShow.length > 2) { - const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper); + // Add navigation indicators if there are more videos than can fit in one view + if (videosToShow.length > itemsPerView) { + const indicators = this.createSwiperIndicators(videosToShow.length, swiperWrapper, itemsPerView); container.appendChild(indicators); } @@ -190,10 +245,41 @@ class EndScreenOverlay extends Component { // Calculate maximum rows that can fit - be more aggressive const maxRows = Math.max(2, Math.floor((availableHeight + gap) / (cardHeight + gap))); - console.log('Grid Config:', { playerWidth, playerHeight, availableHeight, maxRows }); + // Detect landscape orientation on mobile + // Check screen/window orientation first, then player dimensions + const screenWidth = window.innerWidth || document.documentElement.clientWidth; + const screenHeight = window.innerHeight || document.documentElement.clientHeight; + const isScreenLandscape = screenWidth > screenHeight; + + const isLandscape = playerWidth > playerHeight; + const isMobile = playerWidth < 700; + + // For mobile, prioritize screen orientation over player dimensions + // Only consider it landscape if BOTH screen and player are in landscape + const isDefinitelyLandscape = isMobile ? isScreenLandscape && isLandscape : isLandscape; + + console.log('Grid Config:', { + screenWidth, + screenHeight, + isScreenLandscape, + playerWidth, + playerHeight, + availableHeight, + maxRows, + isLandscape, + isDefinitelyLandscape, + isMobile, + }); // Enhanced grid configuration to fill all available space - if (playerWidth >= 1600) { + // Check mobile conditions first to prevent being caught by desktop breakpoints + if (isMobile && isDefinitelyLandscape) { + // Mobile landscape: show 2x2 grid (4 items total) with swiper for pagination + return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 4, gridRows: 2 }; + } else if (isMobile) { + // Mobile portrait: show 2 items in single row swiper mode + return { columns: 2, maxVideos: 12, useSwiper: true, itemsPerView: 2, gridRows: 1 }; + } else if (playerWidth >= 1600) { const columns = 5; return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows } else if (playerWidth >= 1200) { @@ -202,11 +288,9 @@ class EndScreenOverlay extends Component { } else if (playerWidth >= 900) { const columns = 3; return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows - } else if (playerWidth >= 700) { + } else { const columns = 2; return { columns, maxVideos: columns * maxRows, useSwiper: false }; // Fill all available rows - } else { - return { columns: 2, maxVideos: 12, useSwiper: true }; // Use swiper for small screens } } @@ -220,7 +304,7 @@ class EndScreenOverlay extends Component { return this.createSampleVideos().slice(0, maxVideos); } - createVideoItem(video, isSwiperMode = false) { + createVideoItem(video, isSwiperMode = false, itemsPerView = 2, isGridMode = false) { const item = videojs.dom.createEl('div', { className: `vjs-related-video-item ${isSwiperMode ? 'vjs-swiper-item' : ''}`, }); @@ -228,7 +312,7 @@ class EndScreenOverlay extends Component { // Consistent item styling with fixed dimensions item.style.position = 'relative'; item.style.backgroundColor = '#1a1a1a'; - item.style.borderRadius = '6px'; + item.style.borderRadius = '8px'; item.style.overflow = 'hidden'; item.style.cursor = 'pointer'; item.style.transition = 'transform 0.15s ease, box-shadow 0.15s ease'; @@ -236,11 +320,21 @@ class EndScreenOverlay extends Component { item.style.flexDirection = 'column'; // Consistent dimensions for all cards - if (isSwiperMode) { - // Calculate proper width for swiper items (2 items visible + gap) - item.style.minWidth = 'calc(50% - 6px)'; // 50% width minus half the gap - item.style.width = 'calc(50% - 6px)'; - item.style.maxWidth = '180px'; // Maximum width for larger screens + if (isGridMode) { + // Grid mode (2x2): items fill their grid cell completely + item.style.height = '100%'; + item.style.minHeight = '0'; + item.style.width = '100%'; + item.style.maxWidth = 'none'; + item.style.flex = '1'; + } else if (isSwiperMode) { + // Single row swiper mode: calculate width based on items per view + // Formula: (100% / itemsPerView) - (gap * (itemsPerView - 1) / itemsPerView) + const itemsPerRow = itemsPerView / (itemsPerView === 4 ? 2 : 1); // For 4 items in 2 rows, show 2 per row + const gapAdjustment = (12 * (itemsPerRow - 1)) / itemsPerRow; + item.style.minWidth = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`; + item.style.width = `calc(${100 / itemsPerRow}% - ${gapAdjustment}px)`; + item.style.maxWidth = itemsPerView === 4 ? '150px' : '180px'; // Smaller max width for 4 items // Simpler height since text is overlaid on thumbnail const cardHeight = '120px'; // Just the thumbnail height @@ -270,7 +364,7 @@ class EndScreenOverlay extends Component { } // Create thumbnail container with overlaid text - const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode); + const thumbnailContainer = this.createThumbnailWithOverlay(video, isSwiperMode, itemsPerView); item.appendChild(thumbnailContainer); console.log('Created video item with overlay:', item); @@ -331,7 +425,7 @@ class EndScreenOverlay extends Component { return container; } - createThumbnailWithOverlay(video, isSwiperMode = false) { + createThumbnailWithOverlay(video, isSwiperMode = false, itemsPerView = 2) { const container = videojs.dom.createEl('div', { className: 'vjs-related-video-thumbnail-container', }); @@ -339,9 +433,13 @@ class EndScreenOverlay extends Component { // Container styling - full height since it contains everything container.style.position = 'relative'; container.style.width = '100%'; - container.style.height = '120px'; + container.style.height = '100%'; + container.style.minHeight = '120px'; container.style.overflow = 'hidden'; - container.style.borderRadius = '6px'; + container.style.borderRadius = '8px'; + container.style.flex = '1'; + container.style.display = 'flex'; + container.style.flexDirection = 'column'; // Create thumbnail image const thumbnail = videojs.dom.createEl('img', { @@ -354,6 +452,9 @@ class EndScreenOverlay extends Component { thumbnail.style.height = '100%'; thumbnail.style.objectFit = 'cover'; thumbnail.style.display = 'block'; + thumbnail.style.flex = '1'; + thumbnail.style.minWidth = '0'; + thumbnail.style.minHeight = '0'; container.appendChild(thumbnail); @@ -370,7 +471,7 @@ class EndScreenOverlay extends Component { duration.style.color = 'white'; duration.style.padding = '2px 6px'; duration.style.borderRadius = '3px'; - duration.style.fontSize = '11px'; + duration.style.fontSize = itemsPerView === 4 ? '10px' : '11px'; duration.style.fontWeight = '600'; duration.style.lineHeight = '1'; duration.style.zIndex = '3'; @@ -384,12 +485,12 @@ class EndScreenOverlay extends Component { }); textOverlay.style.position = 'absolute'; - textOverlay.style.top = '8px'; - textOverlay.style.left = '8px'; - textOverlay.style.right = '8px'; + textOverlay.style.top = itemsPerView === 4 ? '6px' : '8px'; + textOverlay.style.left = itemsPerView === 4 ? '6px' : '8px'; + textOverlay.style.right = itemsPerView === 4 ? '6px' : '8px'; textOverlay.style.background = 'linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 70%, transparent 100%)'; - textOverlay.style.padding = '8px'; + textOverlay.style.padding = itemsPerView === 4 ? '6px' : '8px'; textOverlay.style.borderRadius = '4px'; textOverlay.style.zIndex = '2'; @@ -399,10 +500,11 @@ class EndScreenOverlay extends Component { }); title.textContent = video.title || 'Sample Video Title'; title.style.color = '#ffffff'; - title.style.fontSize = isSwiperMode ? '12px' : '13px'; + // Adjust font sizes based on items per view + title.style.fontSize = itemsPerView === 4 ? '11px' : isSwiperMode ? '12px' : '13px'; title.style.fontWeight = '600'; title.style.lineHeight = '1.3'; - title.style.marginBottom = '4px'; + title.style.marginBottom = itemsPerView === 4 ? '3px' : '4px'; title.style.overflow = 'hidden'; title.style.textOverflow = 'ellipsis'; title.style.display = '-webkit-box'; @@ -428,7 +530,8 @@ class EndScreenOverlay extends Component { meta.textContent = metaText; meta.style.color = '#e0e0e0'; - meta.style.fontSize = isSwiperMode ? '10px' : '11px'; + // Adjust font sizes based on items per view + meta.style.fontSize = itemsPerView === 4 ? '9px' : isSwiperMode ? '10px' : '11px'; meta.style.lineHeight = '1.2'; meta.style.overflow = 'hidden'; meta.style.textOverflow = 'ellipsis'; @@ -529,7 +632,7 @@ class EndScreenOverlay extends Component { return info; } - createSwiperIndicators(totalVideos, swiperWrapper) { + createSwiperIndicators(totalVideos, swiperWrapper, itemsPerView = 2) { const indicators = videojs.dom.createEl('div', { className: 'vjs-swiper-indicators', }); @@ -539,7 +642,6 @@ class EndScreenOverlay extends Component { indicators.style.gap = '8px'; indicators.style.marginTop = '10px'; - const itemsPerView = 2; const totalPages = Math.ceil(totalVideos / itemsPerView); for (let i = 0; i < totalPages; i++) {