From a87cca23eacd16f23375530facf2c2c1eef9788d Mon Sep 17 00:00:00 2001
From: Nicolas Mowen <nickmowen213@gmail.com>
Date: Wed, 17 Apr 2024 06:02:03 -0600
Subject: [PATCH] Add ability to link to review items directly (#11002)

* Fix action group icon colors

* Add ability to query specific review item

* Pull id search key and open recordings to review item
---
 frigate/api/review.py                         |  9 ++++++
 .../components/filter/ReviewActionGroup.tsx   |  8 ++---
 web/src/hooks/use-overlay-state.tsx           | 29 ++++++++++++++++++-
 web/src/pages/Events.tsx                      | 20 ++++++++++++-
 4 files changed, 60 insertions(+), 6 deletions(-)

diff --git a/frigate/api/review.py b/frigate/api/review.py
index 2ad36962e..fa1dee73c 100644
--- a/frigate/api/review.py
+++ b/frigate/api/review.py
@@ -8,6 +8,7 @@ from pathlib import Path
 import pandas as pd
 from flask import Blueprint, jsonify, make_response, request
 from peewee import Case, DoesNotExist, fn, operator
+from playhouse.shortcuts import model_to_dict
 
 from frigate.models import Recordings, ReviewSegment
 from frigate.util.builtin import get_tz_modifiers
@@ -78,6 +79,14 @@ def review():
     return jsonify([r for r in review])
 
 
+@ReviewBp.route("/review/<id>")
+def get_review(id: str):
+    try:
+        return model_to_dict(ReviewSegment.get(ReviewSegment.id == id))
+    except DoesNotExist:
+        return "Review item not found", 404
+
+
 @ReviewBp.route("/review/summary")
 def review_summary():
     tz_name = request.args.get("timezone", default="utc", type=str)
diff --git a/web/src/components/filter/ReviewActionGroup.tsx b/web/src/components/filter/ReviewActionGroup.tsx
index cd31112af..fa32c92a6 100644
--- a/web/src/components/filter/ReviewActionGroup.tsx
+++ b/web/src/components/filter/ReviewActionGroup.tsx
@@ -56,7 +56,7 @@ export default function ReviewActionGroup({
               onClearSelected();
             }}
           >
-            <FaCompactDisc />
+            <FaCompactDisc className="text-secondary-foreground" />
             {isDesktop && <div className="text-primary">Export</div>}
           </Button>
         )}
@@ -65,15 +65,15 @@ export default function ReviewActionGroup({
           size="sm"
           onClick={onMarkAsReviewed}
         >
-          <FaCircleCheck />
+          <FaCircleCheck className="text-secondary-foreground" />
           {isDesktop && <div className="text-primary">Mark as reviewed</div>}
         </Button>
         <Button
-          className="p-2 flex items-center gap-1"
+          className="p-2 flex items-center gap-2"
           size="sm"
           onClick={onDelete}
         >
-          <HiTrash />
+          <HiTrash className="text-secondary-foreground" />
           {isDesktop && <div className="text-primary">Delete</div>}
         </Button>
       </div>
diff --git a/web/src/hooks/use-overlay-state.tsx b/web/src/hooks/use-overlay-state.tsx
index c2f2a0f85..656b61bda 100644
--- a/web/src/hooks/use-overlay-state.tsx
+++ b/web/src/hooks/use-overlay-state.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo } from "react";
+import { useCallback, useEffect, useMemo } from "react";
 import { useLocation, useNavigate } from "react-router-dom";
 import { usePersistence } from "./use-persistence";
 
@@ -91,3 +91,30 @@ export function useHashState<S extends string>(): [
 
   return [hash, setHash];
 }
+
+export function useSearchEffect(
+  key: string,
+  callback: (value: string) => void,
+) {
+  const location = useLocation();
+
+  const param = useMemo(() => {
+    if (!location || !location.search || location.search.length == 0) {
+      return undefined;
+    }
+
+    const params = location.search.substring(1).split("&");
+
+    return params
+      .find((p) => p.includes("=") && p.split("=")[0] == key)
+      ?.split("=");
+  }, [location, key]);
+
+  useEffect(() => {
+    if (!param) {
+      return;
+    }
+
+    callback(param[1]);
+  }, [param, callback]);
+}
diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx
index 6277c6994..0cc3cd6b5 100644
--- a/web/src/pages/Events.tsx
+++ b/web/src/pages/Events.tsx
@@ -1,7 +1,7 @@
 import ActivityIndicator from "@/components/indicators/activity-indicator";
 import useApiFilter from "@/hooks/use-api-filter";
 import { useTimezone } from "@/hooks/use-date-utils";
-import { useOverlayState } from "@/hooks/use-overlay-state";
+import { useOverlayState, useSearchEffect } from "@/hooks/use-overlay-state";
 import { FrigateConfig } from "@/types/frigateConfig";
 import { Preview } from "@/types/preview";
 import { RecordingStartingPoint } from "@/types/record";
@@ -33,6 +33,24 @@ export default function Events() {
   const [recording, setRecording] =
     useOverlayState<RecordingStartingPoint>("recording");
 
+  useSearchEffect("id", (reviewId: string) => {
+    axios
+      .get(`review/${reviewId}`)
+      .then((resp) => {
+        if (resp.status == 200 && resp.data) {
+          setRecording(
+            {
+              camera: resp.data.camera,
+              startTime: resp.data.start_time,
+              severity: resp.data.severity,
+            },
+            true,
+          );
+        }
+      })
+      .catch(() => {});
+  });
+
   const [startTime, setStartTime] = useState<number>();
 
   useEffect(() => {