mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-12-23 19:11:14 +01:00
UI fixes (#14933)
* Fix plus dialog * Remove activity indicator on review item download button * fix explore view
This commit is contained in:
parent
9c20cd5f7b
commit
ed9c67804a
@ -1,7 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import ActivityIndicator from "../indicators/activity-indicator";
|
||||
import { FaDownload } from "react-icons/fa";
|
||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -19,8 +17,6 @@ export function DownloadVideoButton({
|
||||
startTime,
|
||||
className,
|
||||
}: DownloadVideoButtonProps) {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const formattedDate = formatUnixTimestampToDateTime(startTime, {
|
||||
strftime_fmt: "%D-%T",
|
||||
time_style: "medium",
|
||||
@ -29,7 +25,6 @@ export function DownloadVideoButton({
|
||||
const filename = `${camera}_${formattedDate}.mp4`;
|
||||
|
||||
const handleDownloadStart = () => {
|
||||
setIsDownloading(true);
|
||||
toast.success("Your review item video has started downloading.", {
|
||||
position: "top-center",
|
||||
});
|
||||
@ -39,19 +34,14 @@ export function DownloadVideoButton({
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
asChild
|
||||
disabled={isDownloading}
|
||||
className="flex items-center gap-2"
|
||||
size="sm"
|
||||
aria-label="Download Video"
|
||||
>
|
||||
<a href={source} download={filename} onClick={handleDownloadStart}>
|
||||
{isDownloading ? (
|
||||
<ActivityIndicator className="size-4" />
|
||||
) : (
|
||||
<FaDownload
|
||||
className={cn("size-4 text-secondary-foreground", className)}
|
||||
/>
|
||||
)}
|
||||
<FaDownload
|
||||
className={cn("size-4 text-secondary-foreground", className)}
|
||||
/>
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
@ -13,6 +13,7 @@ type SearchThumbnailProps = {
|
||||
findSimilar: () => void;
|
||||
refreshResults: () => void;
|
||||
showObjectLifecycle: () => void;
|
||||
showSnapshot: () => void;
|
||||
};
|
||||
|
||||
export default function SearchThumbnailFooter({
|
||||
@ -21,6 +22,7 @@ export default function SearchThumbnailFooter({
|
||||
findSimilar,
|
||||
refreshResults,
|
||||
showObjectLifecycle,
|
||||
showSnapshot,
|
||||
}: SearchThumbnailProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
@ -54,6 +56,7 @@ export default function SearchThumbnailFooter({
|
||||
findSimilar={findSimilar}
|
||||
refreshResults={refreshResults}
|
||||
showObjectLifecycle={showObjectLifecycle}
|
||||
showSnapshot={showSnapshot}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -37,15 +37,14 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
||||
import useSWR from "swr";
|
||||
import { Event } from "@/types/event";
|
||||
|
||||
type SearchResultActionsProps = {
|
||||
searchResult: SearchResult;
|
||||
findSimilar: () => void;
|
||||
refreshResults: () => void;
|
||||
showObjectLifecycle: () => void;
|
||||
showSnapshot: () => void;
|
||||
isContextMenu?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
@ -55,12 +54,12 @@ export default function SearchResultActions({
|
||||
findSimilar,
|
||||
refreshResults,
|
||||
showObjectLifecycle,
|
||||
showSnapshot,
|
||||
isContextMenu = false,
|
||||
children,
|
||||
}: SearchResultActionsProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
const [showFrigatePlus, setShowFrigatePlus] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
@ -130,10 +129,7 @@ export default function SearchResultActions({
|
||||
searchResult.has_snapshot &&
|
||||
searchResult.end_time &&
|
||||
!searchResult.plus_id && (
|
||||
<MenuItem
|
||||
aria-label="Submit to Frigate Plus"
|
||||
onClick={() => setShowFrigatePlus(true)}
|
||||
>
|
||||
<MenuItem aria-label="Submit to Frigate Plus" onClick={showSnapshot}>
|
||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||
<span>Submit to Frigate+</span>
|
||||
</MenuItem>
|
||||
@ -178,16 +174,6 @@ export default function SearchResultActions({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<FrigatePlusDialog
|
||||
upload={
|
||||
showFrigatePlus ? (searchResult as unknown as Event) : undefined
|
||||
}
|
||||
onClose={() => setShowFrigatePlus(false)}
|
||||
onEventUploaded={() => {
|
||||
searchResult.plus_id = "submitted";
|
||||
}}
|
||||
/>
|
||||
|
||||
{isContextMenu ? (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
@ -216,7 +202,7 @@ export default function SearchResultActions({
|
||||
<TooltipTrigger>
|
||||
<FrigatePlusIcon
|
||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||
onClick={() => setShowFrigatePlus(true)}
|
||||
onClick={showSnapshot}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Submit to Frigate+</TooltipContent>
|
||||
|
@ -161,7 +161,7 @@ export default function ReviewDetailDialog({
|
||||
)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
aria-label="Share this review item"
|
||||
size="sm"
|
||||
|
@ -533,7 +533,7 @@ type ObjectSnapshotTabProps = {
|
||||
search: Event;
|
||||
onEventUploaded: () => void;
|
||||
};
|
||||
function ObjectSnapshotTab({
|
||||
export function ObjectSnapshotTab({
|
||||
search,
|
||||
onEventUploaded,
|
||||
}: ObjectSnapshotTabProps) {
|
||||
|
@ -1,23 +1,14 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Event } from "@/types/event";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import axios from "axios";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
||||
import useSWR from "swr";
|
||||
|
||||
type SubmissionState = "reviewing" | "uploading" | "submitted";
|
||||
import { isDesktop, isMobile } from "react-device-detect";
|
||||
import { ObjectSnapshotTab } from "../detail/SearchDetailDialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type FrigatePlusDialogProps = {
|
||||
upload?: Event;
|
||||
@ -31,154 +22,35 @@ export function FrigatePlusDialog({
|
||||
onClose,
|
||||
onEventUploaded,
|
||||
}: FrigatePlusDialogProps) {
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
|
||||
// layout
|
||||
|
||||
const Title = isDesktop ? DialogTitle : "div";
|
||||
const Description = isDesktop ? DialogDescription : "div";
|
||||
|
||||
const grow = useMemo(() => {
|
||||
if (!config || !upload) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const camera = config.cameras[upload.camera];
|
||||
|
||||
if (!camera) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (camera.detect.width / camera.detect.height < 16 / 9) {
|
||||
return "aspect-video object-contain";
|
||||
}
|
||||
|
||||
return "";
|
||||
}, [config, upload]);
|
||||
|
||||
// upload
|
||||
|
||||
const [state, setState] = useState<SubmissionState>(
|
||||
upload?.plus_id ? "submitted" : "reviewing",
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => setState(upload?.plus_id ? "submitted" : "reviewing"),
|
||||
[upload],
|
||||
);
|
||||
|
||||
const onSubmitToPlus = useCallback(
|
||||
async (falsePositive: boolean) => {
|
||||
if (!upload) {
|
||||
return;
|
||||
}
|
||||
|
||||
falsePositive
|
||||
? axios.put(`events/${upload.id}/false_positive`)
|
||||
: axios.post(`events/${upload.id}/plus`, {
|
||||
include_annotation: 1,
|
||||
});
|
||||
|
||||
setState("submitted");
|
||||
onEventUploaded();
|
||||
onClose();
|
||||
},
|
||||
[upload, onClose, onEventUploaded],
|
||||
);
|
||||
|
||||
const content = (
|
||||
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<DialogHeader
|
||||
className={state == "submitted" ? "sr-only" : "text-left"}
|
||||
>
|
||||
<Title
|
||||
className={
|
||||
!isDesktop
|
||||
? "text-lg font-semibold leading-none tracking-tight"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Submit To Frigate+
|
||||
</Title>
|
||||
<Description
|
||||
className={!isDesktop ? "text-sm text-muted-foreground" : undefined}
|
||||
>
|
||||
Objects in locations you want to avoid are not false positives.
|
||||
Submitting them as false positives will confuse the model.
|
||||
</Description>
|
||||
</DialogHeader>
|
||||
<TransformComponent
|
||||
wrapperStyle={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
contentStyle={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{upload?.id && (
|
||||
<img
|
||||
className={`w-full ${grow} bg-black`}
|
||||
src={`${baseUrl}api/events/${upload?.id}/snapshot.jpg`}
|
||||
alt={`${upload?.label}`}
|
||||
/>
|
||||
)}
|
||||
</TransformComponent>
|
||||
|
||||
<DialogFooter className="flex flex-row justify-end gap-2">
|
||||
{state == "reviewing" && (
|
||||
<>
|
||||
{dialog && (
|
||||
<Button aria-label="Cancel" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="bg-success"
|
||||
aria-label="Confirm this label for Frigate Plus"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(false);
|
||||
}}
|
||||
>
|
||||
This is {/^[aeiou]/i.test(upload?.label || "") ? "an" : "a"}{" "}
|
||||
{upload?.label}
|
||||
</Button>
|
||||
<Button
|
||||
className="text-white"
|
||||
aria-label="Do not confirm this label for Frigate Plus"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
setState("uploading");
|
||||
onSubmitToPlus(true);
|
||||
}}
|
||||
>
|
||||
This is not {/^[aeiou]/i.test(upload?.label || "") ? "an" : "a"}{" "}
|
||||
{upload?.label}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{state == "uploading" && <ActivityIndicator />}
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</TransformWrapper>
|
||||
);
|
||||
|
||||
if (!upload) {
|
||||
return;
|
||||
}
|
||||
if (dialog) {
|
||||
return (
|
||||
<Dialog
|
||||
open={upload != undefined}
|
||||
onOpenChange={(open) => (!open ? onClose() : null)}
|
||||
>
|
||||
<DialogContent className="md:max-w-3xl lg:max-w-4xl xl:max-w-7xl">
|
||||
{content}
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"scrollbar-container overflow-y-auto",
|
||||
isDesktop &&
|
||||
"max-h-[95dvh] sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl",
|
||||
isMobile && "px-4",
|
||||
)}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="sr-only">Submit to Frigate+</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Submit this snapshot to Frigate+
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ObjectSnapshotTab
|
||||
search={upload}
|
||||
onEventUploaded={onEventUploaded}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
@ -228,12 +228,17 @@ function ExploreThumbnailImage({
|
||||
onSelectSearch(event, 0, "object lifecycle");
|
||||
};
|
||||
|
||||
const handleShowSnapshot = () => {
|
||||
onSelectSearch(event, 0, "snapshot");
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchResultActions
|
||||
searchResult={event}
|
||||
findSimilar={handleFindSimilar}
|
||||
refreshResults={mutate}
|
||||
showObjectLifecycle={handleShowObjectLifecycle}
|
||||
showSnapshot={handleShowSnapshot}
|
||||
isContextMenu={true}
|
||||
>
|
||||
<div className="relative size-full">
|
||||
|
@ -471,6 +471,9 @@ export default function SearchView({
|
||||
showObjectLifecycle={() =>
|
||||
onSelectSearch(value, index, "object lifecycle")
|
||||
}
|
||||
showSnapshot={() =>
|
||||
onSelectSearch(value, index, "snapshot")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user