// noinspection SpellCheckingInspection import { createSignal, onCleanup, createEffect } from "solid-js"; import { formatMsToTime } from "@/entrypoints/content/utils"; import "./style.css"; import "./dragControls.css"; interface ISavingControls { onClose: ( save: boolean, obj?: { blob?: Blob; name?: string; comment?: string; useHook?: boolean; thumbnail?: string; crop?: [number, number]; }, ) => void; getVideoData: () => Promise; getErrorEvents: () => Promise<{title:string,time:number}> } const base64ToBlob = (base64: string) => { const splitStr = base64.split(","); const len = splitStr.length; const byteString = atob(splitStr[len - 1]); const ab = new ArrayBuffer(byteString.length); const ia = new Uint8Array(ab); for (let i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new Blob([ab], { type: "video/webm" }); }; function SavingControls({ onClose, getVideoData, getErrorEvents }: ISavingControls) { const [name, setName] = createSignal(`Issues in — ${document.title}`); const [description, setDescription] = createSignal(""); const [currentTime, setCurrentTime] = createSignal(0); const [duration, setDuration] = createSignal(0); const [playing, setPlaying] = createSignal(false); const [trimBounds, setTrimBounds] = createSignal([0, 0]); const [videoData, setVideoData] = createSignal(undefined); const [videoBlob, setVideoBlob] = createSignal(undefined); const [processing, setProcessing] = createSignal(false); const [startPos, setStartPos] = createSignal(0); const [endPos, setEndPos] = createSignal(100); const [dragging, setDragging] = createSignal(null); const [isTyping, setIsTyping] = createSignal(false); const [errorEvents, setErrorEvents] = createSignal([]) createEffect(() => { setTrimBounds([0, 0]); getErrorEvents().then(r => { setErrorEvents(r) }) }); const spacePressed = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || isTyping() ) { return; } e.preventDefault() e.stopPropagation() if (e.key === " ") { if (playing()) { pause(); } else { resume(); } } }; createEffect(() => { window.addEventListener("keydown", spacePressed); onCleanup(() => window.removeEventListener("keydown", spacePressed)); }); const convertToPercentage = (clientX: number, element: HTMLElement) => { const rect = element.getBoundingClientRect(); const x = clientX - rect.left; return (x / rect.width) * 100; }; const startDrag = (marker: "start" | "end" | "body") => (event: MouseEvent) => { event.stopPropagation(); setDragging(marker); }; const onDrag = (event: MouseEvent) => { if (dragging() && event.clientX !== 0) { const newPos = convertToPercentage( event.clientX, event.currentTarget as HTMLElement, ); if (dragging() === "start") { if (endPos() - newPos <= 1 || newPos < 0 || newPos > 100) { return; } setStartPos(newPos); } else if (dragging() === "end") { if (newPos - startPos() <= 1 || newPos < 0 || newPos > 100) { return; } setEndPos(newPos); } onTrimChange( (startPos() / 100) * duration(), (endPos() / 100) * duration(), ); } }; const endDrag = () => { setDragging(null); }; onCleanup(() => { setDragging(null); }); if (videoData() === undefined) { getVideoData().then(async (data: Record) => { const fullData = data.base64data.join(""); const blob = base64ToBlob(fullData); const blobUrl = URL.createObjectURL(blob); setVideoBlob(blob); setVideoData(blobUrl); }); } let videoRef: HTMLVideoElement; const onSave = async () => { setProcessing(true); const thumbnail = await generateThumbnail(); setProcessing(false); const bounds = trimBounds(); const trim = bounds[0] + bounds[1] === 0 ? null : (bounds.map((i: number) => Math.round(i * 1000)) as [number, number]); const dataObj = { blob: videoBlob(), name: name(), comment: description(), useHook: false, thumbnail, crop: trim, }; onClose(true, dataObj); }; const onCancel = () => { onClose(false); }; const pause = () => { videoRef.pause(); setPlaying(false); }; const resume = () => { void videoRef.play(); setPlaying(true); }; const updateCurrentTime = () => { setCurrentTime(videoRef.currentTime); }; const generateThumbnail = async (): Promise => { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (!context) return ""; let thumbnailRes = ""; const aspectRatio = videoRef.videoWidth / videoRef.videoHeight; const width = 1080; const height = width / aspectRatio; canvas.width = width; canvas.height = height; videoRef.currentTime = duration() ? duration() : 3; context.drawImage(videoRef, 0, 0, canvas.width, canvas.height); thumbnailRes = canvas.toDataURL("image/jpeg", 0.7); return new Promise((res) => { const interval = setInterval(() => { if (thumbnailRes) { clearInterval(interval); res(thumbnailRes); } }, 100); }); }; const getDuration = async () => { videoRef.currentTime = 1e101; await new Promise((resolve) => { videoRef.ontimeupdate = () => { videoRef.ontimeupdate = null; resolve(videoRef.duration); }; }); setTimeout(() => { videoRef.currentTime = 0; }, 25); return videoRef.duration; }; const onMetaLoad = async () => { let videoDuration = videoRef.duration; if (videoDuration === Infinity || Number.isNaN(videoDuration)) { videoDuration = await getDuration(); } setDuration(videoDuration); void generateThumbnail(); }; const onVideoEnd = () => { setPlaying(false); }; const setVideoRef = (el: HTMLVideoElement) => { videoRef = el; videoRef.addEventListener("loadedmetadata", onMetaLoad); videoRef.addEventListener("ended", onVideoEnd); }; const round = (num: number) => { return Math.round((num + Number.EPSILON) * 100) / 100; }; const onTrimChange = (a: number, b: number) => { const start = round(a); const end = round(b); setTrimBounds([start, end]); }; const getTrimDuration = () => { const [trimStart, trimEnd] = trimBounds(); return trimEnd - trimStart; }; const pageUrl = document.location.href; let dialogRef: HTMLDialogElement; createEffect(() => { if (dialogRef) { dialogRef.showModal(); } }); const safeUrl = pageUrl.length > 60 ? pageUrl.slice(0, 60) + "..." : pageUrl; const int = setInterval(() => { updateCurrentTime(); }, 100); onCleanup(() => { clearInterval(int); videoRef.removeEventListener("loadedmetadata", onMetaLoad); videoRef.removeEventListener("ended", onVideoEnd); }); return ( (dialogRef = el)} id="editRecording" class="modal save-controls" >