mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
UI tweaks (#10946)
* 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:
parent
7a7ae81d50
commit
13cac082d5
@ -439,7 +439,7 @@ def motion_activity():
|
||||
# normalize data
|
||||
motion = (
|
||||
df["motion"]
|
||||
.resample(f"{scale}S")
|
||||
.resample(f"{scale}s")
|
||||
.apply(lambda x: max(x, key=abs, default=0.0))
|
||||
.fillna(0.0)
|
||||
.to_frame()
|
||||
|
@ -693,7 +693,9 @@ function ShowMotionOnlyButton({
|
||||
variant={motionOnlyButton ? "select" : "default"}
|
||||
onClick={() => setMotionOnlyButton(!motionOnlyButton)}
|
||||
>
|
||||
<FaRunning />
|
||||
<FaRunning
|
||||
className={`${motionOnlyButton ? "text-selected-foreground" : "text-secondary-foreground"}`}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { navbarLinks } from "@/pages/site-navigation";
|
||||
import NavItem from "./NavItem";
|
||||
import { IoIosWarning } from "react-icons/io";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
@ -9,20 +8,15 @@ import { useMemo } from "react";
|
||||
import useStats from "@/hooks/use-stats";
|
||||
import GeneralSettings from "../settings/GeneralSettings";
|
||||
import AccountSettings from "../settings/AccountSettings";
|
||||
import useNavigation from "@/hooks/use-navigation";
|
||||
|
||||
function Bottombar() {
|
||||
const navItems = useNavigation("secondary");
|
||||
|
||||
return (
|
||||
<div className="absolute h-16 inset-x-4 bottom-0 flex flex-row items-center justify-between">
|
||||
{navbarLinks.map((item) => (
|
||||
<NavItem
|
||||
className=""
|
||||
variant="secondary"
|
||||
key={item.id}
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
url={item.url}
|
||||
dev={item.dev}
|
||||
/>
|
||||
{navItems.map((item) => (
|
||||
<NavItem key={item.id} item={item} Icon={item.icon} />
|
||||
))}
|
||||
<GeneralSettings />
|
||||
<AccountSettings />
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { IconType } from "react-icons";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { ENV } from "@/env";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
@ -8,6 +6,8 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import { NavData } from "@/types/navigation";
|
||||
import { IconType } from "react-icons";
|
||||
|
||||
const variants = {
|
||||
primary: {
|
||||
@ -21,37 +21,29 @@ const variants = {
|
||||
};
|
||||
|
||||
type NavItemProps = {
|
||||
className: string;
|
||||
variant?: "primary" | "secondary";
|
||||
className?: string;
|
||||
item: NavData;
|
||||
Icon: IconType;
|
||||
title: string;
|
||||
url: string;
|
||||
dev?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export default function NavItem({
|
||||
className,
|
||||
variant = "primary",
|
||||
item,
|
||||
Icon,
|
||||
title,
|
||||
url,
|
||||
dev,
|
||||
onClick,
|
||||
}: NavItemProps) {
|
||||
const shouldRender = dev ? ENV !== "production" : true;
|
||||
|
||||
if (!shouldRender) {
|
||||
if (item.enabled == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<NavLink
|
||||
to={url}
|
||||
to={item.url}
|
||||
onClick={onClick}
|
||||
className={({ isActive }) =>
|
||||
`${className} flex flex-col justify-center items-center rounded-lg ${
|
||||
variants[variant][isActive ? "active" : "inactive"]
|
||||
`flex flex-col justify-center items-center rounded-lg ${className ?? ""} ${
|
||||
variants[item.variant ?? "primary"][isActive ? "active" : "inactive"]
|
||||
}`
|
||||
}
|
||||
>
|
||||
@ -65,7 +57,7 @@ export default function NavItem({
|
||||
<TooltipTrigger>{content}</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent side="right">
|
||||
<p>{title}</p>
|
||||
<p>{item.title}</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
|
@ -1,14 +1,16 @@
|
||||
import Logo from "../Logo";
|
||||
import { navbarLinks } from "@/pages/site-navigation";
|
||||
import NavItem from "./NavItem";
|
||||
import { CameraGroupSelector } from "../filter/CameraGroupSelector";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import GeneralSettings from "../settings/GeneralSettings";
|
||||
import AccountSettings from "../settings/AccountSettings";
|
||||
import useNavigation from "@/hooks/use-navigation";
|
||||
|
||||
function Sidebar() {
|
||||
const location = useLocation();
|
||||
|
||||
const navbarLinks = useNavigation();
|
||||
|
||||
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">
|
||||
<span tabIndex={0} className="sr-only" />
|
||||
@ -22,10 +24,8 @@ function Sidebar() {
|
||||
<div key={item.id}>
|
||||
<NavItem
|
||||
className={`mx-[10px] ${showCameraGroups ? "mb-2" : "mb-4"}`}
|
||||
item={item}
|
||||
Icon={item.icon}
|
||||
title={item.title}
|
||||
url={item.url}
|
||||
dev={item.dev}
|
||||
/>
|
||||
{showCameraGroups && <CameraGroupSelector className="mb-4" />}
|
||||
</div>
|
||||
|
59
web/src/hooks/use-navigation.ts
Normal file
59
web/src/hooks/use-navigation.ts
Normal 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],
|
||||
);
|
||||
}
|
@ -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,
|
||||
},
|
||||
];
|
10
web/src/types/navigation.ts
Normal file
10
web/src/types/navigation.ts
Normal 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;
|
||||
};
|
@ -295,6 +295,7 @@ export default function EventView({
|
||||
filter={filter}
|
||||
timeRange={timeRange}
|
||||
startTime={startTime}
|
||||
loading={severity != severityToggle}
|
||||
markItemAsReviewed={markItemAsReviewed}
|
||||
markAllItemsAsReviewed={markAllItemsAsReviewed}
|
||||
onSelectReview={onSelectReview}
|
||||
@ -334,6 +335,7 @@ type DetectionReviewProps = {
|
||||
filter?: ReviewFilter;
|
||||
timeRange: { before: number; after: number };
|
||||
startTime?: number;
|
||||
loading: boolean;
|
||||
markItemAsReviewed: (review: ReviewSegment) => void;
|
||||
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
||||
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
|
||||
@ -349,6 +351,7 @@ function DetectionReview({
|
||||
filter,
|
||||
timeRange,
|
||||
startTime,
|
||||
loading,
|
||||
markItemAsReviewed,
|
||||
markAllItemsAsReviewed,
|
||||
onSelectReview,
|
||||
@ -600,33 +603,41 @@ function DetectionReview({
|
||||
</div>
|
||||
<div className="w-[65px] md:w-[110px] flex flex-row">
|
||||
<div className="w-[55px] md:w-[100px] overflow-y-auto no-scrollbar">
|
||||
<EventReviewTimeline
|
||||
segmentDuration={segmentDuration}
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
showMinimap={showMinimap && !previewTime}
|
||||
minimapStartTime={minimapBounds.start}
|
||||
minimapEndTime={minimapBounds.end}
|
||||
showHandlebar={previewTime != undefined}
|
||||
handlebarTime={previewTime}
|
||||
visibleTimestamps={visibleTimestamps}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
contentRef={contentRef}
|
||||
timelineRef={reviewTimelineRef}
|
||||
dense={isMobile}
|
||||
/>
|
||||
{loading ? (
|
||||
<Skeleton className="size-full" />
|
||||
) : (
|
||||
<EventReviewTimeline
|
||||
segmentDuration={segmentDuration}
|
||||
timestampSpread={15}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
showMinimap={showMinimap && !previewTime}
|
||||
minimapStartTime={minimapBounds.start}
|
||||
minimapEndTime={minimapBounds.end}
|
||||
showHandlebar={previewTime != undefined}
|
||||
handlebarTime={previewTime}
|
||||
visibleTimestamps={visibleTimestamps}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
contentRef={contentRef}
|
||||
timelineRef={reviewTimelineRef}
|
||||
dense={isMobile}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-[10px]">
|
||||
<SummaryTimeline
|
||||
reviewTimelineRef={reviewTimelineRef}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
segmentDuration={segmentDuration}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
/>
|
||||
{loading ? (
|
||||
<Skeleton className="w-full" />
|
||||
) : (
|
||||
<SummaryTimeline
|
||||
reviewTimelineRef={reviewTimelineRef}
|
||||
timelineStart={timeRange.before}
|
||||
timelineEnd={timeRange.after}
|
||||
segmentDuration={segmentDuration}
|
||||
events={reviewItems?.all ?? []}
|
||||
severityType={severity}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -39,6 +39,7 @@ import MobileTimelineDrawer from "@/components/overlay/MobileTimelineDrawer";
|
||||
import MobileReviewSettingsDrawer from "@/components/overlay/MobileReviewSettingsDrawer";
|
||||
import Logo from "@/components/Logo";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { FaVideo } from "react-icons/fa";
|
||||
|
||||
const SEGMENT_DURATION = 30;
|
||||
|
||||
@ -251,14 +252,28 @@ export function RecordingView({
|
||||
{isMobile && (
|
||||
<Logo className="absolute inset-x-1/2 -translate-x-1/2 h-8" />
|
||||
)}
|
||||
<Button
|
||||
className="flex items-center gap-2 rounded-lg"
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
<div
|
||||
className={`flex items-center gap-2 ${isMobile ? "landscape:flex-col" : ""}`}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5" size="small" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
</Button>
|
||||
<Button
|
||||
className={`flex items-center gap-2.5 rounded-lg`}
|
||||
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">
|
||||
<MobileCameraDrawer
|
||||
allCameras={allCameras}
|
||||
|
@ -228,7 +228,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
|
||||
size="sm"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IoMdArrowRoundBack className="size-5" />
|
||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||
{isDesktop && <div className="text-primary">Back</div>}
|
||||
</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>}
|
||||
</Button>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user