Files
YoutubeRedzone/App.tsx
2025-06-14 18:25:25 -04:00

232 lines
8.9 KiB
TypeScript

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { VideoCell } from './components/VideoCell';
import { BorderlessToolbar } from './components/BorderlessToolbar';
import { getYouTubeEmbedUrl } from './utils/youtube';
import { loadYouTubeIframeApi } from './utils/youtubeApiLoader';
const initialVideoUrls = ['', '', '', '']; // Up to 4 videos
const App: React.FC = () => {
const [videoUrls, setVideoUrls] = useState<string[]>(initialVideoUrls);
const [isBorderlessModeActive, setIsBorderlessModeActive] = useState<boolean>(false);
const [activeAudioVideoId, setActiveAudioVideoId] = useState<string | null>(null);
const [isYtApiReady, setIsYtApiReady] = useState<boolean>(false);
const [appErrorMessage, setAppErrorMessage] = useState<string | null>(null);
const [toolbarHeight, setToolbarHeight] = useState<number>(0);
const playerRefs = useRef<{ [key: string]: YT.Player | null }>({});
const toolbarRef = useRef<HTMLDivElement>(null);
useEffect(() => {
loadYouTubeIframeApi()
.then(() => {
setIsYtApiReady(true);
setAppErrorMessage(null);
})
.catch(error => {
console.error("Failed to load YouTube API:", error);
setAppErrorMessage("Error: Could not load YouTube player API. Video playback may not work correctly. Please try refreshing the page.");
setIsYtApiReady(false);
});
}, []);
const handleUrlChange = useCallback((index: number, newUrl: string) => {
setVideoUrls(prevUrls => {
const updatedUrls = [...prevUrls];
updatedUrls[index] = newUrl;
return updatedUrls;
});
if (!newUrl && activeAudioVideoId === `video-${index}`) {
setActiveAudioVideoId(null);
}
}, [activeAudioVideoId]);
const setPlayerInstance = useCallback((cellId: string, player: YT.Player | null) => {
playerRefs.current[cellId] = player;
}, []);
const handleSelectAudio = useCallback((cellId: string) => {
setActiveAudioVideoId(cellId);
Object.keys(playerRefs.current).forEach(key => {
const player = playerRefs.current[key];
if (player && typeof player.isMuted === 'function') {
if (key === cellId) {
console.log(`Unmuting player for cell: ${cellId}`);
if (player.isMuted()) player.unMute();
player.setVolume(100); // Ensure volume is set to audible level
} else {
console.log(`Muting player for cell: ${key}`);
player.mute(); // Always mute without checking current state
}
}
});
}, []);
const toggleBorderlessMode = () => {
setIsBorderlessModeActive(prev => {
const newMode = !prev;
if (!newMode) { // Exiting borderless mode
setActiveAudioVideoId(null);
Object.values(playerRefs.current).forEach(player => {
if (player && typeof player.isMuted === 'function' && !player.isMuted()) {
player.mute();
}
});
}
return newMode;
});
};
useEffect(() => {
const calculateToolbarHeight = () => {
if (isBorderlessModeActive && toolbarRef.current) {
setToolbarHeight(toolbarRef.current.offsetHeight);
}
};
if (isBorderlessModeActive) {
document.body.style.overflow = 'hidden';
calculateToolbarHeight(); // Initial calculation
window.addEventListener('resize', calculateToolbarHeight);
} else {
document.body.style.overflow = '';
setToolbarHeight(0); // Reset when exiting
}
return () => {
document.body.style.overflow = '';
window.removeEventListener('resize', calculateToolbarHeight);
};
}, [isBorderlessModeActive]);
const activeVideoDetails = videoUrls
.map((url, index) => ({
url,
cellId: `video-${index}`,
cellNumber: index + 1,
originalIndex: index, // Keep original index
isValid: getYouTubeEmbedUrl(url) !== null,
}))
.filter(video => video.isValid);
let gridLayoutClasses = "grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4 w-full max-w-7xl";
let mainContainerStyles: React.CSSProperties = {};
if (isBorderlessModeActive) {
mainContainerStyles = {
position: 'fixed',
top: `${toolbarHeight}px`, // Offset by toolbar height
left: 0,
width: '100vw',
height: `calc(100vh - ${toolbarHeight}px)`, // Adjust height
zIndex: 40, // Below toolbar (z-50) but above default content
background: '#000',
};
switch (activeVideoDetails.length) {
case 1:
gridLayoutClasses = "grid grid-cols-1 grid-rows-1 h-full";
break;
case 2:
gridLayoutClasses = "grid grid-cols-2 grid-rows-1 h-full";
break;
case 3:
gridLayoutClasses = "grid grid-cols-3 grid-rows-1 h-full";
break;
case 4:
default:
gridLayoutClasses = "grid grid-cols-2 grid-rows-2 h-full";
break;
}
if (activeVideoDetails.length === 0) {
gridLayoutClasses = "flex items-center justify-center h-full";
}
}
return (
<div className={`min-h-screen flex flex-col items-center ${isBorderlessModeActive ? 'bg-black' : 'bg-gray-900 text-white p-4 md:p-6 lg:p-8'}`}>
{!isBorderlessModeActive && (
<header className="w-full max-w-7xl mb-6 text-center">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-red-500 mb-2">YouTube Redzone Viewer</h1>
<p className="text-gray-300 mb-6 text-sm sm:text-base">Load up to four YouTube videos. Enter Borderless Mode for immersive viewing!</p>
<button
onClick={toggleBorderlessMode}
disabled={!isYtApiReady && activeVideoDetails.length > 0}
className="px-5 py-2.5 sm:px-6 sm:py-3 bg-red-600 hover:bg-red-700 text-white font-bold rounded-lg shadow-md transition-all duration-150 ease-in-out disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:bg-red-600 focus:outline-none focus:ring-4 focus:ring-red-400 focus:ring-opacity-50"
aria-label={isBorderlessModeActive ? 'Exit Borderless Mode' : 'Enter Borderless Mode'}
>
{isBorderlessModeActive ? 'Exit Borderless Mode' : 'Enter Borderless Mode'}
</button>
{(!isYtApiReady && activeVideoDetails.length > 0) && (
<p className="text-xs sm:text-sm text-yellow-300 mt-3 p-2 bg-yellow-800 bg-opacity-30 rounded-md">
YouTube Player API is not ready. Borderless mode might not function correctly.
</p>
)}
{appErrorMessage && (
<p role="alert" aria-live="assertive" className="text-xs sm:text-sm text-red-300 mt-3 p-2 bg-red-800 bg-opacity-40 rounded-md">{appErrorMessage}</p>
)}
</header>
)}
{isBorderlessModeActive && (
<BorderlessToolbar
ref={toolbarRef}
onExitBorderlessMode={toggleBorderlessMode}
activeVideos={activeVideoDetails.map(v => ({ cellId: v.cellId, cellNumber: v.cellNumber }))}
onSelectAudio={handleSelectAudio}
currentActiveAudioVideoId={activeAudioVideoId}
/>
)}
<main
id="video-grid-container"
className={`${gridLayoutClasses} ${isBorderlessModeActive ? '' : 'w-full max-w-7xl'}`}
style={mainContainerStyles}
aria-label="Video grid"
>
{isBorderlessModeActive ? (
activeVideoDetails.length > 0 ? (
activeVideoDetails.map(video => (
<VideoCell
key={video.cellId}
cellId={video.cellId}
initialUrl={video.url}
onUrlChange={(newUrl) => handleUrlChange(video.originalIndex, newUrl)}
cellNumber={video.cellNumber}
isBorderless={true}
isAudioActive={activeAudioVideoId === video.cellId}
setPlayerInstance={setPlayerInstance}
isApiReady={isYtApiReady}
/>
))
) : (
<div className="text-gray-400 text-lg">No active videos to display in borderless mode. Add some URLs!</div>
)
) : (
videoUrls.map((url, index) => (
<VideoCell
key={`video-cell-${index}`}
cellId={`video-${index}`}
initialUrl={url}
onUrlChange={(newUrl) => handleUrlChange(index, newUrl)}
cellNumber={index + 1}
isBorderless={false}
isAudioActive={activeAudioVideoId === `video-${index}`}
setPlayerInstance={setPlayerInstance}
isApiReady={isYtApiReady}
/>
))
)}
</main>
{!isBorderlessModeActive && (
<footer className="mt-10 sm:mt-12 text-center text-gray-500 text-xs sm:text-sm">
<p>&copy; {new Date().getFullYear()} YouTube Redzone Viewer. All video content is property of its respective owners.</p>
<p>Ensure you have the necessary rights to view and display the content.</p>
</footer>
)}
</div>
);
};
export default App;