style: Refactor header layout for mobile and desktop

This commit is contained in:
Peifan Li
2025-11-22 23:56:54 -05:00
parent 534044c3f7
commit 7a847ed1cc
2 changed files with 191 additions and 131 deletions

View File

@@ -4,6 +4,7 @@ import {
Clear,
CloudUpload,
Download,
Menu as MenuIcon,
Search
} from '@mui/icons-material';
import {
@@ -12,14 +13,18 @@ import {
Box,
Button,
CircularProgress,
Collapse,
IconButton,
InputAdornment,
Menu,
MenuItem,
Stack,
TextField,
Toolbar,
Tooltip,
Typography
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import { FormEvent, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
@@ -58,7 +63,10 @@ const Header: React.FC<HeaderProps> = ({
const [error, setError] = useState<string>('');
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [uploadModalOpen, setUploadModalOpen] = useState<boolean>(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState<boolean>(false);
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const isDownloading = activeDownloads.length > 0;
@@ -95,10 +103,12 @@ const Header: React.FC<HeaderProps> = ({
const result = await onSubmit(videoUrl);
if (result.success) {
setVideoUrl('');
setMobileMenuOpen(false);
} else if (result.isSearchTerm) {
const searchResult = await onSearch(videoUrl);
if (searchResult.success) {
setVideoUrl('');
setMobileMenuOpen(false);
navigate('/');
} else {
setError(searchResult.error);
@@ -110,6 +120,7 @@ const Header: React.FC<HeaderProps> = ({
const result = await onSearch(videoUrl);
if (result.success) {
setVideoUrl('');
setMobileMenuOpen(false);
navigate('/');
} else {
setError(result.error);
@@ -124,11 +135,6 @@ const Header: React.FC<HeaderProps> = ({
};
const handleUploadSuccess = () => {
// Refresh the home page to show the new video
// If we are already on home, we might need to trigger a refresh.
// For now, navigating to / usually triggers a re-render if the component fetches on mount.
// If not, we might need a context or prop to trigger refresh.
// Assuming Home component fetches on mount.
if (window.location.pathname === '/') {
window.location.reload();
} else {
@@ -136,114 +142,153 @@ const Header: React.FC<HeaderProps> = ({
}
};
const renderActionButtons = () => (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title="Upload Video">
<IconButton color="inherit" onClick={() => setUploadModalOpen(true)} sx={{ mr: 1 }}>
<CloudUpload />
</IconButton>
</Tooltip>
{isDownloading && (
<>
<IconButton color="inherit" onClick={handleDownloadsClick}>
<Badge badgeContent={activeDownloads.length} color="secondary">
<Download />
</Badge>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleDownloadsClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{activeDownloads.map((download) => (
<MenuItem key={download.id}>
<CircularProgress size={20} sx={{ mr: 2 }} />
<Typography variant="body2" noWrap sx={{ maxWidth: 200 }}>
{download.title}
</Typography>
</MenuItem>
))}
</Menu>
</>
)}
<IconButton sx={{ ml: 1 }} onClick={toggleTheme} color="inherit">
{currentThemeMode === 'dark' ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Box>
);
const renderSearchInput = () => (
<Box component="form" onSubmit={handleSubmit} sx={{ flexGrow: 1, display: 'flex', justifyContent: 'center', width: '100%' }}>
<TextField
fullWidth
variant="outlined"
placeholder="Enter YouTube/Bilibili URL or search term"
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
disabled={isSubmitting}
error={!!error}
helperText={error}
size="small"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{isSearchMode && searchTerm && (
<IconButton onClick={onResetSearch} edge="end" size="small" sx={{ mr: 0.5 }}>
<Clear />
</IconButton>
)}
<Button
type="submit"
variant="contained"
disabled={isSubmitting}
sx={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, height: '100%', minWidth: 'auto', px: 3 }}
>
{isSubmitting ? <CircularProgress size={24} color="inherit" /> : <Search />}
</Button>
</InputAdornment>
),
sx: { pr: 0, borderRadius: 2 }
}}
/>
</Box>
);
return (
<AppBar position="sticky" color="default" elevation={0} sx={{ bgcolor: 'background.paper', borderBottom: 1, borderColor: 'divider' }}>
<Toolbar>
<Box sx={{ display: 'flex', alignItems: 'center', flexGrow: 0, mr: 2 }}>
<Toolbar sx={{ flexDirection: isMobile ? 'column' : 'row', alignItems: isMobile ? 'stretch' : 'center', py: isMobile ? 1 : 0 }}>
{/* Top Bar for Mobile / Main Bar for Desktop */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: isMobile ? '100%' : 'auto', flexGrow: isMobile ? 0 : 0, mr: isMobile ? 0 : 2 }}>
<Link to="/" onClick={onResetSearch} style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', color: 'inherit' }}>
<img src={logo} alt="MyTube Logo" height={40} />
<Typography variant="h5" sx={{ ml: 1, fontWeight: 'bold' }}>
MyTube
</Typography>
</Link>
</Box>
<Box component="form" onSubmit={handleSubmit} sx={{ flexGrow: 1, display: 'flex', justifyContent: 'center', maxWidth: 800, mx: 'auto' }}>
<TextField
fullWidth
variant="outlined"
placeholder="Enter YouTube/Bilibili URL or search term"
value={videoUrl}
onChange={(e) => setVideoUrl(e.target.value)}
disabled={isSubmitting}
error={!!error}
helperText={error}
size="small"
InputProps={{
endAdornment: (
<InputAdornment position="end">
{isSearchMode && searchTerm && (
<IconButton onClick={onResetSearch} edge="end" size="small" sx={{ mr: 0.5 }}>
<Clear />
</IconButton>
)}
<Button
type="submit"
variant="contained"
disabled={isSubmitting}
sx={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, height: '100%', minWidth: 'auto', px: 3 }}
>
{isSubmitting ? <CircularProgress size={24} color="inherit" /> : <Search />}
</Button>
</InputAdornment>
),
sx: { pr: 0, borderRadius: 2 }
}}
/>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', ml: 2 }}>
<Tooltip title="Upload Video">
<IconButton color="inherit" onClick={() => setUploadModalOpen(true)} sx={{ mr: 1 }}>
<CloudUpload />
{isMobile && (
<IconButton onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
<MenuIcon />
</IconButton>
</Tooltip>
{isDownloading && (
<>
<IconButton color="inherit" onClick={handleDownloadsClick}>
<Badge badgeContent={activeDownloads.length} color="secondary">
<Download />
</Badge>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleDownloadsClose}
PaperProps={{
elevation: 0,
sx: {
overflow: 'visible',
filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
mt: 1.5,
'& .MuiAvatar-root': {
width: 32,
height: 32,
ml: -0.5,
mr: 1,
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
top: 0,
right: 14,
width: 10,
height: 10,
bgcolor: 'background.paper',
transform: 'translateY(-50%) rotate(45deg)',
zIndex: 0,
},
},
}}
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
>
{activeDownloads.map((download) => (
<MenuItem key={download.id}>
<CircularProgress size={20} sx={{ mr: 2 }} />
<Typography variant="body2" noWrap sx={{ maxWidth: 200 }}>
{download.title}
</Typography>
</MenuItem>
))}
</Menu>
</>
)}
<IconButton sx={{ ml: 1 }} onClick={toggleTheme} color="inherit">
{currentThemeMode === 'dark' ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Box>
{/* Desktop Layout */}
{!isMobile && (
<>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'center', maxWidth: 800, mx: 'auto' }}>
{renderSearchInput()}
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', ml: 2 }}>
{renderActionButtons()}
</Box>
</>
)}
{/* Mobile Dropdown Layout */}
{isMobile && (
<Collapse in={mobileMenuOpen} sx={{ width: '100%' }}>
<Stack spacing={2} sx={{ py: 2 }}>
{/* Row 1: Search Input */}
<Box>
{renderSearchInput()}
</Box>
{/* Row 2: Action Buttons */}
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{renderActionButtons()}
</Box>
</Stack>
</Collapse>
)}
</Toolbar>
<UploadModal

View File

@@ -34,6 +34,7 @@ import {
Stack,
TextField,
Typography,
useMediaQuery,
useTheme
} from '@mui/material';
import axios from 'axios';
@@ -65,6 +66,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [video, setVideo] = useState<Video | null>(null);
const [loading, setLoading] = useState<boolean>(true);
@@ -341,38 +343,51 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
</video>
{/* Custom Controls Area */}
<Box sx={{ p: 1, display: 'flex', justifyContent: 'center', gap: 2, bgcolor: '#1a1a1a' }}>
<Button
variant="contained"
color={isPlaying ? "warning" : "primary"}
onClick={handlePlayPause}
startIcon={isPlaying ? <Pause /> : <PlayArrow />}
<Box sx={{ p: 1, bgcolor: '#1a1a1a' }}>
<Stack
direction={{ xs: 'column', sm: 'row' }}
alignItems="center"
justifyContent="center"
spacing={{ xs: 2, sm: 2 }}
>
{isPlaying ? "Pause" : "Play"}
</Button>
{/* 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 ? "Pause" : "Play"}
</Button>
<Button
variant={isLooping ? "contained" : "outlined"}
color="secondary"
onClick={handleToggleLoop}
startIcon={<Loop />}
>
Loop {isLooping ? "On" : "Off"}
</Button>
<Button
variant={isLooping ? "contained" : "outlined"}
color="secondary"
onClick={handleToggleLoop}
startIcon={<Loop />}
fullWidth={isMobile}
>
Loop {isLooping ? "On" : "Off"}
</Button>
</Stack>
<Stack direction="row" spacing={1}>
<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>
{/* 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>
</Stack>
</Stack>
</Box>
</Box>