Files
MyTube/frontend/src/components/ManagePage/VideosTable.tsx
2025-12-18 21:26:26 -05:00

327 lines
16 KiB
TypeScript

import {
Check,
Close,
Delete,
Edit,
Refresh,
Search,
VideoLibrary
} from '@mui/icons-material';
import {
Alert,
Box,
CircularProgress,
IconButton,
InputAdornment,
Pagination,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TableSortLabel,
TextField,
Tooltip,
Typography
} from '@mui/material';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { useLanguage } from '../../contexts/LanguageContext';
import { useVisitorMode } from '../../contexts/VisitorModeContext';
import { Video } from '../../types';
import { formatDuration, formatSize } from '../../utils/formatUtils';
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
interface VideosTableProps {
displayedVideos: Video[];
totalVideosCount: number;
totalSize: number;
searchTerm: string;
onSearchChange: (value: string) => void;
orderBy: keyof Video | 'fileSize';
order: 'asc' | 'desc';
onSort: (property: keyof Video | 'fileSize') => void;
page: number;
totalPages: number;
onPageChange: (event: React.ChangeEvent<unknown>, value: number) => void;
onDeleteClick: (id: string) => void;
deletingId: string | null;
onRefreshThumbnail: (id: string) => void;
refreshingId: string | null;
onUpdateVideo: (id: string, data: Partial<Video>) => Promise<any>;
}
const VideosTable: React.FC<VideosTableProps> = ({
displayedVideos,
totalVideosCount,
totalSize,
searchTerm,
onSearchChange,
orderBy,
order,
onSort,
page,
totalPages,
onPageChange,
onDeleteClick,
deletingId,
onRefreshThumbnail,
refreshingId,
onUpdateVideo
}) => {
const { t } = useLanguage();
const { visitorMode } = useVisitorMode();
// Local editing state
const [editingVideoId, setEditingVideoId] = useState<string | null>(null);
const [editTitle, setEditTitle] = useState<string>('');
const [isSavingTitle, setIsSavingTitle] = useState<boolean>(false);
const handleEditClick = (video: Video) => {
setEditingVideoId(video.id);
setEditTitle(video.title);
};
const handleCancelEdit = () => {
setEditingVideoId(null);
setEditTitle('');
};
const handleSaveTitle = async (id: string) => {
if (!editTitle.trim()) return;
setIsSavingTitle(true);
await onUpdateVideo(id, { title: editTitle });
setIsSavingTitle(false);
setEditingVideoId(null);
setEditTitle('');
};
const getThumbnailSrc = (video: Video) => {
if (video.thumbnailPath) {
return video.thumbnailPath.startsWith("http://") || video.thumbnailPath.startsWith("https://")
? video.thumbnailPath
: `${BACKEND_URL}${video.thumbnailPath}`;
}
return video.thumbnailUrl || 'https://via.placeholder.com/120x90?text=No+Thumbnail';
};
return (
<Box>
{/* Videos List */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h5" sx={{ display: 'flex', alignItems: 'center' }}>
<VideoLibrary sx={{ mr: 1, color: 'primary.main' }} />
{t('videos')} ({totalVideosCount}) - {formatSize(totalSize)}
</Typography>
<TextField
placeholder="Search videos..."
size="small"
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
slotProps={{
input: {
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}
}}
sx={{ width: 300 }}
/>
</Box>
{displayedVideos.length > 0 ? (
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>{t('thumbnail')}</TableCell>
<TableCell>
<TableSortLabel
active={orderBy === 'title'}
direction={orderBy === 'title' ? order : 'asc'}
onClick={() => onSort('title')}
>
{t('title')}
</TableSortLabel>
</TableCell>
<TableCell>
<TableSortLabel
active={orderBy === 'author'}
direction={orderBy === 'author' ? order : 'asc'}
onClick={() => onSort('author')}
>
{t('author')}
</TableSortLabel>
</TableCell>
<TableCell>
<TableSortLabel
active={orderBy === 'fileSize'}
direction={orderBy === 'fileSize' ? order : 'asc'}
onClick={() => onSort('fileSize')}
>
{t('size')}
</TableSortLabel>
</TableCell>
{!visitorMode && <TableCell align="right">{t('actions')}</TableCell>}
</TableRow>
</TableHead>
<TableBody>
{displayedVideos.map(video => (
<TableRow key={video.id} hover>
<TableCell sx={{ width: 140 }}>
<Box sx={{ position: 'relative', width: 120, height: 68 }}>
<Link to={`/video/${video.id}`} style={{ display: 'block', width: '100%', height: '100%' }}>
<Box
component="img"
src={getThumbnailSrc(video)}
alt={video.title}
sx={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: 1 }}
/>
</Link>
{!visitorMode && (
<Tooltip title={t('refreshThumbnail') || "Refresh Thumbnail"}>
<IconButton
size="small"
onClick={() => onRefreshThumbnail(video.id)}
disabled={refreshingId === video.id}
sx={{
position: 'absolute',
top: 0,
right: 0,
bgcolor: 'rgba(0,0,0,0.5)',
color: 'white',
'&:hover': { bgcolor: 'rgba(0,0,0,0.7)' },
p: 0.5,
width: 24,
height: 24
}}
>
{refreshingId === video.id ? <CircularProgress size={14} color="inherit" /> : <Refresh sx={{ fontSize: 16 }} />}
</IconButton>
</Tooltip>
)}
</Box>
<Typography variant="caption" display="block" sx={{ mt: 0.5, color: 'text.secondary', textAlign: 'center' }}>
{formatDuration(video.duration)}
</Typography>
</TableCell>
<TableCell sx={{ fontWeight: 500, maxWidth: 400 }}>
{editingVideoId === video.id ? (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<TextField
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
size="small"
fullWidth
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveTitle(video.id);
if (e.key === 'Escape') handleCancelEdit();
}}
/>
<IconButton
size="small"
color="primary"
onClick={() => handleSaveTitle(video.id)}
disabled={isSavingTitle}
>
{isSavingTitle ? <CircularProgress size={20} /> : <Check />}
</IconButton>
<IconButton
size="small"
color="error"
onClick={handleCancelEdit}
disabled={isSavingTitle}
>
<Close />
</IconButton>
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'flex-start' }}>
{!visitorMode && (
<IconButton
size="small"
onClick={() => handleEditClick(video)}
sx={{ mr: 1, mt: -0.5, opacity: 0.6, '&:hover': { opacity: 1 } }}
>
<Edit fontSize="small" />
</IconButton>
)}
<Typography
variant="body2"
sx={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: 1.4
}}
>
{video.title}
</Typography>
</Box>
)}
</TableCell>
<TableCell>
<Link
to={`/author/${encodeURIComponent(video.author)}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Typography
variant="body2"
sx={{
'&:hover': { textDecoration: 'underline', color: 'primary.main' }
}}
>
{video.author}
</Typography>
</Link>
</TableCell>
<TableCell>{formatSize(video.fileSize)}</TableCell>
{!visitorMode && (
<TableCell align="right">
<Tooltip title={t('deleteVideo')}>
<IconButton
color="error"
onClick={() => onDeleteClick(video.id)}
disabled={deletingId === video.id}
>
{deletingId === video.id ? <CircularProgress size={24} /> : <Delete />}
</IconButton>
</Tooltip>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<Alert severity="info" variant="outlined">{t('noVideosFoundMatching')}</Alert>
)}
{totalPages > 1 && (
<Box sx={{ mt: 2, mb: 4, display: 'flex', justifyContent: 'center' }}>
<Pagination
count={totalPages}
page={page}
onChange={onPageChange}
color="primary"
showFirstButton
showLastButton
/>
</Box>
)}
</Box>
);
};
export default VideosTable;