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

UI Improvements and Tweaks #13689

Merged
merged 12 commits into from
Sep 12, 2024
1 change: 1 addition & 0 deletions web/src/components/camera/CameraImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export default function CameraImage({
"rounded-lg md:rounded-2xl",
)}
onLoad={handleImageLoad}
loading="lazy"
/>
) : (
<div className="pt-6 text-center">
Expand Down
16 changes: 15 additions & 1 deletion web/src/components/card/ExportCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LuTrash } from "react-icons/lu";
import { Button } from "../ui/button";
import { useCallback, useState } from "react";
import { isDesktop } from "react-device-detect";
import { FaDownload, FaPlay } from "react-icons/fa";
import { FaDownload, FaPlay, FaShareAlt } from "react-icons/fa";
import Chip from "../indicators/Chip";
import { Skeleton } from "../ui/skeleton";
import {
Expand All @@ -19,6 +19,7 @@ import { DeleteClipType, Export } from "@/types/export";
import { MdEditSquare } from "react-icons/md";
import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import { shareOrCopy } from "@/utils/browserUtil";

type ExportProps = {
className: string;
Expand Down Expand Up @@ -147,6 +148,19 @@ export default function ExportCard({
<div>
<div className="absolute inset-0 rounded-lg bg-black bg-opacity-60 md:rounded-2xl" />
<div className="absolute right-1 top-1 flex items-center gap-2">
{!exportedRecording.in_progress && (
<Chip
className="cursor-pointer rounded-md bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500"
onClick={() =>
shareOrCopy(
`${baseUrl}exports?id=${exportedRecording.id}`,
exportedRecording.name.replaceAll("_", " "),
)
}
>
<FaShareAlt className="size-4 text-white" />
</Chip>
)}
{!exportedRecording.in_progress && (
<a
download
Expand Down
2 changes: 2 additions & 0 deletions web/src/components/filter/ReviewFilterGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ export function GeneralFilterContent({
<div className="my-2.5 flex flex-col gap-2.5">
{allLabels.map((item) => (
<FilterSwitch
key={item}
label={item.replaceAll("_", " ")}
isChecked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
Expand Down Expand Up @@ -516,6 +517,7 @@ export function GeneralFilterContent({
<div className="my-2.5 flex flex-col gap-2.5">
{allZones.map((item) => (
<FilterSwitch
key={item}
label={item.replaceAll("_", " ")}
isChecked={currentZones?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
Expand Down
54 changes: 32 additions & 22 deletions web/src/components/overlay/detail/ObjectLifecycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { Button } from "@/components/ui/button";
import { ObjectLifecycleSequence } from "@/types/timeline";
import Heading from "@/components/ui/heading";
import { ReviewDetailPaneType, ReviewSegment } from "@/types/review";
import { ReviewDetailPaneType } from "@/types/review";
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { getIconForLabel } from "@/utils/iconUtil";
Expand Down Expand Up @@ -47,14 +47,16 @@ import { AnnotationSettingsPane } from "./AnnotationSettingsPane";
import { TooltipPortal } from "@radix-ui/react-tooltip";

type ObjectLifecycleProps = {
review: ReviewSegment;
className?: string;
event: Event;
fullscreen?: boolean;
setPane: React.Dispatch<React.SetStateAction<ReviewDetailPaneType>>;
};

export default function ObjectLifecycle({
review,
className,
event,
fullscreen = false,
setPane,
}: ObjectLifecycleProps) {
const { data: eventSequence } = useSWR<ObjectLifecycleSequence[]>([
Expand All @@ -78,13 +80,13 @@ export default function ObjectLifecycle({
const getZoneColor = useCallback(
(zoneName: string) => {
const zoneColor =
config?.cameras?.[review.camera]?.zones?.[zoneName]?.color;
config?.cameras?.[event.camera]?.zones?.[zoneName]?.color;
if (zoneColor) {
const reversed = [...zoneColor].reverse();
return reversed;
}
},
[config, review],
[config, event],
);

const getZonePolygon = useCallback(
Expand All @@ -93,7 +95,7 @@ export default function ObjectLifecycle({
return;
}
const zonePoints =
config?.cameras[review.camera].zones[zoneName].coordinates;
config?.cameras[event.camera].zones[zoneName].coordinates;
const imgElement = imgRef.current;
const imgRect = imgElement.getBoundingClientRect();

Expand All @@ -110,7 +112,7 @@ export default function ObjectLifecycle({
}, [] as number[])
.join(",");
},
[config, imgRef, review],
[config, imgRef, event],
);

const [boxStyle, setBoxStyle] = useState<React.CSSProperties | null>(null);
Expand Down Expand Up @@ -224,17 +226,19 @@ export default function ObjectLifecycle({
}

return (
<>
<div className={cn("flex items-center gap-2")}>
<Button
className="flex items-center gap-2.5 rounded-lg"
size="sm"
onClick={() => setPane("overview")}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>}
</Button>
</div>
<div className={className}>
{!fullscreen && (
<div className={cn("flex items-center gap-2")}>
<Button
className="flex items-center gap-2.5 rounded-lg"
size="sm"
onClick={() => setPane("overview")}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>}
</Button>
</div>
)}

<div className="relative mx-auto">
<ImageLoadingIndicator
Expand Down Expand Up @@ -347,7 +351,10 @@ export default function ObjectLifecycle({
)}

<div className="relative flex flex-col items-center justify-center">
<Carousel className="m-0 w-full" setApi={setMainApi}>
<Carousel
className={cn("m-0 w-full", fullscreen && isDesktop && "w-[75%]")}
setApi={setMainApi}
>
<CarouselContent>
{eventSequence.map((item, index) => (
<CarouselItem key={index}>
Expand Down Expand Up @@ -455,7 +462,7 @@ export default function ObjectLifecycle({
</CarouselContent>
</Carousel>
</div>
<div className="relative flex flex-col items-center justify-center">
<div className="relative mt-4 flex flex-col items-center justify-center">
<Carousel
opts={{
align: "center",
Expand All @@ -474,7 +481,10 @@ export default function ObjectLifecycle({
{eventSequence.map((item, index) => (
<CarouselItem
key={index}
className={cn("basis-1/4 cursor-pointer pl-1 md:basis-[10%]")}
className={cn(
"basis-1/4 cursor-pointer pl-1 md:basis-[10%]",
fullscreen && "md:basis-16",
)}
onClick={() => handleThumbnailClick(index)}
>
<div className="p-1">
Expand Down Expand Up @@ -513,7 +523,7 @@ export default function ObjectLifecycle({
<CarouselNext />
</Carousel>
</div>
</>
</div>
);
}

Expand Down
29 changes: 19 additions & 10 deletions web/src/components/overlay/detail/ReviewDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { cn } from "@/lib/utils";
import { FrigatePlusDialog } from "../dialog/FrigatePlusDialog";
import ObjectLifecycle from "./ObjectLifecycle";
import Chip from "@/components/indicators/Chip";
import { FaDownload, FaImages } from "react-icons/fa";
import { FaDownload, FaImages, FaShareAlt } from "react-icons/fa";
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";
import { FaArrowsRotate } from "react-icons/fa6";
import {
Expand All @@ -34,6 +34,9 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { baseUrl } from "@/api/baseUrl";
import { shareOrCopy } from "@/utils/browserUtil";

type ReviewDetailDialogProps = {
review?: ReviewSegment;
Expand Down Expand Up @@ -136,11 +139,21 @@ export default function ReviewDetailDialog({
<div className="text-sm text-primary/40">Timestamp</div>
<div className="text-sm">{formattedDate}</div>
</div>
<Button
className="flex max-w-24 gap-2"
variant="secondary"
size="sm"
onClick={() =>
shareOrCopy(`${baseUrl}review?id=${review.id}`)
}
>
<FaShareAlt className="size-4" />
</Button>
</div>
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-1.5">
<div className="flex w-full flex-col items-center gap-2">
<div className="flex w-full flex-col gap-1.5">
<div className="text-sm text-primary/40">Objects</div>
<div className="flex flex-col items-start gap-2 text-sm capitalize">
<div className="scrollbar-container flex max-h-32 flex-col items-start gap-2 overflow-y-scroll text-sm capitalize">
{events?.map((event) => {
return (
<div
Expand All @@ -159,7 +172,7 @@ export default function ReviewDetailDialog({
</div>
</div>
{review.data.zones.length > 0 && (
<div className="flex flex-col gap-1.5">
<div className="scrollbar-container flex max-h-32 w-full flex-col gap-1.5">
<div className="text-sm text-primary/40">Zones</div>
<div className="flex flex-col items-start gap-2 text-sm capitalize">
{review.data.zones.map((zone) => {
Expand Down Expand Up @@ -199,11 +212,7 @@ export default function ReviewDetailDialog({

{pane == "details" && selectedEvent && (
<div className="scrollbar-container overflow-x-none mt-0 flex size-full flex-col gap-2 overflow-y-auto overflow-x-hidden">
<ObjectLifecycle
review={review}
event={selectedEvent}
setPane={setPane}
/>
<ObjectLifecycle event={selectedEvent} setPane={setPane} />
</div>
)}
</Content>
Expand Down
52 changes: 42 additions & 10 deletions web/src/components/overlay/detail/SearchDetailDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,16 @@ import { baseUrl } from "@/api/baseUrl";
import { cn } from "@/lib/utils";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { ASPECT_VERTICAL_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
import { FaRegListAlt, FaVideo } from "react-icons/fa";
import FrigatePlusIcon from "@/components/icons/FrigatePlusIcon";

const SEARCH_TABS = ["details", "frigate+", "video"] as const;
import { FaImage, FaRegListAlt, FaVideo } from "react-icons/fa";
import { FaRotate } from "react-icons/fa6";
import ObjectLifecycle from "./ObjectLifecycle";

const SEARCH_TABS = [
"details",
"snapshot",
"video",
"object lifecycle",
] as const;
type SearchTab = (typeof SEARCH_TABS)[number];

type SearchDetailDialogProps = {
Expand Down Expand Up @@ -66,8 +72,8 @@ export default function SearchDetailDialog({

const views = [...SEARCH_TABS];

if (!config.plus.enabled || !search.has_snapshot) {
const index = views.indexOf("frigate+");
if (!search.has_snapshot) {
const index = views.indexOf("snapshot");
views.splice(index, 1);
}

Expand All @@ -80,6 +86,16 @@ export default function SearchDetailDialog({
return views;
}, [config, search]);

useEffect(() => {
if (searchTabs.length == 0) {
return;
}

if (!searchTabs.includes(pageToggle)) {
setPage("details");
}
}, [pageToggle, searchTabs]);

if (!search) {
return;
}
Expand All @@ -104,7 +120,7 @@ export default function SearchDetailDialog({
<Content
className={
isDesktop
? "sm:max-w-xl md:max-w-3xl lg:max-w-4xl xl:max-w-7xl"
? "sm:max-w-xl md:max-w-4xl lg:max-w-4xl xl:max-w-7xl"
: "max-h-[75dvh] overflow-hidden px-2 pb-4"
}
>
Expand Down Expand Up @@ -136,8 +152,11 @@ export default function SearchDetailDialog({
aria-label={`Select ${item}`}
>
{item == "details" && <FaRegListAlt className="size-4" />}
{item == "frigate+" && <FrigatePlusIcon className="size-4" />}
{item == "snapshot" && <FaImage className="size-4" />}
{item == "video" && <FaVideo className="size-4" />}
{item == "object lifecycle" && (
<FaRotate className="size-4" />
)}
<div className="capitalize">{item}</div>
</ToggleGroupItem>
))}
Expand All @@ -153,9 +172,14 @@ export default function SearchDetailDialog({
setSimilarity={setSimilarity}
/>
)}
{page == "frigate+" && (
{page == "snapshot" && (
<FrigatePlusDialog
upload={search as unknown as Event}
upload={
{
...search,
plus_id: config?.plus?.enabled ? search.plus_id : "not_enabled",
} as unknown as Event
}
dialog={false}
onClose={() => {}}
onEventUploaded={() => {
Expand All @@ -164,6 +188,14 @@ export default function SearchDetailDialog({
/>
)}
{page == "video" && <VideoTab search={search} config={config} />}
{page == "object lifecycle" && (
<ObjectLifecycle
className="w-full"
event={search as unknown as Event}
fullscreen={true}
setPane={() => {}}
/>
)}
</Content>
</Overlay>
);
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/overlay/dialog/FrigatePlusDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export function FrigatePlusDialog({

const content = (
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
<DialogHeader>
<DialogHeader className={state == "submitted" ? "sr-only" : ""}>
<DialogTitle>Submit To Frigate+</DialogTitle>
<DialogDescription>
Objects in locations you want to avoid are not false positives.
Expand Down
1 change: 1 addition & 0 deletions web/src/components/player/PreviewPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ function PreviewFramesPlayer({
<img
ref={imgRef}
className={`size-full rounded-lg bg-black object-contain md:rounded-2xl`}
loading="lazy"
onLoad={onImageLoaded}
/>
{previewFrames?.length === 0 && (
Expand Down
4 changes: 2 additions & 2 deletions web/src/hooks/use-api-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,11 @@ export function useApiFilterArgs<
const filter: { [key: string]: unknown } = {};

rawParams.forEach((value, key) => {
if (isNaN(parseFloat(value))) {
if (value != "true" && value != "false" && isNaN(parseFloat(value))) {
filter[key] = value.includes(",") ? value.split(",") : [value];
} else {
if (value != undefined) {
filter[key] = `${value}`;
filter[key] = JSON.parse(value);
}
}
});
Expand Down
Loading