232 lines
8.9 KiB
TypeScript
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>© {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;
|