mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-13 13:22:57 +00:00
Feature/ add video 2 (#8)
* api point works * upload api calls done, make better all components state changes, some asesthetic improvements * loading screens for the add video proccess * added and uploaded video now in the uiEvents for the Engine to listen and read --------- Co-authored-by: wil <wil@storyteller.ai>
This commit is contained in:
1 parent
87eaa60716
commit
2bb35f9560
17 files changed
+405
-162
No files matched your search
@@ -142,26 +142,36 @@ export class MediaUploadApi extends ApiManager {
|
||||
blob,
|
||||
fileName,
|
||||
uuid,
|
||||
is_intermediate_system_file = true,
|
||||
maybe_title,
|
||||
maybe_visibility = Visibility.Public,
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
maybe_trim_end_millis,
|
||||
maybe_trim_start_millis,
|
||||
}: {
|
||||
blob: Blob;
|
||||
fileName: string;
|
||||
uuid: string;
|
||||
is_intermediate_system_file?: boolean;
|
||||
maybe_title?: string;
|
||||
maybe_visibility?: Visibility;
|
||||
maybe_style_name?: string;
|
||||
maybe_scene_source_media_file_token?: string;
|
||||
maybe_trim_end_millis?: number;
|
||||
maybe_trim_start_millis?: number;
|
||||
}): Promise<ApiResponse<string>> {
|
||||
const endpoint = `${this.ApiTargets.BaseApi}/v1/media_files/upload/new_video`;
|
||||
const options: Record<string, string | number | undefined> = {
|
||||
is_intermediate_system_file: "true",
|
||||
is_intermediate_system_file: is_intermediate_system_file
|
||||
? "true"
|
||||
: "false",
|
||||
maybe_title,
|
||||
maybe_visibility: maybe_visibility?.toString(),
|
||||
maybe_style_name,
|
||||
maybe_scene_source_media_file_token,
|
||||
maybe_trim_end_millis,
|
||||
maybe_trim_start_millis,
|
||||
};
|
||||
return this.Upload({ endpoint, blob, fileName, uuid, options });
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface MediaFile {
|
||||
maybe_duration_millis: number | null;
|
||||
maybe_style_name: null | string;
|
||||
public_bucket_path: string;
|
||||
public_bucket_url: string;
|
||||
cover_image: {
|
||||
maybe_cover_image_public_bucket_path: null | string;
|
||||
default_cover: {
|
||||
|
||||
@@ -54,8 +54,8 @@ export class Engine {
|
||||
this.addImage(image);
|
||||
});
|
||||
uiEvents.onGetStagedVideo((video) => {
|
||||
console.log("Engine got trim data: ", video.trimData);
|
||||
this.addVideo(video.file);
|
||||
console.log("Engine got video: " + video.url);
|
||||
// this.addVideo(video.file);
|
||||
});
|
||||
|
||||
// TODO: You may listen to all the image toolbar events here
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useCallback } from "react";
|
||||
import { Signal } from "@preact/signals-react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { MediaUploadApi, MediaFilesApi } from "~/Classes/ApiManager";
|
||||
import { TrimData } from "./TrimmerPlaybar";
|
||||
import { DialogAddMediaStatuses } from "./enums";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
import { dispatchUiEvents } from "~/signals";
|
||||
|
||||
export const ButtonSubmitAdd = ({
|
||||
file,
|
||||
trimData,
|
||||
onStatusChanged,
|
||||
}: {
|
||||
file: File | null;
|
||||
trimData: Signal<TrimData | undefined>;
|
||||
onStatusChanged: (newStatus: DialogAddMediaStatuses) => void;
|
||||
}) => {
|
||||
const handleAdd = useCallback(async () => {
|
||||
if (file && trimData.value) {
|
||||
// setup payload
|
||||
const payload = {
|
||||
blob: file,
|
||||
fileName: file.name,
|
||||
uuid: uuidv4(),
|
||||
is_intermediate_system_file: false,
|
||||
maybe_trim_start_millis: Math.round(trimData.value.trimStartMs),
|
||||
maybe_trim_end_millis: Math.round(trimData.value.trimEndMs),
|
||||
};
|
||||
console.log("Add Video Payload >> ", payload);
|
||||
|
||||
// upload the file
|
||||
onStatusChanged(DialogAddMediaStatuses.FILE_UPLOADING);
|
||||
const meaidaUpload = new MediaUploadApi();
|
||||
const fileUploadResponse = await meaidaUpload.UploadNewVideo(payload);
|
||||
console.log("Add Video Response >> ", fileUploadResponse);
|
||||
|
||||
// if upload fails
|
||||
if (!fileUploadResponse.success || !fileUploadResponse.data) {
|
||||
onStatusChanged(DialogAddMediaStatuses.ERROR_FILE_UPLOAD);
|
||||
return;
|
||||
}
|
||||
|
||||
// request the file record
|
||||
onStatusChanged(DialogAddMediaStatuses.FILE_RECORD_REQUESTING);
|
||||
const mediaFiles = new MediaFilesApi();
|
||||
const recordRequestResponse = await mediaFiles.GetMediaFileByToken({
|
||||
mediaFileToken: fileUploadResponse.data,
|
||||
});
|
||||
|
||||
// if reqest fails
|
||||
if (!recordRequestResponse.success || !recordRequestResponse.data) {
|
||||
onStatusChanged(DialogAddMediaStatuses.ERROR_FILE_RECORD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
// return the good result
|
||||
onStatusChanged(DialogAddMediaStatuses.FILE_RECORD_RECEIVED);
|
||||
console.log(recordRequestResponse);
|
||||
dispatchUiEvents.addVideoToEngine({
|
||||
url: recordRequestResponse.data.public_bucket_url,
|
||||
});
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
disabled={file === null || trimData === undefined}
|
||||
>
|
||||
Add Video
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
|
||||
import {
|
||||
dialogBackgroundStyles,
|
||||
paperWrapperStyles,
|
||||
} from "~/components/styles";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
import { QuickTrimVideoUploader, TrimData } from "./QuickTrimVideoUploader";
|
||||
import { ButtonSubmitAdd } from "./ButtonSumbitAdd";
|
||||
|
||||
import { DialogAddMediaStatuses } from "./enums";
|
||||
import { LoadingScreens } from "./LoadingScreens";
|
||||
// import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
|
||||
const initialState = {
|
||||
file: null,
|
||||
dialogStatus: DialogAddMediaStatuses.STAGING_FILE,
|
||||
};
|
||||
export const DialogAddVideo = ({
|
||||
isOpen,
|
||||
closeCallback,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
}) => {
|
||||
// useRenderCounter("DialogAddVideo");
|
||||
|
||||
const [{ file, dialogStatus }, setStates] = useState<{
|
||||
file: File | null;
|
||||
dialogStatus: DialogAddMediaStatuses;
|
||||
}>(initialState);
|
||||
const trimData = signal<TrimData | undefined>(undefined);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
closeCallback();
|
||||
}, [closeCallback]);
|
||||
|
||||
const changeDialogStatus = useCallback(
|
||||
(newStatus: DialogAddMediaStatuses) => {
|
||||
setStates((curr) => ({ ...curr, dialogStatus: newStatus }));
|
||||
if (newStatus === DialogAddMediaStatuses.FILE_RECORD_RECEIVED) {
|
||||
setTimeout(handleClose, 1000);
|
||||
setTimeout(() => setStates(initialState), 1100);
|
||||
}
|
||||
},
|
||||
[handleClose],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={closeCallback}
|
||||
unmount={true}
|
||||
className="relative z-50"
|
||||
>
|
||||
<div className={dialogBackgroundStyles}>
|
||||
<DialogPanel className="w-full max-w-5xl">
|
||||
<div
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex w-full max-w-5xl flex-col gap-4 px-6 py-4",
|
||||
)}
|
||||
style={{ height: "calc(100vh - 200px)" }}
|
||||
>
|
||||
<DialogTitle className="font-bold">Upload Video</DialogTitle>
|
||||
{dialogStatus === DialogAddMediaStatuses.STAGING_FILE && (
|
||||
<>
|
||||
<QuickTrimVideoUploader
|
||||
file={file}
|
||||
onFileStaged={(file) => {
|
||||
setStates((curr) => ({ ...curr, file }));
|
||||
}}
|
||||
trimData={trimData}
|
||||
onTrimChange={(newTrimData: TrimData) => {
|
||||
trimData.value = newTrimData;
|
||||
}}
|
||||
/>
|
||||
<span className="grow" />
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<ButtonSubmitAdd
|
||||
file={file}
|
||||
trimData={trimData}
|
||||
onStatusChanged={changeDialogStatus}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<LoadingScreens currStatus={dialogStatus} />
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { LoadingSpinner } from "~/components/ui";
|
||||
import { faCircleCheck } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { DialogAddMediaStatuses } from "./enums";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
export const LoadingScreens = ({
|
||||
currStatus,
|
||||
}: {
|
||||
currStatus: DialogAddMediaStatuses;
|
||||
}) => {
|
||||
if (currStatus === DialogAddMediaStatuses.FILE_UPLOADING) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner isShowing={true} message="Uploading File..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (currStatus === DialogAddMediaStatuses.FILE_RECORD_REQUESTING) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoadingSpinner isShowing={true} message="Processing File..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (currStatus === DialogAddMediaStatuses.ERROR_FILE_UPLOAD) {
|
||||
return (
|
||||
<ErrorScreens
|
||||
title="Fail to Upload File"
|
||||
message="Your video maybe too long, the file maybe too big, or it maybe malformed. Please try again."
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (currStatus === DialogAddMediaStatuses.ERROR_FILE_RECORD_REQUEST) {
|
||||
return (
|
||||
<ErrorScreens
|
||||
title="Fail to Process File"
|
||||
message="Your video maybe too long, the file maybe too big, or it maybe malformed. Please try again."
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (currStatus === DialogAddMediaStatuses.FILE_RECORD_RECEIVED) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center gap-4">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleCheck}
|
||||
className="size-10 text-green-500"
|
||||
/>
|
||||
<p>Upload and proccessing is successful!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const ErrorScreens = ({
|
||||
title,
|
||||
message,
|
||||
retry,
|
||||
}: {
|
||||
title: string;
|
||||
message: string;
|
||||
retry?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
<p>{message}</p>
|
||||
<Button onClick={retry}>Retry</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { VideoControls } from "./VideoControls";
|
||||
import { TrimmerPlaybar, TrimData } from "./TrimmerPlaybar";
|
||||
export type { TrimData };
|
||||
export const QuickTrimVideoPlayer = ({
|
||||
file,
|
||||
onTrimChange,
|
||||
}: {
|
||||
file: File;
|
||||
onTrimChange: (trimData: TrimData) => void;
|
||||
}) => {
|
||||
const [vidEl, setVidEl] = useState<HTMLVideoElement | undefined>(undefined);
|
||||
|
||||
const videoRefCallback = useCallback(
|
||||
(node: HTMLVideoElement) => {
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
setVidEl(node);
|
||||
}
|
||||
},
|
||||
[
|
||||
// No Dependency !
|
||||
],
|
||||
); //END videoRefCallback\
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="relative w-full bg-black"
|
||||
style={{ height: "calc(100vh - 500px)" }}
|
||||
>
|
||||
<video
|
||||
className="max-w-8/12 mx-auto max-h-full w-full"
|
||||
ref={videoRefCallback}
|
||||
src={URL.createObjectURL(file)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center bg-gray-100">
|
||||
<VideoControls vidEl={vidEl} className="w-fit" />
|
||||
<TrimmerPlaybar
|
||||
vidEl={vidEl}
|
||||
className="grow"
|
||||
onTrimChange={onTrimChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Signal } from "@preact/signals-react";
|
||||
|
||||
import { FileUploader, VIDEO_FILE_TYPE } from "../FileUploader";
|
||||
import { VideoControls } from "./VideoControls";
|
||||
import { TrimmerPlaybar, TrimData } from "./TrimmerPlaybar";
|
||||
|
||||
export type { TrimData };
|
||||
|
||||
export const QuickTrimVideoUploader = ({
|
||||
file,
|
||||
onFileStaged,
|
||||
onTrimChange,
|
||||
trimData,
|
||||
}: {
|
||||
file: File | null;
|
||||
onFileStaged: (newFile: File | null) => void;
|
||||
onTrimChange: (trimData: TrimData) => void;
|
||||
trimData?: Signal<TrimData | undefined>;
|
||||
}) => {
|
||||
const [vidEl, setVidEl] = useState<HTMLVideoElement | undefined>(undefined);
|
||||
|
||||
const videoRefCallback = useCallback(
|
||||
(node: HTMLVideoElement) => {
|
||||
if (node !== null) {
|
||||
// DOM node referenced by ref has changed and exists
|
||||
setVidEl(node);
|
||||
}
|
||||
},
|
||||
[
|
||||
// No Dependency !
|
||||
],
|
||||
); //END videoRefCallback\
|
||||
|
||||
return (
|
||||
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
|
||||
<FileUploader
|
||||
title=""
|
||||
fileTypes={Object.values(VIDEO_FILE_TYPE)}
|
||||
file={file}
|
||||
setFile={async (file: File | null) => {
|
||||
onFileStaged(file);
|
||||
}}
|
||||
/>
|
||||
{file && (
|
||||
<>
|
||||
<div
|
||||
className="relative w-full bg-black"
|
||||
style={{ height: "calc(100vh - 500px)" }}
|
||||
>
|
||||
<video
|
||||
className="max-w-8/12 mx-auto max-h-full w-full"
|
||||
ref={videoRefCallback}
|
||||
src={URL.createObjectURL(file)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-center bg-gray-100">
|
||||
<VideoControls vidEl={vidEl} className="w-fit" />
|
||||
<TrimmerPlaybar
|
||||
trimData={trimData?.value}
|
||||
vidEl={vidEl}
|
||||
className="grow"
|
||||
onTrimChange={onTrimChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -14,10 +14,12 @@ import { TrimData } from "./utilities";
|
||||
|
||||
export const TrimmerPlaybarCore = ({
|
||||
vidEl,
|
||||
trimData,
|
||||
className,
|
||||
onTrimChange,
|
||||
}: {
|
||||
vidEl: HTMLVideoElement;
|
||||
trimData?: TrimData;
|
||||
className?: string;
|
||||
onTrimChange: (trimData: TrimData) => void;
|
||||
}) => {
|
||||
@@ -84,9 +86,10 @@ export const TrimmerPlaybarCore = ({
|
||||
...prev,
|
||||
durationMs: vidEl.duration * 1000,
|
||||
currentTimeMs: vidEl.currentTime * 1000,
|
||||
trimStartMs: 0,
|
||||
trimEndMs:
|
||||
vidEl.duration * 1000 >= MAX_TRIM_DURATION
|
||||
trimStartMs: trimData?.trimStartMs ?? 0,
|
||||
trimEndMs: trimData?.trimEndMs
|
||||
? trimData.trimEndMs
|
||||
: vidEl.duration * 1000 >= MAX_TRIM_DURATION
|
||||
? MAX_TRIM_DURATION
|
||||
: vidEl.duration * 1000,
|
||||
}));
|
||||
@@ -102,8 +105,8 @@ export const TrimmerPlaybarCore = ({
|
||||
vidEl.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
vidEl.addEventListener("timeupdate", handleTimeupdate);
|
||||
return () => {
|
||||
vidEl.removeEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
vidEl.removeEventListener("timeupdate", handleTimeupdate);
|
||||
vidEl.removeEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
};
|
||||
}, [vidEl]);
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ import { TrimData } from "./utilities";
|
||||
export type { TrimData };
|
||||
export const TrimmerPlaybar = ({
|
||||
vidEl,
|
||||
trimData,
|
||||
className,
|
||||
onTrimChange,
|
||||
}: {
|
||||
vidEl: HTMLVideoElement | undefined;
|
||||
className?: string;
|
||||
trimData?: TrimData;
|
||||
onTrimChange: (trimData: TrimData) => void;
|
||||
}) => {
|
||||
if (!vidEl) {
|
||||
@@ -18,6 +20,7 @@ export const TrimmerPlaybar = ({
|
||||
<TrimmerPlaybarCore
|
||||
vidEl={vidEl}
|
||||
className={className}
|
||||
trimData={trimData}
|
||||
onTrimChange={onTrimChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ export type TrimData = {
|
||||
|
||||
export function formatSecondsToHHMMSSCS(seconds: number) {
|
||||
//example of the ISO String: 1970-01-01T00:01:40.774Z
|
||||
const isoString = new Date(seconds * 1000).toISOString();
|
||||
const isoString = new Date(Math.round(seconds * 1000)).toISOString();
|
||||
if (seconds > 3600)
|
||||
return isoString.substring(11, 19) + "." + isoString.substring(20, 22);
|
||||
else return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export function LabelTimeDuration({ vidEl }: { vidEl: HTMLVideoElement }) {
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
const safeGetDuration = useCallback(() => {
|
||||
return typeof vidEl.duration === "number" ? vidEl.duration : 0;
|
||||
}, [vidEl]);
|
||||
|
||||
const [{ currentTime, duration }, setState] = useState<{
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
}>({ currentTime: 0, duration: safeGetDuration() });
|
||||
|
||||
const setCurrentTime = useCallback((newCurrentTime: number) => {
|
||||
setState((curr) => ({ ...curr, currentTime: newCurrentTime }));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTimeStamp = () => setCurrentTime(vidEl.currentTime || 0);
|
||||
|
||||
const handleLoadedmetadata = () => {
|
||||
setState((curr) => ({
|
||||
...curr,
|
||||
duration: safeGetDuration(),
|
||||
}));
|
||||
};
|
||||
vidEl.addEventListener("timeupdate", handleTimeStamp);
|
||||
vidEl.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
return () => {
|
||||
vidEl.removeEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
vidEl.removeEventListener("timeupdate", handleTimeStamp);
|
||||
};
|
||||
}, [vidEl]);
|
||||
@@ -16,13 +35,13 @@ export function LabelTimeDuration({ vidEl }: { vidEl: HTMLVideoElement }) {
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="w-12">{`${formatSecondsToHHMMSSCS(currentTime)}`}</p>
|
||||
<span>/</span>
|
||||
<p className="w-12">{`${formatSecondsToHHMMSSCS(vidEl.duration)}`}</p>
|
||||
<p className="w-12">{`${formatSecondsToHHMMSSCS(duration)}`}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function formatSecondsToHHMMSSCS(seconds: number) {
|
||||
//example of the ISO String: 1970-01-01T00:01:40.774Z
|
||||
const isoString = new Date(seconds * 1000).toISOString();
|
||||
const isoString = new Date(Math.round(seconds * 1000)).toISOString();
|
||||
if (seconds > 3600)
|
||||
return isoString.substring(11, 19) + "." + isoString.substring(20, 22);
|
||||
else return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { VIDEO_STATE_STATUSES } from "./enum";
|
||||
@@ -14,32 +14,43 @@ export const VideoControls = ({
|
||||
vidEl: HTMLVideoElement | undefined;
|
||||
className?: string;
|
||||
}) => {
|
||||
const prevVidEl = useRef<HTMLVideoElement | undefined>(undefined);
|
||||
const [status, setStatus] = useState<VIDEO_STATE_STATUSES>(
|
||||
VIDEO_STATE_STATUSES.INIT,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// helper function for removing listenders
|
||||
const handleLoadedmetadata = () => {
|
||||
if (vidEl) {
|
||||
setStatus(VIDEO_STATE_STATUSES.METADATA_LOADED);
|
||||
prevVidEl.current = vidEl;
|
||||
}
|
||||
};
|
||||
|
||||
// DOM node referencs has changed and exists
|
||||
if (vidEl) {
|
||||
vidEl.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
}
|
||||
// DOM node referencs has changed
|
||||
setStatus(VIDEO_STATE_STATUSES.INIT);
|
||||
// if the node exist, attach the listener and its remover
|
||||
vidEl?.addEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
return () => {
|
||||
vidEl?.removeEventListener("loadedmetadata", handleLoadedmetadata);
|
||||
};
|
||||
}, [vidEl]);
|
||||
|
||||
useEffect(() => {
|
||||
prevVidEl.current = vidEl;
|
||||
}, []);
|
||||
|
||||
const wrapperClass = twMerge(
|
||||
"flex w-full h-10 justify-center items-center gap-2",
|
||||
className,
|
||||
);
|
||||
|
||||
if (status === VIDEO_STATE_STATUSES.METADATA_LOADED && vidEl) {
|
||||
if (
|
||||
status === VIDEO_STATE_STATUSES.METADATA_LOADED &&
|
||||
vidEl &&
|
||||
prevVidEl.current === vidEl
|
||||
) {
|
||||
return (
|
||||
<div className={wrapperClass}>
|
||||
<ButtonPlaypause vidEl={vidEl} />
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export enum DialogAddMediaStatuses {
|
||||
STAGING_FILE = "STAGING_FILE",
|
||||
FILE_UPLOADING = "FILE_UPLOADING",
|
||||
FILE_UPLOADED = "FILE_UPLOADED",
|
||||
ERROR_FILE_UPLOAD = "ERROR_FILE_UPLOAD",
|
||||
FILE_RECORD_REQUESTING = "FILE_RECORD_REQUESTING",
|
||||
FILE_RECORD_RECEIVED = "FILE_RECORD_RECEIVED",
|
||||
ERROR_FILE_RECORD_REQUEST = "ERROR_FILE_RECORD_REQUEST",
|
||||
}
|
||||
@@ -1,77 +1 @@
|
||||
import { useState, useRef } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
|
||||
|
||||
import { FileUploader, VIDEO_FILE_TYPE } from "../FileUploader";
|
||||
import { Button } from "~/components/ui";
|
||||
|
||||
import {
|
||||
dialogBackgroundStyles,
|
||||
paperWrapperStyles,
|
||||
} from "~/components/styles";
|
||||
import { dispatchUiEvents } from "~/signals/uiEvents";
|
||||
import { QuickTrimVideoPlayer, TrimData } from "./QuickTrimVideoPlayer";
|
||||
|
||||
export const DialogAddVideo = ({
|
||||
isOpen,
|
||||
closeCallback,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
closeCallback: () => void;
|
||||
}) => {
|
||||
const [assetFile, setAssetFile] = useState<File | null>(null);
|
||||
const trimDataRef = useRef<TrimData | undefined>(undefined);
|
||||
|
||||
function handleClose() {
|
||||
setAssetFile(null);
|
||||
closeCallback();
|
||||
}
|
||||
|
||||
function handleEnter() {
|
||||
if (assetFile) {
|
||||
dispatchUiEvents.addVideoToEngine(assetFile, trimDataRef.current);
|
||||
}
|
||||
handleClose();
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onClose={closeCallback} className="relative z-50">
|
||||
<div className={dialogBackgroundStyles}>
|
||||
<DialogPanel
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex w-full max-w-5xl flex-col gap-4 px-6 py-4",
|
||||
)}
|
||||
>
|
||||
<DialogTitle className="font-bold">Upload Video</DialogTitle>
|
||||
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
|
||||
<FileUploader
|
||||
title=""
|
||||
fileTypes={Object.values(VIDEO_FILE_TYPE)}
|
||||
file={assetFile}
|
||||
setFile={async (file: File | null) => {
|
||||
setAssetFile(file);
|
||||
}}
|
||||
/>
|
||||
{assetFile && (
|
||||
<QuickTrimVideoPlayer
|
||||
file={assetFile}
|
||||
onTrimChange={(trimData) => {
|
||||
trimDataRef.current = trimData;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
<Button onClick={handleClose} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleEnter} disabled={assetFile === null}>
|
||||
Enter
|
||||
</Button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export * from "./DialogAddVideo";
|
||||
@@ -22,7 +22,7 @@ export const DragAndDropZone = ({ file, fileTypes }: Props) => {
|
||||
? `${Math.floor(file.size / 1024)} KB`
|
||||
: null;
|
||||
|
||||
const fileName = file && getFileName(file).toUpperCase();
|
||||
// const fileName = file && getFileName(file).toUpperCase();
|
||||
const wrapperClassName = twMerge(
|
||||
"group cursor-pointer p-3 bg-gray-100",
|
||||
!file && "aspect-video flex flex-col items-center justify-center gap-6",
|
||||
@@ -54,15 +54,13 @@ export const DragAndDropZone = ({ file, fileTypes }: Props) => {
|
||||
<p className="font-medium">
|
||||
{file.name.slice(0, file.name.lastIndexOf("."))}
|
||||
</p>
|
||||
<p className="flex items-center gap-2 text-sm font-normal">
|
||||
<span className="opacity-50">
|
||||
{`${fileName} file size: ${fileSize} `}
|
||||
</span>
|
||||
<u className="transition-all group-hover:text-primary">
|
||||
Change File
|
||||
</u>
|
||||
<p className="flex items-center gap-2 text-sm font-normal text-gray-500">
|
||||
{`file size: ${fileSize} `}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-md bg-primary px-4 py-2 hover:bg-primary-400">
|
||||
<p className="font-normal text-white">Change File</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { signal, effect } from "@preact/signals-react";
|
||||
import { TrimData } from "~/components/features/DialogAddVideo/TrimmerPlaybar";
|
||||
|
||||
const stagedImage = signal<File | null>(null);
|
||||
|
||||
@@ -15,18 +14,13 @@ const onGetStagedImage = (callback: (file: File) => void) => {
|
||||
});
|
||||
};
|
||||
|
||||
const stagedVideo = signal<{ file: File; trimData?: TrimData } | null>(null);
|
||||
const stagedVideo = signal<{ url: string } | null>(null);
|
||||
|
||||
const addVideoToEngine = (file: File, trimData?: TrimData) => {
|
||||
stagedVideo.value = { file, trimData };
|
||||
const addVideoToEngine = (videoData: { url: string }) => {
|
||||
stagedVideo.value = videoData;
|
||||
};
|
||||
|
||||
const onGetStagedVideo = (
|
||||
callback: (videoData: {
|
||||
file: File;
|
||||
trimData?: { trimStartMs: number; trimEndMs: number };
|
||||
}) => void,
|
||||
) => {
|
||||
const onGetStagedVideo = (callback: (videoData: { url: string }) => void) => {
|
||||
effect(() => {
|
||||
if (stagedVideo.value) {
|
||||
callback(stagedVideo.value);
|
||||
|
||||
Reference in new issue
Block a user