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..f8a6905555 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);
@@ -315,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();
}
@@ -343,3 +358,49 @@ 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 (
+
+ {!review.has_been_reviewed && (
+ (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) => (
)}
- >
+
);
}