mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-26 13:47:03 +02:00
Add ability to interact with review items in events list (#11562)
* Add ability to interact with review items * Ignore on iOS * Don't load metadata * Bug fixes
This commit is contained in:
parent
bfeb7b8a96
commit
5900a2a4ba
@ -3,12 +3,24 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils";
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { ReviewSegment } from "@/types/review";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import { isSafari } from "react-device-detect";
|
||||
import { isDesktop, isIOS, isSafari } from "react-device-detect";
|
||||
import useSWR from "swr";
|
||||
import TimeAgo from "../dynamic/TimeAgo";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import useImageLoaded from "@/hooks/use-image-loaded";
|
||||
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
|
||||
import { FaCompactDisc } from "react-icons/fa";
|
||||
import { FaCircleCheck } from "react-icons/fa6";
|
||||
import { HiTrash } from "react-icons/hi";
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from "../ui/context-menu";
|
||||
import { Drawer, DrawerContent } from "../ui/drawer";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type ReviewCardProps = {
|
||||
event: ReviewSegment;
|
||||
@ -33,10 +45,61 @@ export default function ReviewCard({
|
||||
[event, currentTime],
|
||||
);
|
||||
|
||||
return (
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
|
||||
const onMarkAsReviewed = useCallback(async () => {
|
||||
await axios.post(`reviews/viewed`, { ids: [event.id] });
|
||||
event.has_been_reviewed = true;
|
||||
setOptionsOpen(false);
|
||||
}, [event]);
|
||||
|
||||
const onExport = useCallback(async () => {
|
||||
axios
|
||||
.post(
|
||||
`export/${event.camera}/start/${event.start_time}/end/${event.end_time}`,
|
||||
{ playback: "realtime" },
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200) {
|
||||
toast.success(
|
||||
"Successfully started export. View the file in the /exports folder.",
|
||||
{ position: "top-center" },
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response?.data?.message) {
|
||||
toast.error(
|
||||
`Failed to start export: ${error.response.data.message}`,
|
||||
{ position: "top-center" },
|
||||
);
|
||||
} else {
|
||||
toast.error(`Failed to start export: ${error.message}`, {
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
});
|
||||
setOptionsOpen(false);
|
||||
}, [event]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
await axios.post(`reviews/delete`, { ids: [event.id] });
|
||||
event.id = "";
|
||||
setOptionsOpen(false);
|
||||
}, [event]);
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className="relative flex w-full cursor-pointer flex-col gap-1.5"
|
||||
onClick={onClick}
|
||||
onContextMenu={
|
||||
isDesktop
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.preventDefault();
|
||||
setOptionsOpen(true);
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageLoadingIndicator
|
||||
className="absolute inset-0"
|
||||
@ -47,6 +110,15 @@ export default function ReviewCard({
|
||||
className={`size-full rounded-lg ${isSelected ? "outline outline-[3px] outline-offset-1 outline-selected" : ""} ${imgLoaded ? "visible" : "invisible"}`}
|
||||
src={`${baseUrl}${event.thumb_path.replace("/media/frigate/", "")}`}
|
||||
loading={isSafari ? "eager" : "lazy"}
|
||||
style={
|
||||
isIOS
|
||||
? {
|
||||
WebkitUserSelect: "none",
|
||||
WebkitTouchCallout: "none",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
draggable={false}
|
||||
onLoad={() => {
|
||||
onImgLoad();
|
||||
}}
|
||||
@ -69,4 +141,78 @@ export default function ReviewCard({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.id == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDesktop) {
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
|
||||
onClick={onExport}
|
||||
>
|
||||
<FaCompactDisc className="text-secondary-foreground" />
|
||||
<div className="text-primary">Export</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
{!event.has_been_reviewed && (
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
<div className="text-primary">Mark as reviewed</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem>
|
||||
<div
|
||||
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
<div className="text-primary">Delete</div>
|
||||
</div>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer open={optionsOpen} onOpenChange={setOptionsOpen}>
|
||||
{content}
|
||||
<DrawerContent>
|
||||
<div
|
||||
className="flex w-full items-center justify-start gap-2 p-2"
|
||||
onClick={onExport}
|
||||
>
|
||||
<FaCompactDisc className="text-secondary-foreground" />
|
||||
<div className="text-primary">Export</div>
|
||||
</div>
|
||||
{!event.has_been_reviewed && (
|
||||
<div
|
||||
className="flex w-full items-center justify-start gap-2 p-2"
|
||||
onClick={onMarkAsReviewed}
|
||||
>
|
||||
<FaCircleCheck className="text-secondary-foreground" />
|
||||
<div className="text-primary">Mark as reviewed</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex w-full items-center justify-start gap-2 p-2"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
<div className="text-primary">Delete</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -272,8 +272,8 @@ export default function HlsVideoPlayer({
|
||||
? onTimeUpdate(videoRef.current.currentTime)
|
||||
: undefined
|
||||
}
|
||||
onLoadedData={onPlayerLoaded}
|
||||
onLoadedMetadata={() => {
|
||||
onLoadedData={() => {
|
||||
onPlayerLoaded?.();
|
||||
handleLoadedMetadata();
|
||||
|
||||
if (videoRef.current) {
|
||||
|
@ -135,7 +135,7 @@ export default function LivePlayer({
|
||||
);
|
||||
}
|
||||
} else if (liveMode == "jsmpeg") {
|
||||
if (cameraActive) {
|
||||
if (cameraActive || !showStillWithoutActivity) {
|
||||
player = (
|
||||
<JSMpegPlayer
|
||||
className="flex size-full justify-center overflow-hidden rounded-lg md:rounded-2xl"
|
||||
|
@ -302,7 +302,10 @@ function PreviewVideoPlayer({
|
||||
}}
|
||||
>
|
||||
{currentPreview != undefined && (
|
||||
<source src={currentPreview.src} type={currentPreview.type} />
|
||||
<source
|
||||
src={`${baseUrl}${currentPreview.src.substring(1)}`}
|
||||
type={currentPreview.type}
|
||||
/>
|
||||
)}
|
||||
</video>
|
||||
{cameraPreviews && !currentPreview && (
|
||||
|
@ -25,6 +25,7 @@ import { TimeRange } from "@/types/timeline";
|
||||
import { NoThumbSlider } from "../ui/slider";
|
||||
import { PREVIEW_FPS, PREVIEW_PADDING } from "@/types/preview";
|
||||
import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
|
||||
type PreviewPlayerProps = {
|
||||
review: ReviewSegment;
|
||||
@ -575,7 +576,10 @@ export function VideoPreview({
|
||||
muted
|
||||
onTimeUpdate={onProgress}
|
||||
>
|
||||
<source src={relevantPreview.src} type={relevantPreview.type} />
|
||||
<source
|
||||
src={`${baseUrl}${relevantPreview.src.substring(1)}`}
|
||||
type={relevantPreview.type}
|
||||
/>
|
||||
</video>
|
||||
{showProgress && (
|
||||
<NoThumbSlider
|
||||
|
Loading…
Reference in New Issue
Block a user