Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp multiselect #10172

Merged
merged 3 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 46 additions & 37 deletions web/src/components/filter/ReviewActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { LuCheckSquare, LuTrash, LuX } from "react-icons/lu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
import { useCallback } from "react";
import axios from "axios";
import { Button } from "../ui/button";
import { isDesktop } from "react-device-detect";

type ReviewActionGroupProps = {
selectedReviews: string[];
setSelectedReviews: (ids: string[]) => void;
onExport: (id: string) => void;
pullLatestData: () => void;
};
export default function ReviewActionGroup({
selectedReviews,
setSelectedReviews,
onExport,
pullLatestData,
}: ReviewActionGroupProps) {
const onClearSelected = useCallback(() => {
Expand All @@ -37,36 +35,47 @@ export default function ReviewActionGroup({
}, [selectedReviews, setSelectedReviews, pullLatestData]);

return (
<div className="absolute inset-x-2 md:inset-x-[40%] top-0 p-2 bg-primary-foreground md:border-2 md:rounded-lg flex justify-between items-center">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-pointer" onClick={onClearSelected}>
<LuX />
</div>
</TooltipTrigger>
<TooltipContent>Unselect All</TooltipContent>
</Tooltip>
<div className="flex gap-2 items-center">
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-pointer" onClick={onMarkAsReviewed}>
<LuCheckSquare />
</div>
</TooltipTrigger>
<TooltipContent>Mark Selected As Reviewed</TooltipContent>
</Tooltip>
<div className="text-sm font-thin">|</div>
<Tooltip>
<TooltipTrigger asChild>
<div className="cursor-pointer" onClick={onDelete}>
<LuTrash />
</div>
</TooltipTrigger>
<TooltipContent>Delete Selected</TooltipContent>
</Tooltip>
</div>
</TooltipProvider>
<div className="absolute inset-x-2 inset-y-0 md:left-auto md:right-2 p-2 flex gap-2 justify-between items-center bg-background">
<div className="flex items-center">
<div className="text-sm text-gray-500 mr-2">{`${selectedReviews.length} selected | `}</div>
<Button size="xs" variant="link" onClick={onClearSelected}>
Unselect
</Button>
</div>
<div className="flex items-center gap-1 md:gap-2">
{selectedReviews.length == 1 && (
<Button
className="flex items-center"
variant="secondary"
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
onClearSelected();
}}
>
<LuFileUp className="mr-1" />
{isDesktop && "Export"}
</Button>
)}
<Button
className="flex items-center"
variant="secondary"
size="sm"
onClick={onMarkAsReviewed}
>
<LuCheckSquare className="mr-1" />
{isDesktop && "Mark as reviewed"}
</Button>
<Button
className="flex items-center"
variant="secondary"
size="sm"
onClick={onDelete}
>
<LuTrash className="mr-1" />
{isDesktop && "Delete"}
</Button>
</div>
</div>
);
}
224 changes: 69 additions & 155 deletions web/src/components/player/PreviewThumbnailPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { isFirefox, isMobile, isSafari } from "react-device-detect";
import Chip from "../Chip";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
import { LuCheckCheck, LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
import { RiCheckboxMultipleLine } from "react-icons/ri";
import axios from "axios";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useImageLoaded from "@/hooks/use-image-loaded";
import { Skeleton } from "../ui/skeleton";
Expand All @@ -35,7 +25,6 @@ type PreviewPlayerProps = {
allPreviews?: Preview[];
onTimeUpdate?: React.Dispatch<React.SetStateAction<number | undefined>>;
setReviewed: (reviewId: string) => void;
markAboveReviewed: () => void;
onClick: (reviewId: string, ctrl: boolean) => void;
};

Expand All @@ -51,7 +40,6 @@ export default function PreviewThumbnailPlayer({
review,
allPreviews,
setReviewed,
markAboveReviewed,
onClick,
onTimeUpdate,
}: PreviewPlayerProps) {
Expand Down Expand Up @@ -167,85 +155,76 @@ export default function PreviewThumbnailPlayer({
);

return (
<ContextMenu>
<ContextMenuTrigger asChild>
<div
className="relative size-full cursor-pointer"
onMouseEnter={isMobile ? undefined : () => onPlayback(true)}
onMouseLeave={isMobile ? undefined : () => onPlayback(false)}
onClick={handleOnClick}
{...swipeHandlers}
>
{playingBack && (
<div className="absolute inset-0 animate-in fade-in">
<PreviewContent
review={review}
relevantPreview={relevantPreview}
setReviewed={handleSetReviewed}
setIgnoreClick={setIgnoreClick}
isPlayingBack={setPlayback}
onTimeUpdate={onTimeUpdate}
/>
</div>
)}
<PreviewPlaceholder imgLoaded={imgLoaded} />
<div className={`${imgLoaded ? "visible" : "invisible"}`}>
<img
ref={imgRef}
className={`w-full h-full transition-opacity ${
playingBack ? "opacity-0" : "opacity-100"
}`}
src={`${apiHost}${review.thumb_path.replace(
"/media/frigate/",
"",
)}`}
loading={isSafari ? "eager" : "lazy"}
onLoad={() => {
onImgLoad();
}}
/>

{!playingBack && (
<>
<div className="absolute top-0 inset-x-0 rounded-t-l z-10 w-full h-[30%] bg-gradient-to-b from-black/60 to-transparent pointer-events-none">
<div className="flex h-full justify-between items-start mx-3 pb-1 text-white text-sm ">
{(review.severity == "alert" ||
review.severity == "detection") && (
<Chip className="absolute top-2 left-2 flex gap-1 bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 z-0">
{review.data.objects.map((object) => {
return getIconForLabel(object, "size-3 text-white");
})}
{review.data.audio.map((audio) => {
return getIconForLabel(audio, "size-3 text-white");
})}
{review.data.sub_labels?.map((sub) => {
return getIconForSubLabel(sub, "size-3 text-white");
})}
</Chip>
)}
</div>
</div>
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 w-full h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none">
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm ">
<TimeAgo time={review.start_time * 1000} dense />
{formattedDate}
</div>
</div>
</>
)}
</div>
{!playingBack && imgLoaded && review.has_been_reviewed && (
<div className="absolute inset-0 z-10 bg-black bg-opacity-60" />
)}
<div
className="relative size-full cursor-pointer"
onMouseEnter={isMobile ? undefined : () => onPlayback(true)}
onMouseLeave={isMobile ? undefined : () => onPlayback(false)}
onContextMenu={(e) => {
e.preventDefault();
onClick(review.id, true);
}}
onClick={handleOnClick}
{...swipeHandlers}
>
{playingBack && (
<div className="absolute inset-0 animate-in fade-in">
<PreviewContent
review={review}
relevantPreview={relevantPreview}
setReviewed={handleSetReviewed}
setIgnoreClick={setIgnoreClick}
isPlayingBack={setPlayback}
onTimeUpdate={onTimeUpdate}
/>
</div>
</ContextMenuTrigger>
<PreviewContextItems
review={review}
onSelect={() => onClick(review.id, true)}
setReviewed={handleSetReviewed}
markAboveReviewed={markAboveReviewed}
/>
</ContextMenu>
)}
<PreviewPlaceholder imgLoaded={imgLoaded} />
<div className={`${imgLoaded ? "visible" : "invisible"}`}>
<img
ref={imgRef}
className={`w-full h-full transition-opacity ${
playingBack ? "opacity-0" : "opacity-100"
}`}
src={`${apiHost}${review.thumb_path.replace("/media/frigate/", "")}`}
loading={isSafari ? "eager" : "lazy"}
onLoad={() => {
onImgLoad();
}}
/>

{!playingBack && (
<>
<div className="absolute top-0 inset-x-0 rounded-t-l z-10 w-full h-[30%] bg-gradient-to-b from-black/60 to-transparent pointer-events-none">
<div className="flex h-full justify-between items-start mx-3 pb-1 text-white text-sm ">
{(review.severity == "alert" ||
review.severity == "detection") && (
<Chip className="absolute top-2 left-2 flex gap-1 bg-gradient-to-br from-gray-400 to-gray-500 bg-gray-500 z-0">
{review.data.objects.map((object) => {
return getIconForLabel(object, "size-3 text-white");
})}
{review.data.audio.map((audio) => {
return getIconForLabel(audio, "size-3 text-white");
})}
{review.data.sub_labels?.map((sub) => {
return getIconForSubLabel(sub, "size-3 text-white");
})}
</Chip>
)}
</div>
</div>
<div className="absolute bottom-0 inset-x-0 rounded-b-l z-10 w-full h-[20%] bg-gradient-to-t from-black/60 to-transparent pointer-events-none">
<div className="flex h-full justify-between items-end mx-3 pb-1 text-white text-sm ">
<TimeAgo time={review.start_time * 1000} dense />
{formattedDate}
</div>
</div>
</>
)}
</div>
{!playingBack && imgLoaded && review.has_been_reviewed && (
<div className="absolute inset-0 z-10 bg-black bg-opacity-60" />
)}
</div>
);
}

Expand Down Expand Up @@ -613,71 +592,6 @@ function InProgressPreview({
);
}

type PreviewContextItemsProps = {
review: ReviewSegment;
onSelect: () => void;
setReviewed: () => void;
markAboveReviewed: () => void;
};
function PreviewContextItems({
review,
onSelect,
setReviewed,
markAboveReviewed,
}: PreviewContextItemsProps) {
const exportReview = useCallback(() => {
axios.post(
`export/${review.camera}/start/${review.start_time}/end/${review.end_time}`,
{ playback: "realtime" },
);
}, [review]);

const deleteReview = useCallback(() => {
axios.delete(`reviews/${review.id}`);
}, [review]);

return (
<ContextMenuContent>
{isMobile && (
<ContextMenuItem onSelect={onSelect}>
<div className="w-full flex justify-between items-center">
Select
<RiCheckboxMultipleLine className="ml-4 size-4" />
</div>
</ContextMenuItem>
)}
<ContextMenuItem onSelect={markAboveReviewed}>
<div className="w-full flex justify-between items-center">
Mark Above as Reviewed
<LuCheckCheck className="ml-4 size-4" />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
{!review.has_been_reviewed && (
<ContextMenuItem onSelect={() => (setReviewed ? setReviewed() : null)}>
<div className="w-full flex justify-between items-center">
Mark As Reviewed
<LuCheckSquare className="ml-4 size-4" />
</div>
</ContextMenuItem>
)}
<ContextMenuItem onSelect={exportReview}>
<div className="w-full flex justify-between items-center">
Export
<LuFileUp className="ml-4 size-4" />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onSelect={deleteReview}>
<div className="w-full flex justify-between items-center text-danger">
Delete
<LuTrash className="ml-4 size-4" />
</div>
</ContextMenuItem>
</ContextMenuContent>
);
}

function PreviewPlaceholder({ imgLoaded }: { imgLoaded: boolean }) {
if (imgLoaded) {
return;
Expand Down
Loading
Loading