mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add export page in to new web UI (#8929)
This commit is contained in:
		
							parent
							
								
									d2d1278a4d
								
							
						
					
					
						commit
						0e6528a989
					
				
							
								
								
									
										50
									
								
								web-new/src/components/card/ExportCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								web-new/src/components/card/ExportCard.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import { baseUrl } from "@/api/baseUrl";
 | 
				
			||||||
 | 
					import ActivityIndicator from "../ui/activity-indicator";
 | 
				
			||||||
 | 
					import { Card } from "../ui/card";
 | 
				
			||||||
 | 
					import { LuPlay, LuTrash } from "react-icons/lu";
 | 
				
			||||||
 | 
					import { Button } from "../ui/button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ExportProps = {
 | 
				
			||||||
 | 
					  file: {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					  onSelect: (file: string) => void;
 | 
				
			||||||
 | 
					  onDelete: (file: string) => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function ExportCard({ file, onSelect, onDelete }: ExportProps) {
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Card className="my-4 p-4 bg-secondary flex justify-start text-center items-center">
 | 
				
			||||||
 | 
					      {file.name.startsWith("in_progress") ? (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <div className="p-2">
 | 
				
			||||||
 | 
					            <ActivityIndicator size={16} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div className="px-2">
 | 
				
			||||||
 | 
					            {file.name.substring(12, file.name.length - 4)}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      ) : (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					          <Button variant="secondary" onClick={() => onSelect(file.name)}>
 | 
				
			||||||
 | 
					            <LuPlay className="h-4 w-4 text-green-600" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					          <a
 | 
				
			||||||
 | 
					            className="text-blue-500 hover:underline overflow-hidden"
 | 
				
			||||||
 | 
					            href={`${baseUrl}exports/${file.name}`}
 | 
				
			||||||
 | 
					            download
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            {file.name.substring(0, file.name.length - 4)}
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					          <Button
 | 
				
			||||||
 | 
					            className="ml-auto"
 | 
				
			||||||
 | 
					            variant="secondary"
 | 
				
			||||||
 | 
					            onClick={() => onDelete(file.name)}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <LuTrash className="h-4 w-4" stroke="#f87171" />
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </Card>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										74
									
								
								web-new/src/components/player/VideoPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								web-new/src/components/player/VideoPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					import { useEffect, useRef, ReactElement } from "react";
 | 
				
			||||||
 | 
					import videojs from 'video.js';
 | 
				
			||||||
 | 
					import 'videojs-playlist';
 | 
				
			||||||
 | 
					import 'video.js/dist/video-js.css';
 | 
				
			||||||
 | 
					import Player from "video.js/dist/types/player";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type VideoPlayerProps = {
 | 
				
			||||||
 | 
					    children?: ReactElement | ReactElement[],
 | 
				
			||||||
 | 
					    options?: {
 | 
				
			||||||
 | 
					        [key: string]: any
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    seekOptions?: {
 | 
				
			||||||
 | 
					        forward?: number,
 | 
				
			||||||
 | 
					        backward?: number,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onReady?: (player: Player) => void,
 | 
				
			||||||
 | 
					    onDispose?: () => void,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function VideoPlayer({ children, options, seekOptions = {forward:30, backward: 10}, onReady = (_) => {}, onDispose = () => {} }: VideoPlayerProps) {
 | 
				
			||||||
 | 
					    const videoRef = useRef<HTMLVideoElement | null>(null);
 | 
				
			||||||
 | 
					    const playerRef = useRef<Player | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const defaultOptions = {
 | 
				
			||||||
 | 
					        controls: true,
 | 
				
			||||||
 | 
					        controlBar: {
 | 
				
			||||||
 | 
					          skipButtons: seekOptions,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        playbackRates: [0.5, 1, 2, 4, 8],
 | 
				
			||||||
 | 
					        fluid: true,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!videojs.browser.IS_FIREFOX) {
 | 
				
			||||||
 | 
					        defaultOptions.playbackRates.push(16);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Make sure Video.js player is only initialized once
 | 
				
			||||||
 | 
					      if (!playerRef.current) {
 | 
				
			||||||
 | 
					        // The Video.js player needs to be _inside_ the component el for React 18 Strict Mode.
 | 
				
			||||||
 | 
					        const videoElement = document.createElement("video-js");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        videoElement.classList.add('small-player');
 | 
				
			||||||
 | 
					        videoElement.classList.add('video-js');
 | 
				
			||||||
 | 
					        videoElement.classList.add('vjs-default-skin');
 | 
				
			||||||
 | 
					        videoRef.current.appendChild(videoElement);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const player = playerRef.current = videojs(videoElement, { ...defaultOptions, ...options }, () => {
 | 
				
			||||||
 | 
					          onReady && onReady(player);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }, [options, videoRef]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Dispose the Video.js player when the functional component unmounts
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					      const player = playerRef.current;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return () => {
 | 
				
			||||||
 | 
					        if (player && !player.isDisposed()) {
 | 
				
			||||||
 | 
					          player.dispose();
 | 
				
			||||||
 | 
					          playerRef.current = null;
 | 
				
			||||||
 | 
					          onDispose();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }, [playerRef]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div data-vjs-player>
 | 
				
			||||||
 | 
					        <div ref={videoRef} />
 | 
				
			||||||
 | 
					        {children}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
@ -1,9 +1,322 @@
 | 
				
			|||||||
 | 
					import { baseUrl } from "@/api/baseUrl";
 | 
				
			||||||
 | 
					import ExportCard from "@/components/card/ExportCard";
 | 
				
			||||||
 | 
					import VideoPlayer from "@/components/player/VideoPlayer";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AlertDialog,
 | 
				
			||||||
 | 
					  AlertDialogCancel,
 | 
				
			||||||
 | 
					  AlertDialogContent,
 | 
				
			||||||
 | 
					  AlertDialogDescription,
 | 
				
			||||||
 | 
					  AlertDialogFooter,
 | 
				
			||||||
 | 
					  AlertDialogHeader,
 | 
				
			||||||
 | 
					  AlertDialogTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/alert-dialog";
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Calendar } from "@/components/ui/calendar";
 | 
				
			||||||
 | 
					import { Card } from "@/components/ui/card";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Dialog,
 | 
				
			||||||
 | 
					  DialogContent,
 | 
				
			||||||
 | 
					  DialogHeader,
 | 
				
			||||||
 | 
					  DialogTitle,
 | 
				
			||||||
 | 
					} from "@/components/ui/dialog";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DropdownMenuRadioGroup,
 | 
				
			||||||
 | 
					  DropdownMenuTrigger,
 | 
				
			||||||
 | 
					  DropdownMenu,
 | 
				
			||||||
 | 
					  DropdownMenuContent,
 | 
				
			||||||
 | 
					  DropdownMenuLabel,
 | 
				
			||||||
 | 
					  DropdownMenuSeparator,
 | 
				
			||||||
 | 
					  DropdownMenuRadioItem,
 | 
				
			||||||
 | 
					} from "@/components/ui/dropdown-menu";
 | 
				
			||||||
import Heading from "@/components/ui/heading";
 | 
					import Heading from "@/components/ui/heading";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Popover,
 | 
				
			||||||
 | 
					  PopoverContent,
 | 
				
			||||||
 | 
					  PopoverTrigger,
 | 
				
			||||||
 | 
					} from "@/components/ui/popover";
 | 
				
			||||||
 | 
					import { FrigateConfig } from "@/types/frigateConfig";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { format } from "date-fns";
 | 
				
			||||||
 | 
					import { useCallback, useState } from "react";
 | 
				
			||||||
 | 
					import { DateRange } from "react-day-picker";
 | 
				
			||||||
 | 
					import useSWR from "swr";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ExportItem = {
 | 
				
			||||||
 | 
					  name: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function Export() {
 | 
					function Export() {
 | 
				
			||||||
 | 
					  const { data: config } = useSWR<FrigateConfig>("config");
 | 
				
			||||||
 | 
					  const { data: exports, mutate } = useSWR<ExportItem[]>(
 | 
				
			||||||
 | 
					    "exports/",
 | 
				
			||||||
 | 
					    (url: string) => axios({ baseURL: baseUrl, url }).then((res) => res.data)
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Export States
 | 
				
			||||||
 | 
					  const [camera, setCamera] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [playback, setPlayback] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [message, setMessage] = useState({ text: "", error: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const currentDate = new Date();
 | 
				
			||||||
 | 
					  currentDate.setHours(0, 0, 0, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [date, setDate] = useState<DateRange | undefined>({
 | 
				
			||||||
 | 
					    from: currentDate,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  const [startTime, setStartTime] = useState("00:00:00");
 | 
				
			||||||
 | 
					  const [endTime, setEndTime] = useState("23:59:59");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [selectedClip, setSelectedClip] = useState<string | undefined>();
 | 
				
			||||||
 | 
					  const [deleteClip, setDeleteClip] = useState<string | undefined>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onHandleExport = () => {
 | 
				
			||||||
 | 
					    if (camera == "select") {
 | 
				
			||||||
 | 
					      setMessage({ text: "A camera needs to be selected.", error: true });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (playback == "select") {
 | 
				
			||||||
 | 
					      setMessage({
 | 
				
			||||||
 | 
					        text: "A playback factor needs to be selected.",
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!date?.from || !startTime || !endTime) {
 | 
				
			||||||
 | 
					      setMessage({
 | 
				
			||||||
 | 
					        text: "A start and end time needs to be selected",
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const startDate = new Date(date.from.getTime());
 | 
				
			||||||
 | 
					    const [startHour, startMin, startSec] = startTime.split(":");
 | 
				
			||||||
 | 
					    startDate.setHours(
 | 
				
			||||||
 | 
					      parseInt(startHour),
 | 
				
			||||||
 | 
					      parseInt(startMin),
 | 
				
			||||||
 | 
					      parseInt(startSec),
 | 
				
			||||||
 | 
					      0
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const start = startDate.getTime() / 1000;
 | 
				
			||||||
 | 
					    const endDate = new Date((date.to || date.from).getTime());
 | 
				
			||||||
 | 
					    const [endHour, endMin, endSec] = endTime.split(":");
 | 
				
			||||||
 | 
					    endDate.setHours(parseInt(endHour), parseInt(endMin), parseInt(endSec), 0);
 | 
				
			||||||
 | 
					    const end = endDate.getTime() / 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (end <= start) {
 | 
				
			||||||
 | 
					      setMessage({
 | 
				
			||||||
 | 
					        text: "The end time must be after the start time.",
 | 
				
			||||||
 | 
					        error: true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    axios
 | 
				
			||||||
 | 
					      .post(`export/${camera}/start/${start}/end/${end}`, { playback })
 | 
				
			||||||
 | 
					      .then((response) => {
 | 
				
			||||||
 | 
					        if (response.status == 200) {
 | 
				
			||||||
 | 
					          setMessage({
 | 
				
			||||||
 | 
					            text: "Successfully started export. View the file in the /exports folder.",
 | 
				
			||||||
 | 
					            error: false,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        mutate();
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((error) => {
 | 
				
			||||||
 | 
					        if (error.response?.data?.message) {
 | 
				
			||||||
 | 
					          setMessage({
 | 
				
			||||||
 | 
					            text: `Failed to start export: ${error.response.data.message}`,
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          setMessage({
 | 
				
			||||||
 | 
					            text: `Failed to start export: ${error.message}`,
 | 
				
			||||||
 | 
					            error: true,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onHandleDelete = useCallback(() => {
 | 
				
			||||||
 | 
					    if (!deleteClip) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    axios.delete(`export/${deleteClip}`).then((response) => {
 | 
				
			||||||
 | 
					      if (response.status == 200) {
 | 
				
			||||||
 | 
					        setDeleteClip(undefined);
 | 
				
			||||||
 | 
					        mutate();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, [deleteClip]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <>
 | 
					    <>
 | 
				
			||||||
      <Heading as="h2">Export</Heading>
 | 
					      <Heading as="h2">Export</Heading>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {message.text && (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          className={`max-h-20 ${
 | 
				
			||||||
 | 
					            message.error ? "text-red-500" : "text-green-500"
 | 
				
			||||||
 | 
					          }`}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {message.text}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <AlertDialog
 | 
				
			||||||
 | 
					        open={deleteClip != undefined}
 | 
				
			||||||
 | 
					        onOpenChange={(_) => setDeleteClip(undefined)}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <AlertDialogContent>
 | 
				
			||||||
 | 
					          <AlertDialogHeader>
 | 
				
			||||||
 | 
					            <AlertDialogTitle>Delete Export</AlertDialogTitle>
 | 
				
			||||||
 | 
					            <AlertDialogDescription>
 | 
				
			||||||
 | 
					              Confirm deletion of {deleteClip}.
 | 
				
			||||||
 | 
					            </AlertDialogDescription>
 | 
				
			||||||
 | 
					          </AlertDialogHeader>
 | 
				
			||||||
 | 
					          <AlertDialogFooter>
 | 
				
			||||||
 | 
					            <AlertDialogCancel>Cancel</AlertDialogCancel>
 | 
				
			||||||
 | 
					            <Button variant="destructive" onClick={() => onHandleDelete()}>
 | 
				
			||||||
 | 
					              Delete
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </AlertDialogFooter>
 | 
				
			||||||
 | 
					        </AlertDialogContent>
 | 
				
			||||||
 | 
					      </AlertDialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <Dialog
 | 
				
			||||||
 | 
					        open={selectedClip != undefined}
 | 
				
			||||||
 | 
					        onOpenChange={(_) => setSelectedClip(undefined)}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <DialogContent>
 | 
				
			||||||
 | 
					          <DialogHeader>
 | 
				
			||||||
 | 
					            <DialogTitle>Playback</DialogTitle>
 | 
				
			||||||
 | 
					          </DialogHeader>
 | 
				
			||||||
 | 
					          <VideoPlayer
 | 
				
			||||||
 | 
					            options={{
 | 
				
			||||||
 | 
					              preload: "auto",
 | 
				
			||||||
 | 
					              autoplay: true,
 | 
				
			||||||
 | 
					              sources: [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                  src: `${baseUrl}exports/${selectedClip}`,
 | 
				
			||||||
 | 
					                  type: "video/mp4",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            seekOptions={{ forward: 10, backward: 5 }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </DialogContent>
 | 
				
			||||||
 | 
					      </Dialog>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="xl:flex justify-between">
 | 
				
			||||||
 | 
					        <div>
 | 
				
			||||||
 | 
					          <div className="my-2 flex">
 | 
				
			||||||
 | 
					            <DropdownMenu>
 | 
				
			||||||
 | 
					              <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                <Button className="capitalize" variant="outline">
 | 
				
			||||||
 | 
					                  {camera?.replaceAll("_", " ") || "Select A Camera"}
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					              </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					              <DropdownMenuContent>
 | 
				
			||||||
 | 
					                <DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
 | 
				
			||||||
 | 
					                <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                <DropdownMenuRadioGroup
 | 
				
			||||||
 | 
					                  value={camera}
 | 
				
			||||||
 | 
					                  onValueChange={setCamera}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                  {Object.keys(config?.cameras || {}).map((item) => (
 | 
				
			||||||
 | 
					                    <DropdownMenuRadioItem
 | 
				
			||||||
 | 
					                      className="capitalize"
 | 
				
			||||||
 | 
					                      key={item}
 | 
				
			||||||
 | 
					                      value={item}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      {item.replaceAll("_", " ")}
 | 
				
			||||||
 | 
					                    </DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                  ))}
 | 
				
			||||||
 | 
					                </DropdownMenuRadioGroup>
 | 
				
			||||||
 | 
					              </DropdownMenuContent>
 | 
				
			||||||
 | 
					            </DropdownMenu>
 | 
				
			||||||
 | 
					            <div className="mx-2">
 | 
				
			||||||
 | 
					              <DropdownMenu>
 | 
				
			||||||
 | 
					                <DropdownMenuTrigger asChild>
 | 
				
			||||||
 | 
					                  <Button className="capitalize" variant="outline">
 | 
				
			||||||
 | 
					                    {playback?.split("_")[0] || "Select A Playback Factor"}
 | 
				
			||||||
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                </DropdownMenuTrigger>
 | 
				
			||||||
 | 
					                <DropdownMenuContent>
 | 
				
			||||||
 | 
					                  <DropdownMenuLabel>
 | 
				
			||||||
 | 
					                    Select A Playback Factor
 | 
				
			||||||
 | 
					                  </DropdownMenuLabel>
 | 
				
			||||||
 | 
					                  <DropdownMenuSeparator />
 | 
				
			||||||
 | 
					                  <DropdownMenuRadioGroup
 | 
				
			||||||
 | 
					                    value={playback}
 | 
				
			||||||
 | 
					                    onValueChange={setPlayback}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    <DropdownMenuRadioItem value="realtime">
 | 
				
			||||||
 | 
					                      Realtime
 | 
				
			||||||
 | 
					                    </DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                    <DropdownMenuRadioItem value="timelapse_25x">
 | 
				
			||||||
 | 
					                      Timelapse
 | 
				
			||||||
 | 
					                    </DropdownMenuRadioItem>
 | 
				
			||||||
 | 
					                  </DropdownMenuRadioGroup>
 | 
				
			||||||
 | 
					                </DropdownMenuContent>
 | 
				
			||||||
 | 
					              </DropdownMenu>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <Popover>
 | 
				
			||||||
 | 
					            <PopoverTrigger asChild>
 | 
				
			||||||
 | 
					              <Button variant="outline">{`${
 | 
				
			||||||
 | 
					                date?.from ? format(date?.from, "LLL dd, y") : ""
 | 
				
			||||||
 | 
					              } ${startTime} -> ${
 | 
				
			||||||
 | 
					                date?.to ? format(date?.to, "LLL dd, y") : ""
 | 
				
			||||||
 | 
					              } ${endTime}`}</Button>
 | 
				
			||||||
 | 
					            </PopoverTrigger>
 | 
				
			||||||
 | 
					            <PopoverContent className="w-84">
 | 
				
			||||||
 | 
					              <Calendar mode="range" selected={date} onSelect={setDate} />
 | 
				
			||||||
 | 
					              <div className="flex justify-between">
 | 
				
			||||||
 | 
					                <input
 | 
				
			||||||
 | 
					                  className="p-1 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
 | 
				
			||||||
 | 
					                  id="startTime"
 | 
				
			||||||
 | 
					                  type="time"
 | 
				
			||||||
 | 
					                  value={startTime}
 | 
				
			||||||
 | 
					                  step="1"
 | 
				
			||||||
 | 
					                  onChange={(e) => setStartTime(e.target.value)}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <input
 | 
				
			||||||
 | 
					                  className="p-1 mx-2 border border-input bg-background text-secondary-foreground hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
 | 
				
			||||||
 | 
					                  id="endTime"
 | 
				
			||||||
 | 
					                  type="time"
 | 
				
			||||||
 | 
					                  value={endTime}
 | 
				
			||||||
 | 
					                  step="1"
 | 
				
			||||||
 | 
					                  onChange={(e) => setEndTime(e.target.value)}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </PopoverContent>
 | 
				
			||||||
 | 
					          </Popover>
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <Button className="my-4" onClick={() => onHandleExport()}>
 | 
				
			||||||
 | 
					              Submit
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {exports && (
 | 
				
			||||||
 | 
					          <Card className="p-4 xl:w-1/2">
 | 
				
			||||||
 | 
					            <Heading as="h3">Exports</Heading>
 | 
				
			||||||
 | 
					            {Object.values(exports).map((item) => (
 | 
				
			||||||
 | 
					              <ExportCard
 | 
				
			||||||
 | 
					                key={item.name}
 | 
				
			||||||
 | 
					                file={item}
 | 
				
			||||||
 | 
					                onSelect={(file) => setSelectedClip(file)}
 | 
				
			||||||
 | 
					                onDelete={(file) => setDeleteClip(file)}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					          </Card>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </>
 | 
					    </>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
  "compilerOptions": {
 | 
					  "compilerOptions": {
 | 
				
			||||||
    "target": "ES2020",
 | 
					    "target": "ES2020",
 | 
				
			||||||
    "useDefineForClassFields": true,
 | 
					    "useDefineForClassFields": true,
 | 
				
			||||||
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
 | 
					    "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2021.String"],
 | 
				
			||||||
    "module": "ESNext",
 | 
					    "module": "ESNext",
 | 
				
			||||||
    "skipLibCheck": true,
 | 
					    "skipLibCheck": true,
 | 
				
			||||||
    "baseUrl": ".",
 | 
					    "baseUrl": ".",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user