'use client'; import { EventEmitter } from 'events'; import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; import { TopTitle } from '@gitroom/frontend/components/launches/helpers/top.title.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; import { useLaunchStore } from '@gitroom/frontend/components/new-launch/store'; const postUrlEmitter = new EventEmitter(); export const MediaSettingsLayout = () => { const [showPostSelector, setShowPostSelector] = useState(false); const [media, setMedia] = useState(undefined); const [callback, setCallback] = useState<{ callback: (tag: { id: string; name: string; path: string; thumbnail: string; alt: string; }) => void; // eslint-disable-next-line @typescript-eslint/no-empty-function } | null>({ callback: (params: { id: string; name: string; path: string; thumbnail: string; alt: string; }) => {}, } as any); useEffect(() => { postUrlEmitter.on( 'show', (params: { media: any; callback: (url: { id: string; name: string; path: string; thumbnail: string; alt: string; }) => void; }) => { setCallback(params); setMedia(params.media); setShowPostSelector(true); } ); return () => { setShowPostSelector(false); setCallback(null); setMedia(undefined); postUrlEmitter.removeAllListeners(); }; }, []); const close = useCallback(() => { setShowPostSelector(false); setCallback(null); setMedia(undefined); }, []); if (!showPostSelector) { return <>; } return ( ); }; export const useMediaSettings = () => { return useCallback((media: any) => { return new Promise((resolve) => { postUrlEmitter.emit('show', { media, callback: (value: any) => { resolve(value); }, }); }); }, []); }; export const CreateThumbnail: FC<{ onSelect: (blob: Blob, timestampMs: number) => void; media: | { id: string; name: string; path: string; thumbnail?: string; alt?: string; } | undefined; altText?: string; onAltTextChange?: (altText: string) => void; }> = (props) => { const { onSelect, media, altText, onAltTextChange } = props; const videoRef = useRef(null); const canvasRef = useRef(null); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [isLoaded, setIsLoaded] = useState(false); const [isCapturing, setIsCapturing] = useState(false); const handleLoadedMetadata = useCallback(() => { setDuration(videoRef?.current?.duration); setIsLoaded(true); }, []); const handleTimeUpdate = useCallback(() => { setCurrentTime(videoRef?.current?.currentTime); }, []); const handleSeek = useCallback((e: React.ChangeEvent) => { const time = parseFloat(e.target.value); if (videoRef.current) { videoRef.current.currentTime = time; setCurrentTime(time); } }, []); const captureFrame = useCallback(async () => { setIsCapturing(true); try { const video = videoRef.current; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) { setIsCapturing(false); return; } // Set canvas dimensions to match video canvas.width = video.videoWidth; canvas.height = video.videoHeight; // Draw current frame to canvas ctx.drawImage(video, 0, 0, canvas.width, canvas.height); // Get timestamp in milliseconds const timestampMs = Math.round(currentTime * 1000); // Convert canvas to blob canvas.toBlob( (blob: Blob | null) => { if (blob) { onSelect(blob, timestampMs); } setIsCapturing(false); }, 'image/jpeg', 0.8 ); } catch (error) { console.error('Error capturing frame:', error); setIsCapturing(false); // Fallback: try to capture using a different approach try { const video = videoRef.current; if (video) { // Create a temporary canvas element const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); if (tempCtx) { tempCanvas.width = video.videoWidth; tempCanvas.height = video.videoHeight; tempCtx.drawImage(video, 0, 0); // Get timestamp in milliseconds const timestampMs = Math.round(currentTime * 1000); tempCanvas.toBlob( (blob: Blob | null) => { if (blob) { onSelect(blob, timestampMs); } setIsCapturing(false); }, 'image/jpeg', 0.8 ); } } } catch (fallbackError) { console.error('Fallback capture also failed:', fallbackError); alert( 'Unable to capture frame. This might be due to CORS restrictions on the video source.' ); setIsCapturing(false); } } }, [onSelect, currentTime]); const formatTime = useCallback((seconds: number) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }, []); if (!media) return null; return (
{isLoaded && ( <>
{formatTime(currentTime)} {formatTime(duration)}
)}
); }; export const MediaComponentInner: FC<{ onClose: () => void; onSelect: (media: { id: string; name: string; path: string; thumbnail: string; alt: string; }) => void; media: | { id: string; name: string; path: string; thumbnail: string; alt: string; thumbnailTimestamp?: number; } | undefined; }> = (props) => { const { onClose, onSelect, media } = props; const setActivateExitButton = useLaunchStore((e) => e.setActivateExitButton); const newFetch = useFetch(); const [newThumbnail, setNewThumbnail] = useState(null); const [isEditingThumbnail, setIsEditingThumbnail] = useState(false); const [altText, setAltText] = useState(media?.alt || ''); const [loading, setLoading] = useState(false); const [thumbnail, setThumbnail] = useState( props.media?.thumbnail || null ); const [thumbnailTimestamp, setThumbnailTimestamp] = useState( props.media?.thumbnailTimestamp || null ); useEffect(() => { setActivateExitButton(false); return () => { setActivateExitButton(true); }; }, []); const save = useCallback(async () => { setLoading(true); let path = thumbnail || ''; if (newThumbnail) { const blob = await (await fetch(newThumbnail)).blob(); const formData = new FormData(); formData.append('file', blob, 'media.jpg'); formData.append('preventSave', 'true'); const data = await ( await newFetch('/media/upload-simple', { method: 'POST', body: formData, }) ).json(); path = data.path; } const media = await ( await newFetch('/media/information', { method: 'POST', body: JSON.stringify({ id: props.media.id, alt: altText, thumbnail: path, thumbnailTimestamp: thumbnailTimestamp, }), }) ).json(); onSelect(media); onClose(); }, [altText, newThumbnail, thumbnail, thumbnailTimestamp]); return (
setAltText(e.target.value)} placeholder="Describe the image/video content..." className="w-full px-3 py-2 bg-fifth border border-tableBorder rounded-lg text-textColor placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-forth focus:border-transparent" />
{media?.path.indexOf('mp4') > -1 && ( <> {/* Alt Text Input */}
{!isEditingThumbnail ? (
{/* Show existing thumbnail if it exists */} {(newThumbnail || thumbnail) && (
Current Thumbnail: Current thumbnail
)} {/* Action Buttons */}
{(thumbnail || newThumbnail) && ( )}
) : (
{/* Back button */}
{/* Thumbnail Editor */} { // Convert blob to base64 or handle as needed const reader = new FileReader(); reader.onload = () => { // You can handle the result here - for now just call onSelect with the blob URL const url = URL.createObjectURL(blob); setNewThumbnail(url); setThumbnailTimestamp(timestampMs); setIsEditingThumbnail(false); }; reader.readAsDataURL(blob); }} media={media} altText={altText} onAltTextChange={setAltText} />
)}
)} {!isEditingThumbnail && (
)}
); };