feat: Add fullscreen functionality

This commit is contained in:
Peifan Li
2025-11-23 12:46:56 -05:00
parent 9d78f7a372
commit 6e2d648ce1
3 changed files with 110 additions and 45 deletions

View File

@@ -1,15 +1,20 @@
import {
Add,
CalendarToday,
Delete,
Download,
FastForward,
FastRewind,
Folder,
Forward10,
Fullscreen,
FullscreenExit,
Link as LinkIcon,
Loop,
Pause,
PlayArrow,
Replay10
Replay10,
VideoLibrary
} from '@mui/icons-material';
import {
Alert,
@@ -35,6 +40,7 @@ import {
Select,
Stack,
TextField,
Tooltip,
Typography,
useMediaQuery,
useTheme
@@ -116,6 +122,32 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
}
};
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const handleToggleFullscreen = () => {
const videoContainer = videoRef.current?.parentElement;
if (!videoContainer) return;
if (!document.fullscreenElement) {
videoContainer.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
};
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
const handleSeek = (seconds: number) => {
if (videoRef.current) {
videoRef.current.currentTime += seconds;
@@ -385,7 +417,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
</video>
{/* Custom Controls Area */}
<Box sx={{ p: 1, bgcolor: '#1a1a1a' }}>
<Box sx={{
p: 1,
bgcolor: theme.palette.mode === 'dark' ? '#1a1a1a' : '#f5f5f5',
opacity: isFullscreen ? 0.3 : 1,
transition: 'opacity 0.3s',
'&:hover': { opacity: 1 }
}}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems="center"
@@ -394,46 +432,66 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
>
{/* Row 1 on Mobile: Play/Pause and Loop */}
<Stack direction="row" spacing={2} justifyContent="center" width={{ xs: '100%', sm: 'auto' }}>
<Button
variant="contained"
color={isPlaying ? "warning" : "primary"}
onClick={handlePlayPause}
startIcon={isPlaying ? <Pause /> : <PlayArrow />}
fullWidth={isMobile}
>
{isPlaying ? t('paused') : t('playing')}
</Button>
<Tooltip title={isPlaying ? t('paused') : t('playing')}>
<Button
variant="contained"
color={isPlaying ? "warning" : "primary"}
onClick={handlePlayPause}
fullWidth={isMobile}
>
{isPlaying ? <Pause /> : <PlayArrow />}
</Button>
</Tooltip>
<Button
variant={isLooping ? "contained" : "outlined"}
color="secondary"
onClick={handleToggleLoop}
startIcon={<Loop />}
fullWidth={isMobile}
>
{t('loop')} {isLooping ? t('on') : t('off')}
</Button>
<Tooltip title={`${t('loop')} ${isLooping ? t('on') : t('off')}`}>
<Button
variant={isLooping ? "contained" : "outlined"}
color="secondary"
onClick={handleToggleLoop}
fullWidth={isMobile}
>
<Loop />
</Button>
</Tooltip>
<Tooltip title={isFullscreen ? t('exitFullscreen') : t('enterFullscreen')}>
<Button
variant="outlined"
onClick={handleToggleFullscreen}
fullWidth={isMobile}
>
{isFullscreen ? <FullscreenExit /> : <Fullscreen />}
</Button>
</Tooltip>
</Stack>
{/* Row 2 on Mobile: Seek Controls */}
<Stack direction="row" spacing={1} justifyContent="center" width={{ xs: '100%', sm: 'auto' }}>
<Button variant="outlined" onClick={() => handleSeek(-60)} startIcon={<FastRewind />}>
-1m
</Button>
<Button variant="outlined" onClick={() => handleSeek(-10)} startIcon={<Replay10 />}>
-10s
</Button>
<Button variant="outlined" onClick={() => handleSeek(10)} endIcon={<Forward10 />}>
+10s
</Button>
<Button variant="outlined" onClick={() => handleSeek(60)} endIcon={<FastForward />}>
+1m
</Button>
<Tooltip title="-1m">
<Button variant="outlined" onClick={() => handleSeek(-60)}>
<FastRewind />
</Button>
</Tooltip>
<Tooltip title="-10s">
<Button variant="outlined" onClick={() => handleSeek(-10)}>
<Replay10 />
</Button>
</Tooltip>
<Tooltip title="+10s">
<Button variant="outlined" onClick={() => handleSeek(10)}>
<Forward10 />
</Button>
</Tooltip>
<Tooltip title="+1m">
<Button variant="outlined" onClick={() => handleSeek(60)}>
<FastForward />
</Button>
</Tooltip>
</Stack>
</Stack>
</Box>
</Box>
{/* Info Column */}
<Box sx={{ mt: 2 }}>
<Typography variant="h5" component="h1" fontWeight="bold" gutterBottom>
{video.title}
@@ -506,8 +564,9 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
<Box sx={{ bgcolor: 'background.paper', p: 2, borderRadius: 2 }}>
<Stack direction="row" spacing={3} alignItems="center" flexWrap="wrap">
{video.sourceUrl && (
<Typography variant="body2">
<a href={video.sourceUrl} target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main, textDecoration: 'none' }}>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
<a href={video.sourceUrl} target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main, textDecoration: 'none', display: 'flex', alignItems: 'center' }}>
<LinkIcon fontSize="small" sx={{ mr: 0.5 }} />
<strong>{t('originalLink')}</strong>
</a>
</Typography>
@@ -520,11 +579,13 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
</a>
</Typography>
)}
<Typography variant="body2">
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
<VideoLibrary fontSize="small" sx={{ mr: 0.5 }} />
<strong>{t('source')}</strong> {video.source === 'bilibili' ? 'Bilibili' : (video.source === 'local' ? 'Local Upload' : 'YouTube')}
</Typography>
{video.addedAt && (
<Typography variant="body2">
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center' }}>
<CalendarToday fontSize="small" sx={{ mr: 0.5 }} />
<strong>{t('addedDate')}</strong> {new Date(video.addedAt).toLocaleDateString()}
</Typography>
)}

View File

@@ -4,7 +4,7 @@ const getTheme = (mode: 'light' | 'dark') => createTheme({
palette: {
mode,
primary: {
main: '#00e5ff', // Neon Cyan
main: mode === 'dark' ? '#00e5ff' : '#00838f', // Neon Cyan for dark, Cyan 800 for light
},
secondary: {
main: '#651fff', // Deep Purple
@@ -14,7 +14,7 @@ const getTheme = (mode: 'light' | 'dark') => createTheme({
paper: mode === 'dark' ? '#1e1e1e' : '#ffffff',
},
text: {
primary: mode === 'dark' ? '#ffffff' : '#000000',
primary: mode === 'dark' ? '#ffffff' : '#212121', // Dark grey for light mode
secondary: mode === 'dark' ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)',
},
},
@@ -39,9 +39,9 @@ const getTheme = (mode: 'light' | 'dark') => createTheme({
fontWeight: 600,
},
containedPrimary: {
boxShadow: '0 0 10px rgba(0, 229, 255, 0.5)', // Neon glow
boxShadow: mode === 'dark' ? '0 0 10px rgba(0, 229, 255, 0.5)' : 'none', // Neon glow only in dark mode
'&:hover': {
boxShadow: '0 0 20px rgba(0, 229, 255, 0.7)',
boxShadow: mode === 'dark' ? '0 0 20px rgba(0, 229, 255, 0.7)' : '0 2px 4px rgba(0,0,0,0.2)',
},
},
},

View File

@@ -72,8 +72,8 @@ export const translations = {
noVideosFoundMatching: "No videos found matching your search.",
// Video Player
playing: "Playing",
paused: "Paused",
playing: "Play",
paused: "Pause",
next: "Next",
previous: "Previous",
loop: "Loop",
@@ -104,6 +104,8 @@ export const translations = {
loadingVideo: "Loading video...",
current: "(Current)",
rateThisVideo: "Rate this video",
enterFullscreen: "Enter Fullscreen",
exitFullscreen: "Exit Fullscreen",
// Login
signIn: "Sign in",
@@ -242,8 +244,8 @@ export const translations = {
noVideosFoundMatching: "未找到匹配的视频。",
// Video Player
playing: "播放",
paused: "暂停",
playing: "播放",
paused: "暂停",
next: "下一个",
previous: "上一个",
loop: "循环",
@@ -274,6 +276,8 @@ export const translations = {
loadingVideo: "加载视频中...",
current: "(当前)",
rateThisVideo: "给视频评分",
enterFullscreen: "全屏",
exitFullscreen: "退出全屏",
// Login
signIn: "登录",