mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +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 { Button } from "@/components/ui/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import ActivityIndicator from "../indicators/activity-indicator";
|
|
||||||
import { FaDownload } from "react-icons/fa";
|
import { FaDownload } from "react-icons/fa";
|
||||||
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@ -19,8 +17,6 @@ export function DownloadVideoButton({
|
|||||||
startTime,
|
startTime,
|
||||||
className,
|
className,
|
||||||
}: DownloadVideoButtonProps) {
|
}: DownloadVideoButtonProps) {
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
|
|
||||||
const formattedDate = formatUnixTimestampToDateTime(startTime, {
|
const formattedDate = formatUnixTimestampToDateTime(startTime, {
|
||||||
strftime_fmt: "%D-%T",
|
strftime_fmt: "%D-%T",
|
||||||
time_style: "medium",
|
time_style: "medium",
|
||||||
@ -29,7 +25,6 @@ export function DownloadVideoButton({
|
|||||||
const filename = `${camera}_${formattedDate}.mp4`;
|
const filename = `${camera}_${formattedDate}.mp4`;
|
||||||
|
|
||||||
const handleDownloadStart = () => {
|
const handleDownloadStart = () => {
|
||||||
setIsDownloading(true);
|
|
||||||
toast.success("Your review item video has started downloading.", {
|
toast.success("Your review item video has started downloading.", {
|
||||||
position: "top-center",
|
position: "top-center",
|
||||||
});
|
});
|
||||||
@ -39,19 +34,14 @@ export function DownloadVideoButton({
|
|||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
disabled={isDownloading}
|
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label="Download Video"
|
aria-label="Download Video"
|
||||||
>
|
>
|
||||||
<a href={source} download={filename} onClick={handleDownloadStart}>
|
<a href={source} download={filename} onClick={handleDownloadStart}>
|
||||||
{isDownloading ? (
|
|
||||||
<ActivityIndicator className="size-4" />
|
|
||||||
) : (
|
|
||||||
<FaDownload
|
<FaDownload
|
||||||
className={cn("size-4 text-secondary-foreground", className)}
|
className={cn("size-4 text-secondary-foreground", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +13,7 @@ type SearchThumbnailProps = {
|
|||||||
findSimilar: () => void;
|
findSimilar: () => void;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
showObjectLifecycle: () => void;
|
showObjectLifecycle: () => void;
|
||||||
|
showSnapshot: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SearchThumbnailFooter({
|
export default function SearchThumbnailFooter({
|
||||||
@ -21,6 +22,7 @@ export default function SearchThumbnailFooter({
|
|||||||
findSimilar,
|
findSimilar,
|
||||||
refreshResults,
|
refreshResults,
|
||||||
showObjectLifecycle,
|
showObjectLifecycle,
|
||||||
|
showSnapshot,
|
||||||
}: SearchThumbnailProps) {
|
}: SearchThumbnailProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ export default function SearchThumbnailFooter({
|
|||||||
findSimilar={findSimilar}
|
findSimilar={findSimilar}
|
||||||
refreshResults={refreshResults}
|
refreshResults={refreshResults}
|
||||||
showObjectLifecycle={showObjectLifecycle}
|
showObjectLifecycle={showObjectLifecycle}
|
||||||
|
showSnapshot={showSnapshot}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,15 +37,14 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { FrigatePlusDialog } from "@/components/overlay/dialog/FrigatePlusDialog";
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Event } from "@/types/event";
|
|
||||||
|
|
||||||
type SearchResultActionsProps = {
|
type SearchResultActionsProps = {
|
||||||
searchResult: SearchResult;
|
searchResult: SearchResult;
|
||||||
findSimilar: () => void;
|
findSimilar: () => void;
|
||||||
refreshResults: () => void;
|
refreshResults: () => void;
|
||||||
showObjectLifecycle: () => void;
|
showObjectLifecycle: () => void;
|
||||||
|
showSnapshot: () => void;
|
||||||
isContextMenu?: boolean;
|
isContextMenu?: boolean;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
@ -55,12 +54,12 @@ export default function SearchResultActions({
|
|||||||
findSimilar,
|
findSimilar,
|
||||||
refreshResults,
|
refreshResults,
|
||||||
showObjectLifecycle,
|
showObjectLifecycle,
|
||||||
|
showSnapshot,
|
||||||
isContextMenu = false,
|
isContextMenu = false,
|
||||||
children,
|
children,
|
||||||
}: SearchResultActionsProps) {
|
}: SearchResultActionsProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
const [showFrigatePlus, setShowFrigatePlus] = useState(false);
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
@ -130,10 +129,7 @@ export default function SearchResultActions({
|
|||||||
searchResult.has_snapshot &&
|
searchResult.has_snapshot &&
|
||||||
searchResult.end_time &&
|
searchResult.end_time &&
|
||||||
!searchResult.plus_id && (
|
!searchResult.plus_id && (
|
||||||
<MenuItem
|
<MenuItem aria-label="Submit to Frigate Plus" onClick={showSnapshot}>
|
||||||
aria-label="Submit to Frigate Plus"
|
|
||||||
onClick={() => setShowFrigatePlus(true)}
|
|
||||||
>
|
|
||||||
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
<FrigatePlusIcon className="mr-2 size-4 cursor-pointer text-primary" />
|
||||||
<span>Submit to Frigate+</span>
|
<span>Submit to Frigate+</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@ -178,16 +174,6 @@ export default function SearchResultActions({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
<FrigatePlusDialog
|
|
||||||
upload={
|
|
||||||
showFrigatePlus ? (searchResult as unknown as Event) : undefined
|
|
||||||
}
|
|
||||||
onClose={() => setShowFrigatePlus(false)}
|
|
||||||
onEventUploaded={() => {
|
|
||||||
searchResult.plus_id = "submitted";
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isContextMenu ? (
|
{isContextMenu ? (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||||
@ -216,7 +202,7 @@ export default function SearchResultActions({
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<FrigatePlusIcon
|
<FrigatePlusIcon
|
||||||
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
className="size-5 cursor-pointer text-primary-variant hover:text-primary"
|
||||||
onClick={() => setShowFrigatePlus(true)}
|
onClick={showSnapshot}
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Submit to Frigate+</TooltipContent>
|
<TooltipContent>Submit to Frigate+</TooltipContent>
|
||||||
|
@ -161,7 +161,7 @@ export default function ReviewDetailDialog({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Share this review item"
|
aria-label="Share this review item"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@ -533,7 +533,7 @@ type ObjectSnapshotTabProps = {
|
|||||||
search: Event;
|
search: Event;
|
||||||
onEventUploaded: () => void;
|
onEventUploaded: () => void;
|
||||||
};
|
};
|
||||||
function ObjectSnapshotTab({
|
export function ObjectSnapshotTab({
|
||||||
search,
|
search,
|
||||||
onEventUploaded,
|
onEventUploaded,
|
||||||
}: ObjectSnapshotTabProps) {
|
}: ObjectSnapshotTabProps) {
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
import { baseUrl } from "@/api/baseUrl";
|
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Event } from "@/types/event";
|
import { Event } from "@/types/event";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import axios from "axios";
|
import { ObjectSnapshotTab } from "../detail/SearchDetailDialog";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { cn } from "@/lib/utils";
|
||||||
import { isDesktop } from "react-device-detect";
|
|
||||||
import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
|
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
type SubmissionState = "reviewing" | "uploading" | "submitted";
|
|
||||||
|
|
||||||
type FrigatePlusDialogProps = {
|
type FrigatePlusDialogProps = {
|
||||||
upload?: Event;
|
upload?: Event;
|
||||||
@ -31,154 +22,35 @@ export function FrigatePlusDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
onEventUploaded,
|
onEventUploaded,
|
||||||
}: FrigatePlusDialogProps) {
|
}: 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) {
|
if (!upload) {
|
||||||
return;
|
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 (dialog) {
|
if (dialog) {
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={upload != undefined}
|
open={upload != undefined}
|
||||||
onOpenChange={(open) => (!open ? onClose() : null)}
|
onOpenChange={(open) => (!open ? onClose() : null)}
|
||||||
>
|
>
|
||||||
<DialogContent className="md:max-w-3xl lg:max-w-4xl xl:max-w-7xl">
|
<DialogContent
|
||||||
{content}
|
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>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
@ -228,12 +228,17 @@ function ExploreThumbnailImage({
|
|||||||
onSelectSearch(event, 0, "object lifecycle");
|
onSelectSearch(event, 0, "object lifecycle");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShowSnapshot = () => {
|
||||||
|
onSelectSearch(event, 0, "snapshot");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchResultActions
|
<SearchResultActions
|
||||||
searchResult={event}
|
searchResult={event}
|
||||||
findSimilar={handleFindSimilar}
|
findSimilar={handleFindSimilar}
|
||||||
refreshResults={mutate}
|
refreshResults={mutate}
|
||||||
showObjectLifecycle={handleShowObjectLifecycle}
|
showObjectLifecycle={handleShowObjectLifecycle}
|
||||||
|
showSnapshot={handleShowSnapshot}
|
||||||
isContextMenu={true}
|
isContextMenu={true}
|
||||||
>
|
>
|
||||||
<div className="relative size-full">
|
<div className="relative size-full">
|
||||||
|
@ -471,6 +471,9 @@ export default function SearchView({
|
|||||||
showObjectLifecycle={() =>
|
showObjectLifecycle={() =>
|
||||||
onSelectSearch(value, index, "object lifecycle")
|
onSelectSearch(value, index, "object lifecycle")
|
||||||
}
|
}
|
||||||
|
showSnapshot={() =>
|
||||||
|
onSelectSearch(value, index, "snapshot")
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user