diff --git a/frigate/http.py b/frigate/http.py index 179094b07..38a16b299 100644 --- a/frigate/http.py +++ b/frigate/http.py @@ -2420,6 +2420,40 @@ def review(): return jsonify([r for r in review]) +@bp.route("/review//viewed", methods=("POST",)) +def set_reviewed(id): + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == id) + except DoesNotExist: + return make_response( + jsonify({"success": False, "message": "Review " + id + " not found"}), 404 + ) + + review.has_been_reviewed = True + review.save() + + return make_response( + jsonify({"success": True, "message": "Reviewed " + id + " viewed"}), 200 + ) + + +@bp.route("/review//viewed", methods=("DELETE",)) +def set_not_reviewed(id): + try: + review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == id) + except DoesNotExist: + return make_response( + jsonify({"success": False, "message": "Review " + id + " not found"}), 404 + ) + + review.has_been_reviewed = False + review.save() + + return make_response( + jsonify({"success": True, "message": "Reviewed " + id + " not viewed"}), 200 + ) + + @bp.route( "/export//start//end/", methods=["POST"] ) diff --git a/web/src/components/player/PreviewThumbnailPlayer.tsx b/web/src/components/player/PreviewThumbnailPlayer.tsx index d6ae40fe4..ace8a9b8d 100644 --- a/web/src/components/player/PreviewThumbnailPlayer.tsx +++ b/web/src/components/player/PreviewThumbnailPlayer.tsx @@ -2,15 +2,20 @@ import VideoPlayer from "./VideoPlayer"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { useApiHost } from "@/api"; import Player from "video.js/dist/types/player"; -import { isCurrentHour } from "@/utils/dateUtil"; +import { formatUnixTimestampToDateTime, isCurrentHour } from "@/utils/dateUtil"; import { isSafari } from "@/utils/browserUtil"; import { ReviewSegment } from "@/types/review"; import { Slider } from "../ui/slider"; +import { getIconForLabel } from "@/utils/iconUtil"; +import TimeAgo from "../dynamic/TimeAgo"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; type PreviewPlayerProps = { review: ReviewSegment; relevantPreview?: Preview; isMobile: boolean; + setReviewed?: () => void; onClick?: () => void; }; @@ -26,8 +31,10 @@ export default function PreviewThumbnailPlayer({ review, relevantPreview, isMobile, + setReviewed, onClick, }: PreviewPlayerProps) { + const { data: config } = useSWR("config"); const playerRef = useRef(null); const [visible, setVisible] = useState(false); @@ -128,8 +135,29 @@ export default function PreviewThumbnailPlayer({ isInitiallyVisible={isInitiallyVisible} isMobile={isMobile} setProgress={setProgress} + setReviewed={setReviewed} onClick={onClick} /> + {!hover && + (review.severity == "alert" || review.severity == "detection") && ( +
+ {review.data.objects.map((object) => { + return getIconForLabel(object); + })} +
+ )} + {!hover && ( +
+ + {config && + formatUnixTimestampToDateTime(review.start_time, { + strftime_fmt: + config.ui.time_format == "24hour" + ? "%b %-d, %H:%M" + : "%b %-d, %I:%M %p", + })} +
+ )}
{hover && ( @@ -153,6 +181,7 @@ type PreviewContentProps = { isInitiallyVisible: boolean; isMobile: boolean; setProgress?: (progress: number) => void; + setReviewed?: () => void; onClick?: () => void; }; function PreviewContent({ @@ -163,6 +192,7 @@ function PreviewContent({ isInitiallyVisible, isMobile, setProgress, + setReviewed, onClick, }: PreviewContentProps) { const apiHost = useApiHost(); @@ -253,6 +283,14 @@ function PreviewContent({ const playerDuration = review.end_time - review.start_time; const playerPercent = (playerProgress / playerDuration) * 100; + if ( + setReviewed && + !review.has_been_reviewed && + playerPercent > 50 + ) { + setReviewed(); + } + if (playerPercent > 100) { playerRef.current?.pause(); setProgress(100.0); diff --git a/web/src/pages/Events.tsx b/web/src/pages/Events.tsx index c4bd547bf..453ab196b 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -1,12 +1,16 @@ -import TimeAgo from "@/components/dynamic/TimeAgo"; import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; import ActivityIndicator from "@/components/ui/activity-indicator"; import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { FrigateConfig } from "@/types/frigateConfig"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; -import { getIconForLabel } from "@/utils/iconUtil"; import axios from "axios"; import { useCallback, useMemo, useRef, useState } from "react"; import { LuCalendar, LuFilter, LuVideo } from "react-icons/lu"; @@ -21,6 +25,7 @@ export default function Events() { const [severity, setSeverity] = useState("alert"); // review paging + const reviewSearchParams = {}; const reviewSegmentFetcher = useCallback((key: any) => { const [path, params] = Array.isArray(key) ? key : [key, undefined]; @@ -81,6 +86,19 @@ export default function Events() { [isValidating, isDone] ); + // review status + + const setReviewed = useCallback( + async (id: string) => { + const resp = await axios.post(`review/${id}/viewed`); + + if (resp.status == 200) { + updateSegments(); + } + }, + [updateSegments] + ); + // preview videos const previewTimes = useMemo(() => { @@ -158,10 +176,7 @@ export default function Events() { All Cameras - +
{lastRow && !isDone && }
@@ -222,3 +222,29 @@ export default function Events() { ); } + +function ReviewCalendarButton() { + const disabledDates = useMemo(() => { + const tomorrow = new Date(); + tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); + const future = new Date(); + future.setFullYear(tomorrow.getFullYear() + 10); + return { from: tomorrow, to: future }; + }, []); + + return ( + + + + + + + + + ); +} diff --git a/web/src/utils/iconUtil.tsx b/web/src/utils/iconUtil.tsx index 5dbfda599..e688accf9 100644 --- a/web/src/utils/iconUtil.tsx +++ b/web/src/utils/iconUtil.tsx @@ -9,14 +9,14 @@ import { export function getIconForLabel(label: string, className?: string) { switch (label) { case "car": - return ; + return ; case "dog": - return ; + return ; case "package": - return ; + return ; case "person": - return ; + return ; default: - return ; + return ; } }