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.
This commit is contained in:
Yiannis Christodoulou 2025-10-11 01:35:38 +03:00
parent ab96f33bf3
commit 8eb196bf74
4 changed files with 562 additions and 71 deletions

View File

@ -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;
}
}

View File

@ -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 {
</svg>`;
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();
}
}

View File

@ -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;

View File

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