mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-02-05 23:13:03 -05:00
Bulk actions support (#1418)
This commit is contained in:
@@ -13,6 +13,7 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||
const [iosVideoRef, setIosVideoRef] = useState<HTMLVideoElement | null>(null);
|
||||
const [posterImage, setPosterImage] = useState<string | undefined>(undefined);
|
||||
const [isAudioFile, setIsAudioFile] = useState(false);
|
||||
|
||||
// Refs for hold-to-continue functionality
|
||||
const incrementIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -41,12 +42,13 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
setVideoUrl(url);
|
||||
|
||||
// Check if the media is an audio file and set poster image
|
||||
const isAudioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
const audioFile = url.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i) !== null;
|
||||
setIsAudioFile(audioFile);
|
||||
|
||||
// Get posterUrl from MEDIA_DATA, or use audio-poster.jpg as fallback for audio files when posterUrl is empty, null, or "None"
|
||||
const mediaPosterUrl = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.posterUrl) || '';
|
||||
const isValidPoster = mediaPosterUrl && mediaPosterUrl !== 'None' && mediaPosterUrl.trim() !== '';
|
||||
setPosterImage(isValidPoster ? mediaPosterUrl : (isAudioFile ? AUDIO_POSTER_URL : undefined));
|
||||
setPosterImage(isValidPoster ? mediaPosterUrl : (audioFile ? AUDIO_POSTER_URL : undefined));
|
||||
}, [videoRef]);
|
||||
|
||||
// Function to jump 15 seconds backward
|
||||
@@ -128,22 +130,34 @@ const IOSVideoPlayer = ({ videoRef, currentTime, duration }: IOSVideoPlayerProps
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* iOS-optimized Video Element with Native Controls */}
|
||||
<video
|
||||
ref={(ref) => setIosVideoRef(ref)}
|
||||
className="w-full rounded-md"
|
||||
src={videoUrl}
|
||||
controls
|
||||
playsInline
|
||||
webkit-playsinline="true"
|
||||
x-webkit-airplay="allow"
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<p>Your browser doesn't support HTML5 video.</p>
|
||||
</video>
|
||||
{/* Video container with persistent background for audio files */}
|
||||
<div className="ios-video-wrapper">
|
||||
{/* Persistent background image for audio files (Safari fix) */}
|
||||
{isAudioFile && posterImage && (
|
||||
<div
|
||||
className="ios-audio-poster-background"
|
||||
style={{ backgroundImage: `url(${posterImage})` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* iOS-optimized Video Element with Native Controls */}
|
||||
<video
|
||||
ref={(ref) => setIosVideoRef(ref)}
|
||||
className={`w-full rounded-md ${isAudioFile && posterImage ? 'audio-with-poster' : ''}`}
|
||||
src={videoUrl}
|
||||
controls
|
||||
playsInline
|
||||
webkit-playsinline="true"
|
||||
x-webkit-airplay="allow"
|
||||
preload="auto"
|
||||
crossOrigin="anonymous"
|
||||
poster={posterImage}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<p>Your browser doesn't support HTML5 video.</p>
|
||||
</video>
|
||||
</div>
|
||||
|
||||
{/* iOS Video Skip Controls */}
|
||||
<div className="ios-skip-controls mt-3 flex justify-center gap-4">
|
||||
|
||||
@@ -26,6 +26,18 @@ const mediaPageLinkStyles = {
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Helper function to parse time string (HH:MM:SS.mmm) to seconds
|
||||
const parseTimeToSeconds = (timeString: string): number => {
|
||||
const parts = timeString.split(':');
|
||||
if (parts.length !== 3) return 0;
|
||||
|
||||
const hours = parseInt(parts[0], 10) || 0;
|
||||
const minutes = parseInt(parts[1], 10) || 0;
|
||||
const seconds = parseFloat(parts[2]) || 0;
|
||||
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
};
|
||||
|
||||
interface TimelineControlsProps {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
@@ -191,7 +203,17 @@ const TimelineControls = ({
|
||||
setIsAutoSaving(true);
|
||||
|
||||
// Format segments data for API request - use ref to get latest segments and sort by start time
|
||||
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
|
||||
const chapters = clipSegmentsRef.current
|
||||
.filter((segment) => {
|
||||
// Filter out empty titles
|
||||
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
|
||||
return false;
|
||||
}
|
||||
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
|
||||
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
|
||||
return !isDefaultName;
|
||||
})
|
||||
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
|
||||
.map((chapter) => ({
|
||||
startTime: formatDetailedTime(chapter.startTime),
|
||||
@@ -199,7 +221,7 @@ const TimelineControls = ({
|
||||
chapterTitle: chapter.chapterTitle,
|
||||
}));
|
||||
|
||||
logger.debug('chapters', chapters);
|
||||
logger.debug('Filtered chapters (only custom titles):', chapters);
|
||||
|
||||
const mediaId = (typeof window !== 'undefined' && (window as any).MEDIA_DATA?.mediaId) || null;
|
||||
// For testing, use '1234' if no mediaId is available
|
||||
@@ -207,12 +229,13 @@ const TimelineControls = ({
|
||||
|
||||
logger.debug('mediaId', finalMediaId);
|
||||
|
||||
if (!finalMediaId || chapters.length === 0) {
|
||||
logger.debug('No mediaId or segments, skipping auto-save');
|
||||
if (!finalMediaId) {
|
||||
logger.debug('No mediaId, skipping auto-save');
|
||||
setIsAutoSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save chapters (empty array if no chapters have titles)
|
||||
logger.debug('Auto-saving segments:', { mediaId: finalMediaId, chapters });
|
||||
|
||||
const response = await autoSaveVideo(finalMediaId, { chapters });
|
||||
@@ -499,11 +522,20 @@ const TimelineControls = ({
|
||||
|
||||
try {
|
||||
// Format chapters data for API request - sort by start time first
|
||||
// ONLY save chapters that have custom titles - filter out chapters without titles or with default names
|
||||
const chapters = clipSegments
|
||||
.filter((segment) => segment.chapterTitle && segment.chapterTitle.trim())
|
||||
.filter((segment) => {
|
||||
// Filter out empty titles
|
||||
if (!segment.chapterTitle || !segment.chapterTitle.trim()) {
|
||||
return false;
|
||||
}
|
||||
// Filter out default chapter names like "Chapter 1", "Chapter 2", etc.
|
||||
const isDefaultName = /^Chapter \d+$/.test(segment.chapterTitle);
|
||||
return !isDefaultName;
|
||||
})
|
||||
.sort((a, b) => a.startTime - b.startTime) // Sort by start time chronologically
|
||||
.map((segment) => ({
|
||||
chapterTitle: segment.chapterTitle || `Chapter ${segment.id}`,
|
||||
chapterTitle: segment.chapterTitle,
|
||||
from: formatDetailedTime(segment.startTime),
|
||||
to: formatDetailedTime(segment.endTime),
|
||||
}));
|
||||
|
||||
@@ -353,8 +353,18 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
|
||||
return (
|
||||
<div className="video-player-container">
|
||||
{/* Persistent background image for audio files (Safari fix) */}
|
||||
{isAudioFile && posterImage && (
|
||||
<div
|
||||
className="audio-poster-background"
|
||||
style={{ backgroundImage: `url(${posterImage})` }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
|
||||
<video
|
||||
ref={videoRef}
|
||||
className={isAudioFile && posterImage ? 'audio-with-poster' : ''}
|
||||
preload="metadata"
|
||||
crossOrigin="anonymous"
|
||||
onClick={handleVideoClick}
|
||||
|
||||
Reference in New Issue
Block a user