mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Reviewed buttons (#10271)
* mark items as reviewed when they are opened * Update api to use json and add button to mark all as reviewed * fix api so last24 hours has its own review summary * fix sidebar spacing * formatting * Bug fixes * Make motion activity respect filters
This commit is contained in:
parent
b5edcd2fae
commit
68ed18d3f4
@ -76,11 +76,112 @@ def review():
|
|||||||
def review_summary():
|
def review_summary():
|
||||||
tz_name = request.args.get("timezone", default="utc", type=str)
|
tz_name = request.args.get("timezone", default="utc", type=str)
|
||||||
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
hour_modifier, minute_modifier, seconds_offset = get_tz_modifiers(tz_name)
|
||||||
|
day_ago = (datetime.now() - timedelta(hours=24)).timestamp()
|
||||||
month_ago = (datetime.now() - timedelta(days=30)).timestamp()
|
month_ago = (datetime.now() - timedelta(days=30)).timestamp()
|
||||||
|
|
||||||
cameras = request.args.get("cameras", "all")
|
cameras = request.args.get("cameras", "all")
|
||||||
labels = request.args.get("labels", "all")
|
labels = request.args.get("labels", "all")
|
||||||
|
|
||||||
|
clauses = [(ReviewSegment.start_time > day_ago)]
|
||||||
|
|
||||||
|
if cameras != "all":
|
||||||
|
camera_list = cameras.split(",")
|
||||||
|
clauses.append((ReviewSegment.camera << camera_list))
|
||||||
|
|
||||||
|
if labels != "all":
|
||||||
|
# use matching so segments with multiple labels
|
||||||
|
# still match on a search where any label matches
|
||||||
|
label_clauses = []
|
||||||
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
|
for label in filtered_labels:
|
||||||
|
label_clauses.append(
|
||||||
|
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
|
||||||
|
)
|
||||||
|
|
||||||
|
label_clause = reduce(operator.or_, label_clauses)
|
||||||
|
clauses.append((label_clause))
|
||||||
|
|
||||||
|
last_24 = (
|
||||||
|
ReviewSegment.select(
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "alert"),
|
||||||
|
ReviewSegment.has_been_reviewed,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("reviewed_alert"),
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "detection"),
|
||||||
|
ReviewSegment.has_been_reviewed,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("reviewed_detection"),
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "significant_motion"),
|
||||||
|
ReviewSegment.has_been_reviewed,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("reviewed_motion"),
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "alert"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("total_alert"),
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "detection"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("total_detection"),
|
||||||
|
fn.SUM(
|
||||||
|
Case(
|
||||||
|
None,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
(ReviewSegment.severity == "significant_motion"),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
).alias("total_motion"),
|
||||||
|
)
|
||||||
|
.where(reduce(operator.and_, clauses))
|
||||||
|
.dicts()
|
||||||
|
.get()
|
||||||
|
)
|
||||||
|
|
||||||
clauses = [(ReviewSegment.start_time > month_ago)]
|
clauses = [(ReviewSegment.start_time > month_ago)]
|
||||||
|
|
||||||
if cameras != "all":
|
if cameras != "all":
|
||||||
@ -101,7 +202,7 @@ def review_summary():
|
|||||||
label_clause = reduce(operator.or_, label_clauses)
|
label_clause = reduce(operator.or_, label_clauses)
|
||||||
clauses.append((label_clause))
|
clauses.append((label_clause))
|
||||||
|
|
||||||
groups = (
|
last_month = (
|
||||||
ReviewSegment.select(
|
ReviewSegment.select(
|
||||||
fn.strftime(
|
fn.strftime(
|
||||||
"%Y-%m-%d",
|
"%Y-%m-%d",
|
||||||
@ -192,29 +293,20 @@ def review_summary():
|
|||||||
.order_by(ReviewSegment.start_time.desc())
|
.order_by(ReviewSegment.start_time.desc())
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonify([e for e in groups.dicts().iterator()])
|
data = {
|
||||||
|
"last24Hours": last_24,
|
||||||
|
}
|
||||||
|
|
||||||
|
for e in last_month.dicts().iterator():
|
||||||
|
data[e["day"]] = e
|
||||||
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
@ReviewBp.route("/review/<id>/viewed", methods=("POST",))
|
@ReviewBp.route("/reviews/viewed", methods=("POST",))
|
||||||
def set_reviewed(id):
|
def set_multiple_reviewed():
|
||||||
try:
|
json: dict[str, any] = request.get_json(silent=True) or {}
|
||||||
review: ReviewSegment = ReviewSegment.get(ReviewSegment.id == id)
|
list_of_ids = json.get("ids", "")
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@ReviewBp.route("/reviews/<ids>/viewed", methods=("POST",))
|
|
||||||
def set_multiple_reviewed(ids: str):
|
|
||||||
list_of_ids = ids.split(",")
|
|
||||||
|
|
||||||
if not list_of_ids or len(list_of_ids) == 0:
|
if not list_of_ids or len(list_of_ids) == 0:
|
||||||
return make_response(
|
return make_response(
|
||||||
@ -264,13 +356,17 @@ def delete_reviews(ids: str):
|
|||||||
@ReviewBp.route("/review/activity")
|
@ReviewBp.route("/review/activity")
|
||||||
def review_activity():
|
def review_activity():
|
||||||
"""Get motion and audio activity."""
|
"""Get motion and audio activity."""
|
||||||
|
cameras = request.args.get("cameras", "all")
|
||||||
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||||
after = request.args.get(
|
after = request.args.get(
|
||||||
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||||
)
|
)
|
||||||
|
|
||||||
# get scale in seconds
|
clauses = [(Recordings.start_time > after) & (Recordings.end_time < before)]
|
||||||
scale = request.args.get("scale", type=int, default=30)
|
|
||||||
|
if cameras != "all":
|
||||||
|
camera_list = cameras.split(",")
|
||||||
|
clauses.append((Recordings.camera << camera_list))
|
||||||
|
|
||||||
all_recordings: list[Recordings] = (
|
all_recordings: list[Recordings] = (
|
||||||
Recordings.select(
|
Recordings.select(
|
||||||
@ -280,7 +376,7 @@ def review_activity():
|
|||||||
Recordings.motion,
|
Recordings.motion,
|
||||||
Recordings.dBFS,
|
Recordings.dBFS,
|
||||||
)
|
)
|
||||||
.where((Recordings.start_time > after) & (Recordings.end_time < before))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(Recordings.start_time.asc())
|
.order_by(Recordings.start_time.asc())
|
||||||
.iterator()
|
.iterator()
|
||||||
)
|
)
|
||||||
@ -298,6 +394,9 @@ def review_activity():
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# get scale in seconds
|
||||||
|
scale = request.args.get("scale", type=int, default=30)
|
||||||
|
|
||||||
# resample data using pandas to get activity on scaled basis
|
# resample data using pandas to get activity on scaled basis
|
||||||
df = pd.DataFrame(data, columns=["start_time", "motion", "audio"])
|
df = pd.DataFrame(data, columns=["start_time", "motion", "audio"])
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export default function ReviewActionGroup({
|
|||||||
|
|
||||||
const onMarkAsReviewed = useCallback(async () => {
|
const onMarkAsReviewed = useCallback(async () => {
|
||||||
const idList = selectedReviews.join(",");
|
const idList = selectedReviews.join(",");
|
||||||
await axios.post(`reviews/${idList}/viewed`);
|
await axios.post(`reviews/viewed`, { ids: idList });
|
||||||
setSelectedReviews([]);
|
setSelectedReviews([]);
|
||||||
pullLatestData();
|
pullLatestData();
|
||||||
}, [selectedReviews, setSelectedReviews, pullLatestData]);
|
}, [selectedReviews, setSelectedReviews, pullLatestData]);
|
||||||
|
@ -13,20 +13,23 @@ function Sidebar() {
|
|||||||
<span tabIndex={0} className="sr-only" />
|
<span tabIndex={0} className="sr-only" />
|
||||||
<div className="w-full flex flex-col gap-0 items-center">
|
<div className="w-full flex flex-col gap-0 items-center">
|
||||||
<Logo className="w-8 h-8 mb-6" />
|
<Logo className="w-8 h-8 mb-6" />
|
||||||
{navbarLinks.map((item) => (
|
{navbarLinks.map((item) => {
|
||||||
|
const showCameraGroups =
|
||||||
|
item.id == 1 && item.url == location.pathname;
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<NavItem
|
<NavItem
|
||||||
className={`mx-[10px] ${item.id == 1 ? "mb-2" : "mb-4"}`}
|
className={`mx-[10px] ${showCameraGroups ? "mb-2" : "mb-4"}`}
|
||||||
Icon={item.icon}
|
Icon={item.icon}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
url={item.url}
|
url={item.url}
|
||||||
dev={item.dev}
|
dev={item.dev}
|
||||||
/>
|
/>
|
||||||
{item.id == 1 && item.url == location.pathname && (
|
{showCameraGroups && <CameraGroupSelector className="mb-4" />}
|
||||||
<CameraGroupSelector className="mb-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
|
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -29,7 +29,7 @@ type DynamicVideoPlayerProps = {
|
|||||||
timeRange: { start: number; end: number };
|
timeRange: { start: number; end: number };
|
||||||
cameraPreviews: Preview[];
|
cameraPreviews: Preview[];
|
||||||
previewOnly?: boolean;
|
previewOnly?: boolean;
|
||||||
onControllerReady?: (controller: DynamicVideoController) => void;
|
onControllerReady: (controller: DynamicVideoController) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
export default function DynamicVideoPlayer({
|
export default function DynamicVideoPlayer({
|
||||||
@ -86,14 +86,17 @@ export default function DynamicVideoPlayer({
|
|||||||
}, [camera, config, previewOnly]);
|
}, [camera, config, previewOnly]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!controller) {
|
if (!playerRef.current && !previewRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onControllerReady) {
|
if (controller) {
|
||||||
onControllerReady(controller);
|
onControllerReady(controller);
|
||||||
}
|
}
|
||||||
}, [controller, onControllerReady]);
|
|
||||||
|
// we only want to fire once when players are ready
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [playerRef, previewRef]);
|
||||||
|
|
||||||
const [hasRecordingAtTime, setHasRecordingAtTime] = useState(true);
|
const [hasRecordingAtTime, setHasRecordingAtTime] = useState(true);
|
||||||
|
|
||||||
@ -277,10 +280,6 @@ export default function DynamicVideoPlayer({
|
|||||||
player.on("ended", () =>
|
player.on("ended", () =>
|
||||||
controller.fireClipChangeEvent("forward"),
|
controller.fireClipChangeEvent("forward"),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onControllerReady) {
|
|
||||||
onControllerReady(controller);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
onDispose={() => {
|
onDispose={() => {
|
||||||
playerRef.current = undefined;
|
playerRef.current = undefined;
|
||||||
|
@ -10,6 +10,7 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
select: "bg-select text-white hover:bg-select/90",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
outline:
|
outline:
|
||||||
|
@ -115,9 +115,7 @@ export default function Events() {
|
|||||||
|
|
||||||
// review summary
|
// review summary
|
||||||
|
|
||||||
const { data: reviewSummary, mutate: updateSummary } = useSWR<
|
const { data: reviewSummary, mutate: updateSummary } = useSWR<ReviewSummary>([
|
||||||
ReviewSummary[]
|
|
||||||
>([
|
|
||||||
"review/summary",
|
"review/summary",
|
||||||
{
|
{
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
@ -164,7 +162,7 @@ export default function Events() {
|
|||||||
|
|
||||||
const markItemAsReviewed = useCallback(
|
const markItemAsReviewed = useCallback(
|
||||||
async (review: ReviewSegment) => {
|
async (review: ReviewSegment) => {
|
||||||
const resp = await axios.post(`review/${review.id}/viewed`);
|
const resp = await axios.post(`reviews/viewed`, { ids: [review.id] });
|
||||||
|
|
||||||
if (resp.status == 200) {
|
if (resp.status == 200) {
|
||||||
updateSegments(
|
updateSegments(
|
||||||
@ -197,23 +195,30 @@ export default function Events() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
updateSummary(
|
updateSummary(
|
||||||
(data: ReviewSummary[] | undefined) => {
|
(data: ReviewSummary | undefined) => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const day = new Date(review.start_time * 1000);
|
const day = new Date(review.start_time * 1000);
|
||||||
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
|
const today = new Date();
|
||||||
const index = data.findIndex((summary) => summary.day == key);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
if (index == -1) {
|
let key;
|
||||||
|
if (day.getTime() > today.getTime()) {
|
||||||
|
key = "last24Hours";
|
||||||
|
} else {
|
||||||
|
key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Object.keys(data).includes(key)) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = data[index];
|
const item = data[key];
|
||||||
return [
|
return {
|
||||||
...data.slice(0, index),
|
...data,
|
||||||
{
|
[key]: {
|
||||||
...item,
|
...item,
|
||||||
reviewed_alert:
|
reviewed_alert:
|
||||||
review.severity == "alert"
|
review.severity == "alert"
|
||||||
@ -228,8 +233,7 @@ export default function Events() {
|
|||||||
? item.reviewed_motion + 1
|
? item.reviewed_motion + 1
|
||||||
: item.reviewed_motion,
|
: item.reviewed_motion,
|
||||||
},
|
},
|
||||||
...data.slice(index + 1),
|
};
|
||||||
];
|
|
||||||
},
|
},
|
||||||
{ revalidate: false, populateCache: true },
|
{ revalidate: false, populateCache: true },
|
||||||
);
|
);
|
||||||
@ -279,6 +283,11 @@ export default function Events() {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mark item as reviewed since it has been opened
|
||||||
|
if (!selectedReview?.has_been_reviewed) {
|
||||||
|
markItemAsReviewed(selectedReview);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
camera: selectedReview.camera,
|
camera: selectedReview.camera,
|
||||||
severity: selectedReview.severity,
|
severity: selectedReview.severity,
|
||||||
|
@ -28,7 +28,7 @@ export type ReviewFilter = {
|
|||||||
showReviewed?: 0 | 1;
|
showReviewed?: 0 | 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReviewSummary = {
|
type ReviewSummaryDay = {
|
||||||
day: string;
|
day: string;
|
||||||
reviewed_alert: number;
|
reviewed_alert: number;
|
||||||
reviewed_detection: number;
|
reviewed_detection: number;
|
||||||
@ -38,6 +38,10 @@ export type ReviewSummary = {
|
|||||||
total_motion: number;
|
total_motion: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ReviewSummary = {
|
||||||
|
[day: string]: ReviewSummaryDay;
|
||||||
|
};
|
||||||
|
|
||||||
export type MotionData = {
|
export type MotionData = {
|
||||||
start_time: number;
|
start_time: number;
|
||||||
motion: number;
|
motion: number;
|
||||||
|
@ -35,10 +35,11 @@ import { LuFolderCheck } from "react-icons/lu";
|
|||||||
import { MdCircle } from "react-icons/md";
|
import { MdCircle } from "react-icons/md";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
type EventViewProps = {
|
type EventViewProps = {
|
||||||
reviewPages?: ReviewSegment[][];
|
reviewPages?: ReviewSegment[][];
|
||||||
reviewSummary?: ReviewSummary[];
|
reviewSummary?: ReviewSummary;
|
||||||
relevantPreviews?: Preview[];
|
relevantPreviews?: Preview[];
|
||||||
timeRange: { before: number; after: number };
|
timeRange: { before: number; after: number };
|
||||||
reachedEnd: boolean;
|
reachedEnd: boolean;
|
||||||
@ -74,17 +75,17 @@ export default function EventView({
|
|||||||
// review counts
|
// review counts
|
||||||
|
|
||||||
const reviewCounts = useMemo(() => {
|
const reviewCounts = useMemo(() => {
|
||||||
if (!reviewSummary || reviewSummary.length == 0) {
|
if (!reviewSummary) {
|
||||||
return { alert: 0, detection: 0, significant_motion: 0 };
|
return { alert: 0, detection: 0, significant_motion: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary;
|
let summary;
|
||||||
if (filter?.before == undefined) {
|
if (filter?.before == undefined) {
|
||||||
summary = reviewSummary[0];
|
summary = reviewSummary["last24Hours"];
|
||||||
} else {
|
} else {
|
||||||
const day = new Date(filter.before * 1000);
|
const day = new Date(filter.before * 1000);
|
||||||
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
|
const key = `${day.getFullYear()}-${("0" + (day.getMonth() + 1)).slice(-2)}-${("0" + day.getDate()).slice(-2)}`;
|
||||||
summary = reviewSummary.find((check) => check.day == key);
|
summary = reviewSummary[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!summary) {
|
if (!summary) {
|
||||||
@ -211,9 +212,11 @@ export default function EventView({
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
className="*:px-3 *:py-4 *:rounded-2xl"
|
className="*:px-3 *:py-4 *:rounded-2xl"
|
||||||
type="single"
|
type="single"
|
||||||
defaultValue="alert"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
onValueChange={(value: ReviewSeverity) => setSeverity(value)}
|
value={severity}
|
||||||
|
onValueChange={(value: ReviewSeverity) =>
|
||||||
|
value ? setSeverity(value) : null
|
||||||
|
} // don't allow the severity to be unselected
|
||||||
>
|
>
|
||||||
<ToggleGroupItem
|
<ToggleGroupItem
|
||||||
className={`${severity == "alert" ? "" : "text-gray-500"}`}
|
className={`${severity == "alert" ? "" : "text-gray-500"}`}
|
||||||
@ -241,9 +244,7 @@ export default function EventView({
|
|||||||
aria-label="Select motion"
|
aria-label="Select motion"
|
||||||
>
|
>
|
||||||
<MdCircle className="size-2 md:mr-[10px] text-severity_motion" />
|
<MdCircle className="size-2 md:mr-[10px] text-severity_motion" />
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">Motion</div>
|
||||||
Motion ∙ {reviewCounts.significant_motion}
|
|
||||||
</div>
|
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
|
|
||||||
@ -303,6 +304,7 @@ type DetectionReviewProps = {
|
|||||||
detection: ReviewSegment[];
|
detection: ReviewSegment[];
|
||||||
significant_motion: ReviewSegment[];
|
significant_motion: ReviewSegment[];
|
||||||
};
|
};
|
||||||
|
itemsToReview?: number;
|
||||||
relevantPreviews?: Preview[];
|
relevantPreviews?: Preview[];
|
||||||
pagingObserver: MutableRefObject<IntersectionObserver | null>;
|
pagingObserver: MutableRefObject<IntersectionObserver | null>;
|
||||||
selectedReviews: string[];
|
selectedReviews: string[];
|
||||||
@ -320,6 +322,7 @@ function DetectionReview({
|
|||||||
contentRef,
|
contentRef,
|
||||||
currentItems,
|
currentItems,
|
||||||
reviewItems,
|
reviewItems,
|
||||||
|
itemsToReview,
|
||||||
relevantPreviews,
|
relevantPreviews,
|
||||||
pagingObserver,
|
pagingObserver,
|
||||||
selectedReviews,
|
selectedReviews,
|
||||||
@ -359,6 +362,17 @@ function DetectionReview({
|
|||||||
[isValidating, pagingObserver, reachedEnd, loadNextPage],
|
[isValidating, pagingObserver, reachedEnd, loadNextPage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const markAllReviewed = useCallback(async () => {
|
||||||
|
if (!currentItems) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(`reviews/viewed`, {
|
||||||
|
ids: currentItems?.map((seg) => seg.id),
|
||||||
|
});
|
||||||
|
pullLatestData();
|
||||||
|
}, [currentItems, pullLatestData]);
|
||||||
|
|
||||||
// timeline interaction
|
// timeline interaction
|
||||||
|
|
||||||
const { alignStartDateToTimeline } = useEventUtils(
|
const { alignStartDateToTimeline } = useEventUtils(
|
||||||
@ -453,7 +467,7 @@ function DetectionReview({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isValidating && currentItems == null && (
|
{(itemsToReview == 0 || (currentItems == null && !isValidating)) && (
|
||||||
<div className="size-full flex flex-col justify-center items-center">
|
<div className="size-full flex flex-col justify-center items-center">
|
||||||
<LuFolderCheck className="size-16" />
|
<LuFolderCheck className="size-16" />
|
||||||
There are no {severity.replace(/_/g, " ")} items to review
|
There are no {severity.replace(/_/g, " ")} items to review
|
||||||
@ -489,13 +503,27 @@ function DetectionReview({
|
|||||||
onClick={onSelectReview}
|
onClick={onSelectReview}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{lastRow && !reachedEnd && <ActivityIndicator />}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : severity != "alert" ? (
|
) : itemsToReview != 0 ? (
|
||||||
<div ref={lastReviewRef} />
|
<div ref={lastReviewRef} />
|
||||||
) : null}
|
) : null}
|
||||||
|
{currentItems && (
|
||||||
|
<div className="col-span-full flex justify-center items-center">
|
||||||
|
{reachedEnd ? (
|
||||||
|
<Button
|
||||||
|
className="text-white"
|
||||||
|
variant="select"
|
||||||
|
onClick={markAllReviewed}
|
||||||
|
>
|
||||||
|
Mark all items as reviewed
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ActivityIndicator />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
<div className="w-[55px] md:w-[100px] mt-2 overflow-y-auto no-scrollbar">
|
||||||
@ -574,6 +602,7 @@ function MotionReview({
|
|||||||
before: timeRange.before,
|
before: timeRange.before,
|
||||||
after: timeRange.after,
|
after: timeRange.after,
|
||||||
scale: segmentDuration / 2,
|
scale: segmentDuration / 2,
|
||||||
|
cameras: filter?.cameras?.join(",") ?? null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user