mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
Hide the overlays on hover and update reviewed status
This commit is contained in:
parent
c0d2d6ba57
commit
fc157e9422
@ -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"]
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -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} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user