Bulk actions support (#1418)

This commit is contained in:
Markos Gogoulos
2025-11-11 11:32:54 +02:00
committed by GitHub
parent 2a0cb977f2
commit e80590a3aa
160 changed files with 14100 additions and 1797 deletions

View File

@@ -13,6 +13,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
const audioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
setIsAudioFile(audioFile);
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]);
// Function to jump 15 seconds backward
@@ -128,22 +130,34 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span>
</div>
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md"
src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
{/* Video container with persistent background for audio files */}
<div className="ios-video-wrapper">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="ios-audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@@ -26,6 +26,18 @@ const mediaPageLinkStyles = {
},
} as const;
// Helper function to parse time string (HH:MM:SS.mmm) to seconds
const parseTimeToSeconds = (timeString: string): number => {
const parts = timeString.split(':');
if (parts.length !== 3) return 0;
const hours = parseInt(parts[0], 10) || 0;
const minutes = parseInt(parts[1], 10) || 0;
const seconds = parseFloat(parts[2]) || 0;
return hours * 3600 + minutes * 60 + seconds;
};
interface TimelineControlsProps {
currentTime: number;
duration: number;
@@ -191,7 +203,17 @@ const TimelineControls = ({
setIsAutoSaving(true);
// Format segments data for API request - use ref to get latest segments and sort by start time
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
const chapters = clipSegmentsRef.current
.filter((segment) => {
// Filter out empty titles
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
return false;
}
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
return !isDefaultName;
})
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
.map((chapter) => ({
startTime: formatDetailedTime(chapter.startTime),
@@ -199,7 +221,7 @@ const TimelineControls = ({
chapterTitle: chapter.chapterTitle,
}));
logger.debug('chapters', chapters);
logger.debug('Filtered chapters (only custom titles):', chapters);
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
// For testing, use '1234' if no mediaId is available
@@ -207,12 +229,13 @@ const TimelineControls = ({
logger.debug('mediaId', finalMediaId);
if (!finalMediaId || chapters.length === 0) {
logger.debug('No mediaId or segments, skipping auto-save');
if (!finalMediaId) {
logger.debug('No mediaId, skipping auto-save');
setIsAutoSaving(false);
return;
}
// Save chapters (empty array if no chapters have titles)
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters });
const response = await autoSaveVideo(finalMediaId, { chapters });
@@ -499,11 +522,20 @@ const TimelineControls = ({
try {
// Format chapters data for API request - sort by start time first
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
const chapters = clipSegments
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
.filter((segment) => {
// Filter out empty titles
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
return false;
}
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
return !isDefaultName;
})
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
.map((segment) => ({
chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
chapterTitle: segment.chapterTitle,
from: formatDetailedTime(segment.startTime),
to: formatDetailedTime(segment.endTime),
}));

View File

@@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return (
<div className="video-player-container">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
<video
ref={videoRef}
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata"
crossOrigin="anonymous"
onClick={handleVideoClick}

View File

@@ -8,12 +8,40 @@
overflow: hidden;
}
/* Video wrapper for positioning background */
.ios-video-wrapper {
position: relative;
width: 100%;
background-color: black;
border-radius: 0.5rem;
overflow: hidden;
}
/* Persistent background poster for audio files (Safari fix) */
.ios-audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
}
.ios-video-player-container video {
position: relative;
width: 100%;
height: auto;
max-height: 360px;
aspect-ratio: 16/9;
background-color: black;
}
/* Make video transparent only for audio files with poster so background shows through */
.ios-video-player-container video.audio-with-poster {
background-color: transparent;
}
.ios-time-display {

View File

@@ -76,10 +76,26 @@
user-select: none;
}
/* Persistent background poster for audio files (Safari fix) */
.audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
.video-player-container video {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 2;
/* Force hardware acceleration */
transform: translateZ(0);
-webkit-transform: translateZ(0);
@@ -88,6 +104,11 @@
user-select: none;
}
/* Make video transparent only for audio files with poster so background shows through */
.video-player-container video.audio-with-poster {
background: transparent;
}
/* iOS-specific styles */
@supports (-webkit-touch-callout: none) {
.video-player-container video {
@@ -109,6 +130,7 @@
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 3;
}
.video-player-container:hover .play-pause-indicator {
@@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0;
transition: opacity 0.3s;
z-index: 3;
}
.video-player-container:hover .video-controls {

View File

@@ -13,6 +13,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
const [videoUrl, setVideoUrl] = useState<string>('');
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
const [isAudioFile, setIsAudioFile] = useState(false);
// Refs for hold-to-continue functionality
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
@@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
setVideoUrl(url);
// Check if the media is an audio file and set poster image
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
const audioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
setIsAudioFile(audioFile);
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
}, [videoRef]);
// Function to jump 15 seconds backward
@@ -128,22 +130,34 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
</span>
</div>
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className="w-full rounded-md"
src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
{/* Video container with persistent background for audio files */}
<div className="ios-video-wrapper">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="ios-audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
{/* iOS-optimized Video Element with Native Controls */}
<video
ref={(ref) => setIosVideoRef(ref)}
className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
src={videoUrl}
controls
playsInline
webkit-playsinline="true"
x-webkit-airplay="allow"
preload="auto"
crossOrigin="anonymous"
poster={posterImage}
>
<source src={videoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
</video>
</div>
{/* iOS Video Skip Controls */}
<div className="ios-skip-controls mt-3 flex justify-center gap-4">

View File

@@ -47,14 +47,24 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
const posterImage = isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined);
// Detect iOS device
// Detect iOS device and Safari browser
useEffect(() => {
const checkIOS = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
};
const checkSafari = () => {
const userAgent = navigator.userAgent || navigator.vendor || (window as any).opera;
return /Safari/.test(userAgent) && !/Chrome/.test(userAgent) && !/Chromium/.test(userAgent);
};
setIsIOS(checkIOS());
// Store Safari detection globally for other components
if (typeof window !== 'undefined') {
(window as any).isSafari = checkSafari();
}
// Check if video was previously initialized
if (typeof window !== 'undefined') {
@@ -343,9 +353,19 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
return (
<div className="video-player-container">
{/* Persistent background image for audio files (Safari fix) */}
{isAudioFile && posterImage && (
<div
className="audio-poster-background"
style={{ backgroundImage: `url(${posterImage})` }}
aria-hidden="true"
/>
)}
<video
ref={videoRef}
preload="auto"
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
preload="metadata"
crossOrigin="anonymous"
onClick={handleVideoClick}
playsInline
@@ -356,7 +376,10 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
poster={posterImage}
>
<source src={sampleVideoUrl} type="video/mp4" />
<p>Your browser doesn't support HTML5 video.</p>
{/* Safari fallback for audio files */}
<source src={sampleVideoUrl} type="audio/mp4" />
<source src={sampleVideoUrl} type="audio/mpeg" />
<p>Your browser doesn't support HTML5 video or audio.</p>
</video>
{/* iOS First-play indicator - only shown on first visit for iOS devices when not initialized */}

View File

@@ -8,12 +8,40 @@
overflow: hidden;
}
/* Video wrapper for positioning background */
.ios-video-wrapper {
position: relative;
width: 100%;
background-color: black;
border-radius: 0.5rem;
overflow: hidden;
}
/* Persistent background poster for audio files (Safari fix) */
.ios-audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
pointer-events: none;
}
.ios-video-player-container video {
position: relative;
width: 100%;
height: auto;
max-height: 360px;
aspect-ratio: 16/9;
background-color: black;
}
/* Make video transparent only for audio files with poster so background shows through */
.ios-video-player-container video.audio-with-poster {
background-color: transparent;
}
.ios-time-display {

View File

@@ -76,10 +76,26 @@
user-select: none;
}
/* Persistent background poster for audio files (Safari fix) */
.audio-poster-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: 1;
pointer-events: none;
}
.video-player-container video {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
z-index: 2;
/* Force hardware acceleration */
transform: translateZ(0);
-webkit-transform: translateZ(0);
@@ -88,6 +104,11 @@
user-select: none;
}
/* Make video transparent only for audio files with poster so background shows through */
.video-player-container video.audio-with-poster {
background: transparent;
}
/* iOS-specific styles */
@supports (-webkit-touch-callout: none) {
.video-player-container video {
@@ -109,6 +130,7 @@
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
z-index: 3;
}
.video-player-container:hover .play-pause-indicator {
@@ -187,6 +209,7 @@
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
opacity: 0;
transition: opacity 0.3s;
z-index: 3;
}
.video-player-container:hover .video-controls {

View File

@@ -20,6 +20,7 @@ class CustomChaptersOverlay extends Component {
this.touchStartTime = 0;
this.touchThreshold = 150; // ms for tap vs scroll detection
this.isSmallScreen = window.innerWidth <= 480;
this.scrollY = 0; // Track scroll position before locking
// Bind methods
this.createOverlay = this.createOverlay.bind(this);
@@ -31,6 +32,8 @@ class CustomChaptersOverlay extends Component {
this.handleMobileInteraction = this.handleMobileInteraction.bind(this);
this.setupResizeListener = this.setupResizeListener.bind(this);
this.handleResize = this.handleResize.bind(this);
this.lockBodyScroll = this.lockBodyScroll.bind(this);
this.unlockBodyScroll = this.unlockBodyScroll.bind(this);
// Initialize after player is ready
this.player().ready(() => {
@@ -65,6 +68,9 @@ class CustomChaptersOverlay extends Component {
const el = this.player().el();
if (el) el.classList.remove('chapters-open');
// Restore body scroll on mobile when closing
this.unlockBodyScroll();
}
setupResizeListener() {
@@ -164,6 +170,8 @@ 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 when closing
this.unlockBodyScroll();
};
chapterClose.appendChild(closeBtn);
playlistTitle.appendChild(chapterClose);
@@ -355,6 +363,37 @@ class CustomChaptersOverlay extends Component {
}
}
lockBodyScroll() {
if (!this.isMobile) return;
// Save current scroll position
this.scrollY = window.scrollY || window.pageYOffset;
// Lock body scroll with proper iOS handling
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.top = `-${this.scrollY}px`;
document.body.style.left = '0';
document.body.style.right = '0';
document.body.style.width = '100%';
}
unlockBodyScroll() {
if (!this.isMobile) return;
// Restore body scroll
const scrollY = this.scrollY;
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.width = '';
// Restore scroll position
window.scrollTo(0, scrollY);
}
toggleOverlay() {
if (!this.overlay) return;
@@ -369,17 +408,11 @@ class CustomChaptersOverlay extends Component {
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 = '';
}
// Lock/unlock body scroll on mobile when overlay opens/closes
if (isHidden) {
this.lockBodyScroll();
} else {
this.unlockBodyScroll();
}
try {
@@ -390,7 +423,9 @@ class CustomChaptersOverlay extends Component {
m.classList.remove('vjs-lock-showing');
m.style.display = 'none';
});
} catch (e) {}
} catch {
// Ignore errors when closing menus
}
}
updateCurrentChapter() {
@@ -406,7 +441,6 @@ class CustomChaptersOverlay extends Component {
currentTime >= chapter.startTime &&
(index === this.chaptersData.length - 1 || currentTime < this.chaptersData[index + 1].startTime);
const handle = item.querySelector('.playlist-drag-handle');
const dynamic = item.querySelector('.meta-dynamic');
if (isPlaying) {
currentChapterIndex = index;
@@ -463,11 +497,7 @@ class CustomChaptersOverlay extends Component {
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 = '';
}
this.unlockBodyScroll();
}
}
@@ -479,11 +509,7 @@ class CustomChaptersOverlay extends Component {
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 = '';
}
this.unlockBodyScroll();
// Clean up event listeners
if (this.handleResize) {

View File

@@ -25,6 +25,7 @@ class CustomSettingsMenu extends Component {
this.isMobile = this.detectMobile();
this.isSmallScreen = window.innerWidth <= 480;
this.touchThreshold = 150; // ms for tap vs scroll detection
this.scrollY = 0; // Track scroll position before locking
// Bind methods
this.createSettingsButton = this.createSettingsButton.bind(this);
@@ -41,6 +42,8 @@ class CustomSettingsMenu extends Component {
this.detectMobile = this.detectMobile.bind(this);
this.handleMobileInteraction = this.handleMobileInteraction.bind(this);
this.setupResizeListener = this.setupResizeListener.bind(this);
this.lockBodyScroll = this.lockBodyScroll.bind(this);
this.unlockBodyScroll = this.unlockBodyScroll.bind(this);
// Initialize after player is ready
this.player().ready(() => {
@@ -656,6 +659,8 @@ class CustomSettingsMenu extends Component {
if (btnEl) {
btnEl.classList.remove('settings-clicked');
}
// Restore body scroll on mobile when closing
this.unlockBodyScroll();
};
closeButton.addEventListener('click', closeFunction);
@@ -942,6 +947,37 @@ class CustomSettingsMenu extends Component {
this.startSubtitleSync();
}
lockBodyScroll() {
if (!this.isMobile) return;
// Save current scroll position
this.scrollY = window.scrollY || window.pageYOffset;
// Lock body scroll with proper iOS handling
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.top = `-${this.scrollY}px`;
document.body.style.left = '0';
document.body.style.right = '0';
document.body.style.width = '100%';
}
unlockBodyScroll() {
if (!this.isMobile) return;
// Restore body scroll
const scrollY = this.scrollY;
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.top = '';
document.body.style.left = '';
document.body.style.right = '';
document.body.style.width = '';
// Restore scroll position
window.scrollTo(0, scrollY);
}
toggleSettings(e) {
// e.stopPropagation();
const isVisible = this.settingsOverlay.classList.contains('show');
@@ -954,11 +990,7 @@ class CustomSettingsMenu extends Component {
this.stopKeepingControlsVisible();
// Restore body scroll on mobile when closing
if (this.isMobile) {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
}
this.unlockBodyScroll();
} else {
this.settingsOverlay.classList.add('show');
this.settingsOverlay.style.display = 'block';
@@ -972,11 +1004,7 @@ class CustomSettingsMenu extends Component {
}
// Prevent body scroll on mobile when overlay is open
if (this.isMobile) {
document.body.style.overflow = 'hidden';
document.body.style.position = 'fixed';
document.body.style.width = '100%';
}
this.lockBodyScroll();
}
this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles
@@ -1002,6 +1030,9 @@ class CustomSettingsMenu extends Component {
this.settingsOverlay.classList.add('show');
this.settingsOverlay.style.display = 'block';
// Lock body scroll when opening
this.lockBodyScroll();
// Hide other submenus and show subtitles submenu
this.speedSubmenu.style.display = 'none';
if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
@@ -1072,11 +1103,7 @@ class CustomSettingsMenu extends Component {
}
// Restore body scroll on mobile when closing
if (this.isMobile) {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
}
this.unlockBodyScroll();
}
}
@@ -1417,6 +1444,8 @@ class CustomSettingsMenu extends Component {
if (btnEl) {
btnEl.classList.remove('settings-clicked');
}
// Restore body scroll on mobile when closing
this.unlockBodyScroll();
}
}
@@ -1493,11 +1522,7 @@ class CustomSettingsMenu extends Component {
}
// Restore body scroll on mobile when disposing
if (this.isMobile) {
document.body.style.overflow = '';
document.body.style.position = '';
document.body.style.width = '';
}
this.unlockBodyScroll();
// Remove DOM elements
if (this.settingsOverlay) {

View File

@@ -49,10 +49,7 @@ class EndScreenOverlay extends Component {
// Get videos to show - access directly from options during createEl
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
const videosToShow =
relatedVideos.length > 0
? relatedVideos.slice(0, maxVideos)
: this.createSampleVideos().slice(0, maxVideos);
const videosToShow = relatedVideos.slice(0, maxVideos);
if (useSwiper) {
return this.createSwiperGrid(videosToShow, itemsPerView || 2, columns, gridRows || 1);
@@ -307,8 +304,8 @@ class EndScreenOverlay extends Component {
if (this.relatedVideos && Array.isArray(this.relatedVideos) && this.relatedVideos.length > 0) {
return this.relatedVideos.slice(0, maxVideos);
}
// Fallback to sample videos for testing
return this.createSampleVideos().slice(0, maxVideos);
// Return empty array if no related videos
return [];
}
createVideoItem(video, isSwiperMode = false, itemsPerView = 2, isGridMode = false) {
@@ -745,153 +742,12 @@ class EndScreenOverlay extends Component {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
createSampleVideos() {
return [
{
id: 'sample1',
title: 'React Full Course - Complete Tutorial for Beginners',
author: 'Bro Code',
views: '2.1M views',
duration: 1800,
},
{
id: 'sample2',
title: 'JavaScript ES6+ Modern Features',
author: 'Tech Tutorials',
views: '850K views',
duration: 1200,
},
{
id: 'sample3',
title: 'CSS Grid Layout Masterclass',
author: 'Web Dev Academy',
views: '1.2M views',
duration: 2400,
},
{
id: 'sample4',
title: 'Node.js Backend Development',
author: 'Code Master',
views: '650K views',
duration: 3600,
},
{
id: 'sample5',
title: 'Vue.js Complete Guide',
author: 'Frontend Pro',
views: '980K views',
duration: 2800,
},
{
id: 'sample6',
title: 'Python Data Science Bootcamp',
author: 'Data Academy',
views: '1.5M views',
duration: 4200,
},
{
id: 'sample7',
title: 'TypeScript for Beginners',
author: 'Code School',
views: '750K views',
duration: 1950,
},
{
id: 'sample8',
title: 'Docker Container Tutorial',
author: 'DevOps Pro',
views: '920K views',
duration: 2700,
},
{
id: 'sample9',
title: 'MongoDB Database Design',
author: 'DB Expert',
views: '580K views',
duration: 3200,
},
{
id: 'sample10',
title: 'AWS Cloud Computing Essentials',
author: 'Cloud Master',
views: '1.8M views',
duration: 4800,
},
{
id: 'sample11',
title: 'GraphQL API Development',
author: 'API Guru',
views: '420K views',
duration: 2100,
},
{
id: 'sample12',
title: 'Kubernetes Orchestration Guide',
author: 'Container Pro',
views: '680K views',
duration: 3900,
},
{
id: 'sample13',
title: 'Redis Caching Strategies',
author: 'Cache Expert',
views: '520K views',
duration: 2250,
},
{
id: 'sample14',
title: 'Web Performance Optimization',
author: 'Speed Master',
views: '890K views',
duration: 3100,
},
{
id: 'sample15',
title: 'CI/CD Pipeline Setup',
author: 'DevOps Guide',
views: '710K views',
duration: 2900,
},
{
id: 'sample16',
title: 'Microservices Architecture',
author: 'System Design',
views: '1.3M views',
duration: 4500,
},
{
id: 'sample17',
title: 'Next.js App Router Tutorial',
author: 'Web Academy',
views: '640K views',
duration: 2650,
},
{
id: 'sample18',
title: 'Tailwind CSS Crash Course',
author: 'CSS Master',
views: '1.1M views',
duration: 1800,
},
{
id: 'sample19',
title: 'Git and GitHub Essentials',
author: 'Version Control Pro',
views: '2.3M views',
duration: 3300,
},
{
id: 'sample20',
title: 'REST API Best Practices',
author: 'API Design',
views: '780K views',
duration: 2400,
},
];
}
show() {
this.el().style.display = 'flex';
// Only show if there are related videos
const relatedVideos = this.options_?.relatedVideos || this.relatedVideos || [];
if (relatedVideos.length > 0) {
this.el().style.display = 'flex';
}
}
hide() {

View File

@@ -139,12 +139,6 @@ video::cue {
}
}
@media (max-width: 600px) {
.video-js .vjs-control-bar .vjs-autoplay-toggle {
display: none !important;
}
}
@media (max-width: 500px) {
.video-js .vjs-control-bar .vjs-next-video-button {
display: none !important;