From 6311fdb6829d2da9334d6d68623595b219719ffa Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 23 Feb 2024 11:22:50 -0700 Subject: [PATCH 1/3] Add custom context menu for review items --- web/package-lock.json | 29 +++ web/package.json | 1 + .../player/PreviewThumbnailPlayer.tsx | 183 ++++++++++------ web/src/components/ui/context-menu.tsx | 198 ++++++++++++++++++ web/src/pages/Export.tsx | 9 +- 5 files changed, 353 insertions(+), 67 deletions(-) create mode 100644 web/src/components/ui/context-menu.tsx diff --git a/web/package-lock.json b/web/package-lock.json index aa997a927f..9b84a97b5c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^3.3.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -1185,6 +1186,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz", + "integrity": "sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-dialog": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", diff --git a/web/package.json b/web/package.json index 61ef264299..b64f39a588 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,7 @@ "@hookform/resolvers": "^3.3.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index bdde456970..d110ff354d 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -11,6 +11,15 @@ import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { isMobile, isSafari } from "react-device-detect"; import Chip from "../Chip"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "../ui/context-menu"; +import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu"; +import axios from "axios"; type PreviewPlayerProps = { review: ReviewSegment; @@ -86,66 +95,72 @@ export default function PreviewThumbnailPlayer({ ); return ( -
onPlayback(true)} - onMouseLeave={isMobile ? undefined : () => onPlayback(false)} - onClick={onClick} - > - {playingBack ? ( - - ) : ( - - )} - {(review.severity == "alert" || review.severity == "detection") && ( - - {review.data.objects.map((object) => { - return getIconForLabel(object, "w-3 h-3 text-white"); - })} - {review.data.audio.map((audio) => { - return getIconForLabel(audio, "w-3 h-3 text-white"); - })} - {review.data.sub_labels?.map((sub) => { - return getIconForSubLabel(sub, "w-3 h-3 text-white"); - })} - - )} - {!playingBack && ( -
- - {config && - formatUnixTimestampToDateTime(review.start_time, { - strftime_fmt: - config.ui.time_format == "24hour" - ? "%b %-d, %H:%M" - : "%b %-d, %I:%M %p", + + onPlayback(true)} + onMouseLeave={isMobile ? undefined : () => onPlayback(false)} + onClick={onClick} + > + {playingBack ? ( + + ) : ( + + )} + {(review.severity == "alert" || review.severity == "detection") && ( + + {review.data.objects.map((object) => { + return getIconForLabel(object, "w-3 h-3 text-white"); })} -
- )} -
-
- {playingBack && ( - - )} - {!playingBack && review.has_been_reviewed && ( -
- )} -
+ {review.data.audio.map((audio) => { + return getIconForLabel(audio, "w-3 h-3 text-white"); + })} + {review.data.sub_labels?.map((sub) => { + return getIconForSubLabel(sub, "w-3 h-3 text-white"); + })} + + )} + {!playingBack && ( +
+ + {config && + formatUnixTimestampToDateTime(review.start_time, { + strftime_fmt: + config.ui.time_format == "24hour" + ? "%b %-d, %H:%M" + : "%b %-d, %I:%M %p", + })} +
+ )} +
+
+ {playingBack && ( + + )} + {!playingBack && review.has_been_reviewed && ( +
+ )} + + + ); } @@ -291,9 +306,9 @@ function InProgressPreview({ }: InProgressPreviewProps) { const apiHost = useApiHost(); const { data: previewFrames } = useSWR( - `preview/${review.camera}/start/${Math.floor( - review.start_time - ) - 4}/end/${Math.ceil(review.end_time) + 4}/frames` + `preview/${review.camera}/start/${Math.floor(review.start_time) - 4}/end/${ + Math.ceil(review.end_time) + 4 + }/frames` ); const [key, setKey] = useState(0); @@ -343,3 +358,47 @@ function InProgressPreview({
); } + +type PreviewContextItemsProps = { + review: ReviewSegment; + setReviewed?: () => void; +}; +function PreviewContextItems({ + review, + setReviewed, +}: PreviewContextItemsProps) { + const exportReview = useCallback(() => { + console.log( + "trying to export to " + + `export/${review.camera}/start/${review.start_time}/end/${review.end_time}` + ); + axios.post( + `export/${review.camera}/start/${review.start_time}/end/${review.end_time}`, + { playback: "realtime" } + ); + }, [review]); + + return ( + + (setReviewed ? setReviewed() : null)}> +
+ Mark As Reviewed + +
+
+ exportReview()}> +
+ Export + +
+
+ + +
+ Delete + +
+
+
+ ); +} diff --git a/web/src/components/ui/context-menu.tsx b/web/src/components/ui/context-menu.tsx new file mode 100644 index 0000000000..3e5299917f --- /dev/null +++ b/web/src/components/ui/context-menu.tsx @@ -0,0 +1,198 @@ +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/web/src/pages/Export.tsx b/web/src/pages/Export.tsx index a2ca53a9cc..878e1a8aa1 100644 --- a/web/src/pages/Export.tsx +++ b/web/src/pages/Export.tsx @@ -151,8 +151,7 @@ function Export() { }, [deleteClip]); return ( - <> - Export +
-
+
@@ -292,7 +291,7 @@ function Export() {
{exports && ( - + Exports {Object.values(exports).map((item) => ( )}
- +
); } From 30d456dbd2857bb5a44cdb3a6070b4c86fb5d988 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 23 Feb 2024 12:22:12 -0700 Subject: [PATCH 2/3] Only show mark as reviewed when it has not been reviewed --- .../components/player/PreviewThumbnailPlayer.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index d110ff354d..a2a971a5b2 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -380,12 +380,14 @@ function PreviewContextItems({ return ( - (setReviewed ? setReviewed() : null)}> -
- Mark As Reviewed - -
-
+ {!review.has_been_reviewed && ( + (setReviewed ? setReviewed() : null)}> +
+ Mark As Reviewed + +
+
+ )} exportReview()}>
Export From 63b37fbe8ab77ca38f4c84819bc03a3fc6e59658 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 23 Feb 2024 14:42:25 -0700 Subject: [PATCH 3/3] Fix float comparison --- web/src/components/player/PreviewThumbnailPlayer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index a2a971a5b2..f8a6905555 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -330,7 +330,7 @@ function InProgressPreview({ setProgress((key / (previewFrames.length - 1)) * 100); } - if (setReviewed && key == previewFrames.length / 2) { + if (setReviewed && key == Math.floor(previewFrames.length / 2)) { setReviewed(); }