import { resolveTxt } from 'dns'; import { videointelligence } from 'googleapis/build/src/apis/videointelligence'; import { isInteger } from 'lodash'; import * as React from 'react'; import { useEffect, useState, useCallback, useRef, useMemo } from "react" import "./ProgressBar.scss" import { MediaSegment } from './RecordingView'; interface ProgressBarProps { videos: MediaSegment[], setVideos: React.Dispatch>, orderVideos: boolean, progress: number, recording: boolean, } interface SegmentBox { endTime: number, startTime: number, order: number, } interface CurrentHover { index: number, minX: number, maxX: number } export function ProgressBar(props: ProgressBarProps) { const progressBarRef = useRef(null) // array for the order of video segments const [segments, setSegments] = useState([]); const [ordered, setOrdered] = useState([]); const [dragged, setDragged] = useState(-1); // total length of the video, in seconds*100 // const [totalTime, setTotalTime] = useState(0); // length of the time removed from the video, in seconds*100 const [totalRemovedTime, setTotalRemovedTime] = useState(0); // this holds the index of the videoc segment to be removed const [removed, setRemoved] = useState(-1); // const totalTime = useMemo(() => props.progress*1000 - totalRemovedTime, [props.progress, totalRemovedTime]); // const totalTime = useMemo(() => props.videos.lastElement().endTime, [props.videos]) // const totalTime = () => props.videos.lastElement().endTime // const memoTotal = useMemo(totalTime, [props.videos]) // useEffect that updates total time based on progress // useEffect(() => { // console.log("useEffect progress", props.progress, 'totalRemovedTime', totalRemovedTime) // // setTotalTime(prev => { // // // progress is in seconds, prev is in deciseconds? // // const toAdd = props.progress * 10 - prev // // return prev + toAdd // // }) // // setTotalTime(prev => props.progress * 10) // }, [props.progress]) useEffect(() => { console.log("useEffect recording", props.recording) segments.forEach((seg, i) => { const htmlId = seg.props.id; const segmentHtml = document.getElementById(htmlId); segmentHtml?.classList.toggle('no-transition', props.recording); console.log(segmentHtml) }); }, [props.recording]) useEffect(() => { console.log('useEffect dragged, ordered') const totalTime = props.progress * 1000 - totalRemovedTime; const segmentsJSX = ordered.map((seg, i) =>
{seg.order}
); setSegments(segmentsJSX) }, [dragged, ordered]); // to imporve performance, we only want to update the width, not re-render the whole thing useEffect(() => { // console.log('updating width on progress useEffect') const totalTime = props.progress * 1000 - totalRemovedTime; segments.forEach((seg, i) => { const htmlId = seg.props.id; const segmentHtml = document.getElementById(htmlId); segmentHtml?.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`; // console.log('updating width on progress useEffect', htmlId, segmentHtml, segmentHtml?.style.width) }); }, [props.progress]); useEffect(() => { // this useEffect fired when the videos are being rearragned to the order // in this case, do nothing. if (props.orderVideos) return; const order = props.videos.length // in this case, a new video is added if (order && order > ordered.length) { const { endTime, startTime } = props.videos.lastElement(); // update total time // setTotalTime(prevTime => prevTime + (endTime - startTime)); // add the new segment to the ordered array setOrdered(prevOrdered => { return [...prevOrdered, { endTime, startTime , order }]; }); } // in this case, a segment is removed else if (order < ordered.length) { // update the total time // setTotalTime(prevTime => prevTime - (ordered[removed].endTime - ordered[removed].startTime)); // update total removed time setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime)); // remove the segment from the ordered array and decrement the order of the remaining segments // if they are greater than the removed segment's order const removedOrder = ordered[removed].order; setOrdered(prevOrdered => prevOrdered.reduce((acc, seg, i) => { (i !== removed) && acc.push({ ...seg, order: seg.order > removedOrder ? seg.order - 1 : seg.order }); return acc; },[] as SegmentBox[]) ); // reset to default/nullish state setRemoved(-1); } }, [props.videos]); useEffect(() => { props.setVideos(vids => ordered.map((seg) => vids[seg.order - 1])); }, [props.orderVideos]); useEffect(() => { if (removed === -1) return; console.log('removed', removed) // update the videos array -> this will fire the useEffect above, where we update the orders & totalTime const index = ordered[removed].order - 1; props.setVideos(vids => vids.filter((vid, i) => i !== index)); }, [removed]); const updateLastHover = (segId: number): CurrentHover | null => { // get the segId of the segment that will become the new bounding area const rect = progressBarRef.current?.children[segId].getBoundingClientRect() if (rect == null) return null return { index: segId, minX: rect.x, maxX: rect.x + rect.width, } } const onPointerDown = (e: React.PointerEvent) => { // don't move the videobox element e.stopPropagation() // get the segment the user clicked on to be dragged const clickedSegment = e.target as HTMLDivElement & EventTarget // get the profess bar ro add event listeners // don't do anything if null const progressBar = progressBarRef.current if (progressBar == null || clickedSegment.id === progressBar.id) return // if holding shift key, let's remove that segment // TODO: think of a way to accomodate touch -> maybe if poiuntermove isn't fired after x secs? if (e.shiftKey) { const segId = parseInt(clickedSegment.id.split('-')[1]); console.log('removing segment', segId) setRemoved(segId); return } const ptrId = e.pointerId; progressBar.setPointerCapture(ptrId) const rect = clickedSegment.getBoundingClientRect() // id for segment is like 'segment-1' or 'segment-10', // so this works to get the id const segId = parseInt(clickedSegment.id.split('-')[1]) // set the selected segment to be the one dragged setDragged(segId) // this is the logic for storing the lower X bound and upper X bound // to know whether a swap is needed between two segments let lastHover: CurrentHover = { index: segId, minX: rect.x, maxX: rect.x + rect.width, } // create the div element that tracks the cursor const detchedSegment = document.createElement("div") initDeatchSegment(detchedSegment, rect); const updateSegmentOrder = (event: PointerEvent): void => { event.stopPropagation(); event.preventDefault(); // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged console.log('update cursor', progressBar.hasPointerCapture(ptrId), ptrId) if (!progressBar.hasPointerCapture(ptrId)) { placeSegmentandCleanup(); return; } followCursor(event, detchedSegment, rect) const curX = event.clientX; if (curX < lastHover.minX && lastHover.index > 0) { swapSegments(lastHover.index, lastHover.index - 1) lastHover = updateLastHover(lastHover.index - 1) ?? lastHover } else if (curX > lastHover.maxX && lastHover.index < segments.length - 1) { swapSegments(lastHover.index, lastHover.index + 1) lastHover = updateLastHover(lastHover.index + 1) ?? lastHover } } const placeSegmentandCleanup = (event?: PointerEvent): void => { event?.stopPropagation(); event?.preventDefault(); // remove the update event listener for pointermove progressBar.removeEventListener('pointermove', updateSegmentOrder), { once: true } // remove the floating segment from the DOM detchedSegment.remove() // dragged is -1 is equiv to nothing being dragged, so the normal state // so this will place the segment in it's location and update the segment bar setDragged(-1); } progressBar.addEventListener('pointermove', updateSegmentOrder) progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true }) } const swapSegments = (oldIndex: number, newIndex: number) => { if (newIndex == null) return setOrdered(prevOrdered => { const cpy = [...prevOrdered] cpy[oldIndex] = cpy[newIndex] cpy[newIndex] = prevOrdered[oldIndex] return cpy }) setDragged(newIndex) } const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => { dot.classList.add("segment-selected") dot.style.transitionDuration = '0s'; dot.style.position = 'absolute'; dot.style.zIndex = '999'; dot.style.width = `${rect.width}px`; dot.style.height = `${rect.height}px`; dot.style.left = `${rect.x}px`; dot.style.top = `${rect.y}px`; dot.draggable = false; document.body.append(dot) } const cleanupDetachSegment = (dot: HTMLDivElement) => { dot.remove() } const followCursor = (event: PointerEvent, dot: HTMLDivElement, rect: DOMRect): void => { // event.stopPropagation() // const { width, height } = dot.getBoundingClientRect() const { width, height } = rect; dot.style.left = `${event.clientX - width/2}px`; dot.style.top = `${event.clientY - height/2}px`; } return (
{/*
*/} {segments}
) }