fix: Improve seekbar for mobile devices

This commit is contained in:
Yiannis Christodoulou 2025-09-30 12:33:27 +03:00
parent 1788ed27f4
commit e885cc7c28
5 changed files with 185 additions and 27 deletions

View File

@ -26,9 +26,9 @@ html {
visibility: hidden !important; visibility: hidden !important;
} }
/* Simple fix: Move seekbar up by 10px on touch devices */ /* Simple fix: Move seekbar closer to controls on touch devices */
.video-js .vjs-progress-control { .video-js .vjs-progress-control {
bottom: 56px !important; /* Move up 10px from original 46px */ bottom: 44px !important; /* Much closer to control bar - minimal gap */
} }
/* Make seekbar more touch-friendly on Android */ /* Make seekbar more touch-friendly on Android */
@ -551,9 +551,9 @@ button.vjs-button > .vjs-icon-placeholder:before {
border-radius: 0 !important; border-radius: 0 !important;
} }
/* Move seekbar up by 10px on small mobile to prevent accidental button touches */ /* Move seekbar closer to controls on small mobile */
.video-js .vjs-progress-control { .video-js .vjs-progress-control {
bottom: 56px !important; /* Move up 10px from original 46px */ bottom: 44px !important; /* Much closer to control bar - minimal gap */
} }
.video-container { .video-container {

View File

@ -65,6 +65,39 @@
overflow: hidden !important; overflow: hidden !important;
} }
/* Responsive sizing for mobile devices */
@media (max-width: 768px) {
.vjs-seek-indicator {
top: calc(50% - 30px) !important; /* Move up slightly to avoid seekbar on tablet */
}
.youtube-seek-circle {
width: 60px !important;
height: 60px !important;
}
.youtube-seek-icon svg {
width: 24px !important;
height: 24px !important;
}
}
@media (max-width: 480px) {
.vjs-seek-indicator {
top: calc(50% - 40px) !important; /* Move up more to avoid seekbar on mobile */
}
.youtube-seek-circle {
width: 50px !important;
height: 50px !important;
}
.youtube-seek-icon svg {
width: 20px !important;
height: 20px !important;
}
}
.youtube-seek-icon { .youtube-seek-icon {
display: flex !important; display: flex !important;
align-items: center !important; align-items: center !important;

View File

@ -48,13 +48,32 @@ class SeekIndicator extends Component {
clearTimeout(this.showTimeout); clearTimeout(this.showTimeout);
} }
// Get responsive size based on screen width for all directions
const isMobile = window.innerWidth <= 480;
const isTablet = window.innerWidth <= 768 && window.innerWidth > 480;
let circleSize, iconSize, textSize;
if (isMobile) {
circleSize = '50px';
iconSize = '20';
textSize = '8px';
} else if (isTablet) {
circleSize = '60px';
iconSize = '22';
textSize = '9px';
} else {
circleSize = '80px';
iconSize = '24';
textSize = '10px';
}
// Set content based on direction - YouTube-style circular design // Set content based on direction - YouTube-style circular design
if (direction === 'forward') { if (direction === 'forward') {
iconEl.innerHTML = ` iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;"> <div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style=" <div style="
width: 80px; width: ${circleSize};
height: 80px; height: ${circleSize};
border-radius: 50%; border-radius: 50%;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
display: flex; display: flex;
@ -69,14 +88,14 @@ class SeekIndicator extends Component {
-moz-border-radius: 50%; -moz-border-radius: 50%;
"> ">
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 4px;"> <div style="display: flex; align-items: center; justify-content: center; margin-bottom: 4px;">
<svg viewBox="0 0 24 24" width="24" height="24" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));"> <svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M8 5v14l11-7z"/> <path d="M8 5v14l11-7z"/>
<path d="M13 5v14l11-7z" opacity="0.6"/> <path d="M13 5v14l11-7z" opacity="0.6"/>
</svg> </svg>
</div> </div>
<div style=" <div style="
color: white; color: white;
font-size: 10px; font-size: ${textSize};
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 1.2; line-height: 1.2;
@ -90,8 +109,8 @@ class SeekIndicator extends Component {
iconEl.innerHTML = ` iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;"> <div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style=" <div style="
width: 80px; width: ${circleSize};
height: 80px; height: ${circleSize};
border-radius: 50%; border-radius: 50%;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
display: flex; display: flex;
@ -106,14 +125,14 @@ class SeekIndicator extends Component {
-moz-border-radius: 50%; -moz-border-radius: 50%;
"> ">
<div style="display: flex; align-items: center; justify-content: center; margin-bottom: 4px;"> <div style="display: flex; align-items: center; justify-content: center; margin-bottom: 4px;">
<svg viewBox="0 0 24 24" width="24" height="24" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));"> <svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M16 19V5l-11 7z"/> <path d="M16 19V5l-11 7z"/>
<path d="M11 19V5L0 12z" opacity="0.6"/> <path d="M11 19V5L0 12z" opacity="0.6"/>
</svg> </svg>
</div> </div>
<div style=" <div style="
color: white; color: white;
font-size: 10px; font-size: ${textSize};
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
line-height: 1.2; line-height: 1.2;
@ -127,8 +146,8 @@ class SeekIndicator extends Component {
iconEl.innerHTML = ` iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;"> <div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style=" <div style="
width: 80px; width: ${circleSize};
height: 80px; height: ${circleSize};
border-radius: 50%; border-radius: 50%;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
display: flex; display: flex;
@ -139,19 +158,19 @@ class SeekIndicator extends Component {
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
"> ">
<svg viewBox="0 0 24 24" width="32" height="32" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));"> <svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M8 5v14l11-7z"/> <path d="M8 5v14l11-7z"/>
</svg> </svg>
</div> </div>
</div> </div>
`; `;
textEl.textContent = 'Play'; textEl.textContent = 'Play';
} else if (direction === 'pause') { } else if (direction === 'pause' || direction === 'pause-mobile') {
iconEl.innerHTML = ` iconEl.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;"> <div style="display: flex; align-items: center; justify-content: center; animation: youtubeSeekPulse 0.3s ease-out;">
<div style=" <div style="
width: 80px; width: ${circleSize};
height: 80px; height: ${circleSize};
border-radius: 50%; border-radius: 50%;
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
display: flex; display: flex;
@ -162,7 +181,7 @@ class SeekIndicator extends Component {
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
"> ">
<svg viewBox="0 0 24 24" width="32" height="32" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));"> <svg viewBox="0 0 24 24" width="${iconSize}" height="${iconSize}" fill="white" style="filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/> <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg> </svg>
</div> </div>
@ -193,10 +212,73 @@ class SeekIndicator extends Component {
padding: 0 !important; padding: 0 !important;
`; `;
// Auto-hide after 1 second // Auto-hide timing based on action type
if (direction === 'forward' || direction === 'backward') {
// Seek operations: 1 second
this.showTimeout = setTimeout(() => { this.showTimeout = setTimeout(() => {
this.hide(); this.hide();
}, 1000); }, 1000);
} else if (direction === 'play' || direction === 'pause' || direction === 'pause-mobile') {
// Play/pause operations: 500ms
this.showTimeout = setTimeout(() => {
this.hide();
}, 500);
}
}
/**
* Show pause icon for mobile (uses 500ms from main show method)
*/
showMobilePauseIcon() {
this.show('pause-mobile'); // This will auto-hide after 500ms
// Make the icon clickable for mobile
const el = this.el();
el.style.pointerEvents = 'auto !important';
// Add click handler for the center icon
const handleCenterIconClick = (e) => {
e.preventDefault();
e.stopPropagation();
if (this.player().paused()) {
this.player().play();
} else {
this.player().pause();
}
// Hide immediately after click
this.hide();
};
el.addEventListener('click', handleCenterIconClick);
el.addEventListener('touchend', handleCenterIconClick);
// Store handlers for cleanup
this.mobileClickHandler = handleCenterIconClick;
}
/**
* Hide mobile pause icon and clean up
*/
hideMobileIcon() {
const el = this.el();
// Remove click handlers
const allClickHandlers = el.cloneNode(true);
el.parentNode.replaceChild(allClickHandlers, el);
// Reset pointer events
allClickHandlers.style.pointerEvents = 'none !important';
// Hide the icon
this.hide();
// Clear timeout
if (this.mobileTimeout) {
clearTimeout(this.mobileTimeout);
this.mobileTimeout = null;
}
} }
/** /**
@ -210,6 +292,22 @@ class SeekIndicator extends Component {
el.style.display = 'none'; el.style.display = 'none';
el.style.visibility = 'hidden'; el.style.visibility = 'hidden';
}, 200); // Wait for fade out animation }, 200); // Wait for fade out animation
// Clear any existing timeout
if (this.showTimeout) {
clearTimeout(this.showTimeout);
this.showTimeout = null;
}
// Clean up mobile click handlers if they exist
if (this.mobileClickHandler) {
el.removeEventListener('click', this.mobileClickHandler);
el.removeEventListener('touchend', this.mobileClickHandler);
this.mobileClickHandler = null;
}
// Reset pointer events
el.style.pointerEvents = 'none !important';
} }
/** /**

View File

@ -2102,14 +2102,16 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const touch = e.touches[0]; const touch = e.touches[0];
touchStartPos = { x: touch.clientX, y: touch.clientY }; touchStartPos = { x: touch.clientX, y: touch.clientY };
// Check if touch is in seekbar area // Check if touch is in seekbar area or the zone above it
const progressControl = playerRef.current const progressControl = playerRef.current
.getChild('controlBar') .getChild('controlBar')
?.getChild('progressControl'); ?.getChild('progressControl');
if (progressControl && progressControl.el()) { if (progressControl && progressControl.el()) {
const progressRect = progressControl.el().getBoundingClientRect(); const progressRect = progressControl.el().getBoundingClientRect();
const seekbarDeadZone = 8; // Only 8px above seekbar is protected for easier seeking
const isInSeekbarArea = const isInSeekbarArea =
touch.clientY >= progressRect.top && touch.clientY <= progressRect.bottom; touch.clientY >= progressRect.top - seekbarDeadZone &&
touch.clientY <= progressRect.bottom;
if (isInSeekbarArea) { if (isInSeekbarArea) {
playerRef.current.seekbarTouching = true; playerRef.current.seekbarTouching = true;
} }
@ -2142,15 +2144,40 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
window.getComputedStyle(controlBarEl).opacity !== '0' && window.getComputedStyle(controlBarEl).opacity !== '0' &&
window.getComputedStyle(controlBarEl).visibility !== 'hidden'; window.getComputedStyle(controlBarEl).visibility !== 'hidden';
// Check if center play/pause icon is visible and if tap is on it
const seekIndicator = customComponents.current.seekIndicator;
const seekIndicatorEl = seekIndicator ? seekIndicator.el() : null;
const isSeekIndicatorVisible =
seekIndicatorEl &&
window.getComputedStyle(seekIndicatorEl).opacity !== '0' &&
window.getComputedStyle(seekIndicatorEl).visibility !== 'hidden' &&
window.getComputedStyle(seekIndicatorEl).display !== 'none';
let isTapOnCenterIcon = false;
if (seekIndicatorEl && isSeekIndicatorVisible) {
const iconRect = seekIndicatorEl.getBoundingClientRect();
isTapOnCenterIcon =
touch.clientX >= iconRect.left &&
touch.clientX <= iconRect.right &&
touch.clientY >= iconRect.top &&
touch.clientY <= iconRect.bottom;
}
if (playerRef.current.paused()) { if (playerRef.current.paused()) {
// Always play if video is paused // Always play if video is paused
playerRef.current.play(); playerRef.current.play();
} else if (isTapOnCenterIcon) {
// Pause if tapping on center icon (highest priority)
playerRef.current.pause();
} else if (isControlsVisible) { } else if (isControlsVisible) {
// Only pause if controls are actually visible // Pause if controls are visible and not touching seekbar area
playerRef.current.pause(); playerRef.current.pause();
} else { } else {
// If controls are not visible, just show them (trigger user activity) // If controls are not visible, show them AND show center pause icon
playerRef.current.userActive(true); playerRef.current.userActive(true);
if (seekIndicator) {
seekIndicator.showMobilePauseIcon();
}
} }
} }
} }

View File

@ -106,7 +106,7 @@
} }
#page-embed .video-js-root-embed .video-js .vjs-progress-control { #page-embed .video-js-root-embed .video-js .vjs-progress-control {
bottom: 56px !important; /* Adjust for larger control bar */ bottom: 44px !important; /* Much closer to control bar - minimal gap */
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
} }