* fix warning

* Improve event switching speed

* Fix icon colors

* Only show frigate+ page when frigate+ is enabled

* Add link from reecordings to live as well
This commit is contained in:
Nicolas Mowen 2024-04-11 14:54:09 -06:00 committed by GitHub
parent 7a7ae81d50
commit 13cac082d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 152 additions and 107 deletions

View File

@ -439,7 +439,7 @@ def motion_activity():
# normalize data # normalize data
motion = ( motion = (
df["motion"] df["motion"]
.resample(f"{scale}S") .resample(f"{scale}s")
.apply(lambda x: max(x, key=abs, default=0.0)) .apply(lambda x: max(x, key=abs, default=0.0))
.fillna(0.0) .fillna(0.0)
.to_frame() .to_frame()

View File

@ -693,7 +693,9 @@ function ShowMotionOnlyButton({
variant={motionOnlyButton ? "select" : "default"} variant={motionOnlyButton ? "select" : "default"}
onClick={() => setMotionOnlyButton(!motionOnlyButton)} onClick={() => setMotionOnlyButton(!motionOnlyButton)}
> >
<FaRunning /> <FaRunning
className={`${motionOnlyButton ? "text-selected-foreground" : "text-secondary-foreground"}`}
/>
</Button> </Button>
</div> </div>
</> </>

View File

@ -1,4 +1,3 @@
import { navbarLinks } from "@/pages/site-navigation";
import NavItem from "./NavItem"; import NavItem from "./NavItem";
import { IoIosWarning } from "react-icons/io"; import { IoIosWarning } from "react-icons/io";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
@ -9,20 +8,15 @@ import { useMemo } from "react";
import useStats from "@/hooks/use-stats"; import useStats from "@/hooks/use-stats";
import GeneralSettings from "../settings/GeneralSettings"; import GeneralSettings from "../settings/GeneralSettings";
import AccountSettings from "../settings/AccountSettings"; import AccountSettings from "../settings/AccountSettings";
import useNavigation from "@/hooks/use-navigation";
function Bottombar() { function Bottombar() {
const navItems = useNavigation("secondary");
return ( return (
<div className="absolute h-16 inset-x-4 bottom-0 flex flex-row items-center justify-between"> <div className="absolute h-16 inset-x-4 bottom-0 flex flex-row items-center justify-between">
{navbarLinks.map((item) => ( {navItems.map((item) => (
<NavItem <NavItem key={item.id} item={item} Icon={item.icon} />
className=""
variant="secondary"
key={item.id}
Icon={item.icon}
title={item.title}
url={item.url}
dev={item.dev}
/>
))} ))}
<GeneralSettings /> <GeneralSettings />
<AccountSettings /> <AccountSettings />

View File

@ -1,6 +1,4 @@
import { IconType } from "react-icons";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { ENV } from "@/env";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -8,6 +6,8 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { isDesktop } from "react-device-detect"; import { isDesktop } from "react-device-detect";
import { TooltipPortal } from "@radix-ui/react-tooltip"; import { TooltipPortal } from "@radix-ui/react-tooltip";
import { NavData } from "@/types/navigation";
import { IconType } from "react-icons";
const variants = { const variants = {
primary: { primary: {
@ -21,37 +21,29 @@ const variants = {
}; };
type NavItemProps = { type NavItemProps = {
className: string; className?: string;
variant?: "primary" | "secondary"; item: NavData;
Icon: IconType; Icon: IconType;
title: string;
url: string;
dev?: boolean;
onClick?: () => void; onClick?: () => void;
}; };
export default function NavItem({ export default function NavItem({
className, className,
variant = "primary", item,
Icon, Icon,
title,
url,
dev,
onClick, onClick,
}: NavItemProps) { }: NavItemProps) {
const shouldRender = dev ? ENV !== "production" : true; if (item.enabled == false) {
if (!shouldRender) {
return; return;
} }
const content = ( const content = (
<NavLink <NavLink
to={url} to={item.url}
onClick={onClick} onClick={onClick}
className={({ isActive }) => className={({ isActive }) =>
`${className} flex flex-col justify-center items-center rounded-lg ${ `flex flex-col justify-center items-center rounded-lg ${className ?? ""} ${
variants[variant][isActive ? "active" : "inactive"] variants[item.variant ?? "primary"][isActive ? "active" : "inactive"]
}` }`
} }
> >
@ -65,7 +57,7 @@ export default function NavItem({
<TooltipTrigger>{content}</TooltipTrigger> <TooltipTrigger>{content}</TooltipTrigger>
<TooltipPortal> <TooltipPortal>
<TooltipContent side="right"> <TooltipContent side="right">
<p>{title}</p> <p>{item.title}</p>
</TooltipContent> </TooltipContent>
</TooltipPortal> </TooltipPortal>
</Tooltip> </Tooltip>

View File

@ -1,14 +1,16 @@
import Logo from "../Logo"; import Logo from "../Logo";
import { navbarLinks } from "@/pages/site-navigation";
import NavItem from "./NavItem"; import NavItem from "./NavItem";
import { CameraGroupSelector } from "../filter/CameraGroupSelector"; import { CameraGroupSelector } from "../filter/CameraGroupSelector";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import GeneralSettings from "../settings/GeneralSettings"; import GeneralSettings from "../settings/GeneralSettings";
import AccountSettings from "../settings/AccountSettings"; import AccountSettings from "../settings/AccountSettings";
import useNavigation from "@/hooks/use-navigation";
function Sidebar() { function Sidebar() {
const location = useLocation(); const location = useLocation();
const navbarLinks = useNavigation();
return ( return (
<aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-background_alt border-r border-secondary-highlight"> <aside className="absolute w-[52px] z-10 left-o inset-y-0 overflow-y-auto scrollbar-hidden py-4 flex flex-col justify-between bg-background_alt border-r border-secondary-highlight">
<span tabIndex={0} className="sr-only" /> <span tabIndex={0} className="sr-only" />
@ -22,10 +24,8 @@ function Sidebar() {
<div key={item.id}> <div key={item.id}>
<NavItem <NavItem
className={`mx-[10px] ${showCameraGroups ? "mb-2" : "mb-4"}`} className={`mx-[10px] ${showCameraGroups ? "mb-2" : "mb-4"}`}
item={item}
Icon={item.icon} Icon={item.icon}
title={item.title}
url={item.url}
dev={item.dev}
/> />
{showCameraGroups && <CameraGroupSelector className="mb-4" />} {showCameraGroups && <CameraGroupSelector className="mb-4" />}
</div> </div>

View File

@ -0,0 +1,59 @@
import Logo from "@/components/Logo";
import { ENV } from "@/env";
import { FrigateConfig } from "@/types/frigateConfig";
import { NavData } from "@/types/navigation";
import { useMemo } from "react";
import { FaCompactDisc, FaVideo } from "react-icons/fa";
import { LuConstruction } from "react-icons/lu";
import { MdVideoLibrary } from "react-icons/md";
import useSWR from "swr";
export default function useNavigation(
variant: "primary" | "secondary" = "primary",
) {
const { data: config } = useSWR<FrigateConfig>("config");
return useMemo(
() =>
[
{
id: 1,
variant,
icon: FaVideo,
title: "Live",
url: "/",
},
{
id: 2,
variant,
icon: MdVideoLibrary,
title: "Review",
url: "/review",
},
{
id: 3,
variant,
icon: FaCompactDisc,
title: "Export",
url: "/export",
},
{
id: 5,
variant,
icon: Logo,
title: "Frigate+",
url: "/plus",
enabled: config?.plus?.enabled == true,
},
{
id: 4,
variant,
icon: LuConstruction,
title: "UI Playground",
url: "/playground",
enabled: ENV !== "production",
},
] as NavData[],
[config?.plus.enabled, variant],
);
}

View File

@ -1,38 +0,0 @@
import Logo from "@/components/Logo";
import { FaCompactDisc, FaVideo } from "react-icons/fa";
import { LuConstruction } from "react-icons/lu";
import { MdVideoLibrary } from "react-icons/md";
export const navbarLinks = [
{
id: 1,
icon: FaVideo,
title: "Live",
url: "/",
},
{
id: 2,
icon: MdVideoLibrary,
title: "Review",
url: "/review",
},
{
id: 3,
icon: FaCompactDisc,
title: "Export",
url: "/export",
},
{
id: 5,
icon: Logo,
title: "Frigate+",
url: "/plus",
},
{
id: 4,
icon: LuConstruction,
title: "UI Playground",
url: "/playground",
dev: true,
},
];

View File

@ -0,0 +1,10 @@
import { IconType } from "react-icons";
export type NavData = {
id: number;
variant?: "primary" | "secondary";
icon: IconType;
title: string;
url: string;
enabled?: boolean;
};

View File

@ -295,6 +295,7 @@ export default function EventView({
filter={filter} filter={filter}
timeRange={timeRange} timeRange={timeRange}
startTime={startTime} startTime={startTime}
loading={severity != severityToggle}
markItemAsReviewed={markItemAsReviewed} markItemAsReviewed={markItemAsReviewed}
markAllItemsAsReviewed={markAllItemsAsReviewed} markAllItemsAsReviewed={markAllItemsAsReviewed}
onSelectReview={onSelectReview} onSelectReview={onSelectReview}
@ -334,6 +335,7 @@ type DetectionReviewProps = {
filter?: ReviewFilter; filter?: ReviewFilter;
timeRange: { before: number; after: number }; timeRange: { before: number; after: number };
startTime?: number; startTime?: number;
loading: boolean;
markItemAsReviewed: (review: ReviewSegment) => void; markItemAsReviewed: (review: ReviewSegment) => void;
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void; markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void; onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
@ -349,6 +351,7 @@ function DetectionReview({
filter, filter,
timeRange, timeRange,
startTime, startTime,
loading,
markItemAsReviewed, markItemAsReviewed,
markAllItemsAsReviewed, markAllItemsAsReviewed,
onSelectReview, onSelectReview,
@ -600,33 +603,41 @@ function DetectionReview({
</div> </div>
<div className="w-[65px] md:w-[110px] flex flex-row"> <div className="w-[65px] md:w-[110px] flex flex-row">
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar"> <div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
<EventReviewTimeline {loading ? (
segmentDuration={segmentDuration} <Skeleton className="size-full" />
timestampSpread={15} ) : (
timelineStart={timeRange.before} <EventReviewTimeline
timelineEnd={timeRange.after} segmentDuration={segmentDuration}
showMinimap={showMinimap && !previewTime} timestampSpread={15}
minimapStartTime={minimapBounds.start} timelineStart={timeRange.before}
minimapEndTime={minimapBounds.end} timelineEnd={timeRange.after}
showHandlebar={previewTime != undefined} showMinimap={showMinimap && !previewTime}
handlebarTime={previewTime} minimapStartTime={minimapBounds.start}
visibleTimestamps={visibleTimestamps} minimapEndTime={minimapBounds.end}
events={reviewItems?.all ?? []} showHandlebar={previewTime != undefined}
severityType={severity} handlebarTime={previewTime}
contentRef={contentRef} visibleTimestamps={visibleTimestamps}
timelineRef={reviewTimelineRef} events={reviewItems?.all ?? []}
dense={isMobile} severityType={severity}
/> contentRef={contentRef}
timelineRef={reviewTimelineRef}
dense={isMobile}
/>
)}
</div> </div>
<div className="w-[10px]"> <div className="w-[10px]">
<SummaryTimeline {loading ? (
reviewTimelineRef={reviewTimelineRef} <Skeleton className="w-full" />
timelineStart={timeRange.before} ) : (
timelineEnd={timeRange.after} <SummaryTimeline
segmentDuration={segmentDuration} reviewTimelineRef={reviewTimelineRef}
events={reviewItems?.all ?? []} timelineStart={timeRange.before}
severityType={severity} timelineEnd={timeRange.after}
/> segmentDuration={segmentDuration}
events={reviewItems?.all ?? []}
severityType={severity}
/>
)}
</div> </div>
</div> </div>
</> </>

View File

@ -39,6 +39,7 @@ import MobileTimelineDrawer from "@/components/overlay/MobileTimelineDrawer";
import MobileReviewSettingsDrawer from "@/components/overlay/MobileReviewSettingsDrawer"; import MobileReviewSettingsDrawer from "@/components/overlay/MobileReviewSettingsDrawer";
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { FaVideo } from "react-icons/fa";
const SEGMENT_DURATION = 30; const SEGMENT_DURATION = 30;
@ -251,14 +252,28 @@ export function RecordingView({
{isMobile && ( {isMobile && (
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" /> <Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
)} )}
<Button <div
className="flex items-center gap-2 rounded-lg" className={`flex items-center gap-2 ${isMobile ? "landscape:flex-col" : ""}`}
size="sm"
onClick={() => navigate(-1)}
> >
<IoMdArrowRoundBack className="size-5" size="small" /> <Button
{isDesktop && <div className="text-primary">Back</div>} className={`flex items-center gap-2.5 rounded-lg`}
</Button> size="sm"
onClick={() => navigate(-1)}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>}
</Button>
<Button
className="flex items-center gap-2.5 rounded-lg"
size="sm"
onClick={() => {
navigate(`/#${mainCamera}`);
}}
>
<FaVideo className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Live</div>}
</Button>
</div>
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<MobileCameraDrawer <MobileCameraDrawer
allCameras={allCameras} allCameras={allCameras}

View File

@ -228,7 +228,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
size="sm" size="sm"
onClick={() => navigate(-1)} onClick={() => navigate(-1)}
> >
<IoMdArrowRoundBack className="size-5" /> <IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">Back</div>} {isDesktop && <div className="text-primary">Back</div>}
</Button> </Button>
<Button <Button
@ -247,7 +247,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
}); });
}} }}
> >
<LuHistory className="size-5" /> <LuHistory className="size-5 text-secondary-foreground" />
{isDesktop && <div className="text-primary">History</div>} {isDesktop && <div className="text-primary">History</div>}
</Button> </Button>
</div> </div>