Hide the overlays on hover and update reviewed status

This commit is contained in:
Nicolas Mowen 2024-02-18 00:15:30 -07:00
parent c0d2d6ba57
commit fc157e9422
4 changed files with 126 additions and 28 deletions

View File

@ -2420,6 +2420,40 @@ def review():
return jsonify([r for r in review]) return jsonify([r for r in review])
@bp.route("/review/<id>/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/<id>/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( @bp.route(
"/export/<camera_name>/start/<int:start_time>/end/<int:end_time>", methods=["POST"] "/export/<camera_name>/start/<int:start_time>/end/<int:end_time>", methods=["POST"]
) )

View File

@ -2,15 +2,20 @@ import VideoPlayer from "./VideoPlayer";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { useApiHost } from "@/api"; import { useApiHost } from "@/api";
import Player from "video.js/dist/types/player"; 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 { isSafari } from "@/utils/browserUtil";
import { ReviewSegment } from "@/types/review"; import { ReviewSegment } from "@/types/review";
import { Slider } from "../ui/slider"; 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 = { type PreviewPlayerProps = {
review: ReviewSegment; review: ReviewSegment;
relevantPreview?: Preview; relevantPreview?: Preview;
isMobile: boolean; isMobile: boolean;
setReviewed?: () => void;
onClick?: () => void; onClick?: () => void;
}; };
@ -26,8 +31,10 @@ export default function PreviewThumbnailPlayer({
review, review,
relevantPreview, relevantPreview,
isMobile, isMobile,
setReviewed,
onClick, onClick,
}: PreviewPlayerProps) { }: PreviewPlayerProps) {
const { data: config } = useSWR<FrigateConfig>("config");
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
@ -128,8 +135,29 @@ export default function PreviewThumbnailPlayer({
isInitiallyVisible={isInitiallyVisible} isInitiallyVisible={isInitiallyVisible}
isMobile={isMobile} isMobile={isMobile}
setProgress={setProgress} setProgress={setProgress}
setReviewed={setReviewed}
onClick={onClick} onClick={onClick}
/> />
{!hover &&
(review.severity == "alert" || review.severity == "detection") && (
<div className="absolute top-1 right-1 flex gap-1">
{review.data.objects.map((object) => {
return getIconForLabel(object);
})}
</div>
)}
{!hover && (
<div className="absolute left-1 right-1 bottom-1 flex justify-between">
<TimeAgo time={review.start_time * 1000} />
{config &&
formatUnixTimestampToDateTime(review.start_time, {
strftime_fmt:
config.ui.time_format == "24hour"
? "%b %-d, %H:%M"
: "%b %-d, %I:%M %p",
})}
</div>
)}
<div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" /> <div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" />
<div className="absolute bottom-0 left-0 right-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none" /> <div className="absolute bottom-0 left-0 right-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
{hover && ( {hover && (
@ -153,6 +181,7 @@ type PreviewContentProps = {
isInitiallyVisible: boolean; isInitiallyVisible: boolean;
isMobile: boolean; isMobile: boolean;
setProgress?: (progress: number) => void; setProgress?: (progress: number) => void;
setReviewed?: () => void;
onClick?: () => void; onClick?: () => void;
}; };
function PreviewContent({ function PreviewContent({
@ -163,6 +192,7 @@ function PreviewContent({
isInitiallyVisible, isInitiallyVisible,
isMobile, isMobile,
setProgress, setProgress,
setReviewed,
onClick, onClick,
}: PreviewContentProps) { }: PreviewContentProps) {
const apiHost = useApiHost(); const apiHost = useApiHost();
@ -253,6 +283,14 @@ function PreviewContent({
const playerDuration = review.end_time - review.start_time; const playerDuration = review.end_time - review.start_time;
const playerPercent = (playerProgress / playerDuration) * 100; const playerPercent = (playerProgress / playerDuration) * 100;
if (
setReviewed &&
!review.has_been_reviewed &&
playerPercent > 50
) {
setReviewed();
}
if (playerPercent > 100) { if (playerPercent > 100) {
playerRef.current?.pause(); playerRef.current?.pause();
setProgress(100.0); setProgress(100.0);

View File

@ -1,12 +1,16 @@
import TimeAgo from "@/components/dynamic/TimeAgo";
import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer";
import ActivityIndicator from "@/components/ui/activity-indicator"; import ActivityIndicator from "@/components/ui/activity-indicator";
import { Button } from "@/components/ui/button"; 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 { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { ReviewSegment, ReviewSeverity } from "@/types/review"; import { ReviewSegment, ReviewSeverity } from "@/types/review";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { getIconForLabel } from "@/utils/iconUtil";
import axios from "axios"; import axios from "axios";
import { useCallback, useMemo, useRef, useState } from "react"; import { useCallback, useMemo, useRef, useState } from "react";
import { LuCalendar, LuFilter, LuVideo } from "react-icons/lu"; import { LuCalendar, LuFilter, LuVideo } from "react-icons/lu";
@ -21,6 +25,7 @@ export default function Events() {
const [severity, setSeverity] = useState<ReviewSeverity>("alert"); const [severity, setSeverity] = useState<ReviewSeverity>("alert");
// review paging // review paging
const reviewSearchParams = {}; const reviewSearchParams = {};
const reviewSegmentFetcher = useCallback((key: any) => { const reviewSegmentFetcher = useCallback((key: any) => {
const [path, params] = Array.isArray(key) ? key : [key, undefined]; const [path, params] = Array.isArray(key) ? key : [key, undefined];
@ -81,6 +86,19 @@ export default function Events() {
[isValidating, isDone] [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 // preview videos
const previewTimes = useMemo(() => { const previewTimes = useMemo(() => {
@ -158,10 +176,7 @@ export default function Events() {
<LuVideo className=" mr-[10px]" /> <LuVideo className=" mr-[10px]" />
All Cameras All Cameras
</Button> </Button>
<Button className="mx-1" variant="secondary"> <ReviewCalendarButton />
<LuCalendar className=" mr-[10px]" />
Fab 17
</Button>
<Button className="mx-1" variant="secondary"> <Button className="mx-1" variant="secondary">
<LuFilter className=" mr-[10px]" /> <LuFilter className=" mr-[10px]" />
Filter Filter
@ -195,23 +210,8 @@ export default function Events() {
review={value} review={value}
relevantPreview={relevantPreview} relevantPreview={relevantPreview}
isMobile={false} isMobile={false}
setReviewed={() => setReviewed(value.id)}
/> />
{(severity == "alert" || severity == "detection") && (
<div className="absolute top-1 right-1 flex">
{value.data.objects.map((object) => {
return getIconForLabel(object);
})}
</div>
)}
<div className="absolute left-1 right-1 bottom-1 flex justify-between">
<TimeAgo time={value.start_time * 1000} />
{formatUnixTimestampToDateTime(value.start_time, {
strftime_fmt:
config.ui.time_format == "24hour"
? "%b %-d, %H:%M"
: "%b %-d, %I:%M %p",
})}
</div>
</div> </div>
{lastRow && !isDone && <ActivityIndicator />} {lastRow && !isDone && <ActivityIndicator />}
</div> </div>
@ -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 (
<Popover>
<PopoverTrigger asChild>
<Button className="mx-1" variant="secondary">
<LuCalendar className=" mr-[10px]" />
{formatUnixTimestampToDateTime(Date.now() / 1000, {
strftime_fmt: "%b %-d",
})}
</Button>
</PopoverTrigger>
<PopoverContent>
<Calendar mode="single" disabled={disabledDates} />
</PopoverContent>
</Popover>
);
}

View File

@ -9,14 +9,14 @@ import {
export function getIconForLabel(label: string, className?: string) { export function getIconForLabel(label: string, className?: string) {
switch (label) { switch (label) {
case "car": case "car":
return <LuCar className={className} />; return <LuCar key={label} className={className} />;
case "dog": case "dog":
return <LuDog className={className} />; return <LuDog key={label} className={className} />;
case "package": case "package":
return <LuBox className={className} />; return <LuBox key={label} className={className} />;
case "person": case "person":
return <LuPersonStanding className={className} />; return <LuPersonStanding key={label} className={className} />;
default: default:
return <LuLassoSelect className={className} />; return <LuLassoSelect key={label} className={className} />;
} }
} }