Files
frigate/web/src/components/navigation/Bottombar.tsx
T

213 lines
5.9 KiB
TypeScript
Raw Normal View History

2024-02-27 14:39:05 -07:00
import NavItem from "./NavItem";
import { IoIosWarning } from "react-icons/io";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import useSWR from "swr";
import { FrigateStats } from "@/types/stats";
2024-10-22 09:01:01 -05:00
import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws";
2026-06-04 10:56:11 -05:00
import {
useContext,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import useStats from "@/hooks/use-stats";
import GeneralSettings from "../menu/GeneralSettings";
2024-04-11 14:54:09 -06:00
import useNavigation from "@/hooks/use-navigation";
2024-04-22 09:20:23 -05:00
import {
StatusBarMessagesContext,
StatusMessage,
} from "@/context/statusbar-provider";
import { Link } from "react-router-dom";
import { cn } from "@/lib/utils";
2026-06-04 10:56:11 -05:00
import { isMobile } from "react-device-detect";
2024-05-16 17:00:14 -05:00
import { isPWA } from "@/utils/isPWA";
2025-04-19 22:02:15 +08:00
import { useTranslation } from "react-i18next";
2024-02-27 14:39:05 -07:00
function Bottombar() {
2024-04-11 14:54:09 -06:00
const navItems = useNavigation("secondary");
2026-06-04 10:56:11 -05:00
// Render 48px touch targets when they fit with even spacing, otherwise fall
// back to the compact size. Measured against the live bar width and icon
// count (which varies with enabled nav items and the status alert).
const containerRef = useRef<HTMLDivElement | null>(null);
const [large, setLarge] = useState(false);
useLayoutEffect(() => {
const el = containerRef.current;
if (!el) {
return;
}
const TARGET = 48; // standard bottom-nav touch target (px)
const MIN_GAP = 8; // minimum spacing between targets (px)
const compute = () => {
const count = el.children.length;
if (count === 0) {
return;
}
const needed = count * TARGET + Math.max(count - 1, 0) * MIN_GAP;
setLarge(needed <= el.clientWidth);
};
compute();
const resize = new ResizeObserver(compute);
resize.observe(el);
// recompute when items are added/removed (e.g. the status alert appears)
const mutation = new MutationObserver(compute);
mutation.observe(el, { childList: true });
return () => {
resize.disconnect();
mutation.disconnect();
};
}, [navItems]);
2024-02-27 14:39:05 -07:00
return (
2024-05-16 11:51:57 -05:00
<div
2026-06-04 10:56:11 -05:00
ref={containerRef}
2024-05-16 11:51:57 -05:00
className={cn(
2026-06-04 10:56:11 -05:00
"absolute inset-x-4 bottom-0 flex h-16 flex-row items-center justify-between",
isMobile &&
(isPWA
? "h-[calc(3rem+env(safe-area-inset-bottom))] pb-[env(safe-area-inset-bottom)] md:h-[calc(4rem+env(safe-area-inset-bottom))]"
: "h-12 md:h-16 md:pb-2"),
2024-05-16 11:51:57 -05:00
)}
>
2024-04-11 14:54:09 -06:00
{navItems.map((item) => (
2026-06-04 10:56:11 -05:00
<NavItem
key={item.id}
large={large}
className="p-2"
item={item}
Icon={item.icon}
/>
2024-02-28 15:23:56 -07:00
))}
2026-06-04 10:56:11 -05:00
<GeneralSettings large={large} className="p-2" />
<StatusAlertNav large={large} className="p-2" />
2024-02-28 15:23:56 -07:00
</div>
2024-02-27 14:39:05 -07:00
);
}
2024-04-29 09:59:27 -06:00
type StatusAlertNavProps = {
className?: string;
2026-06-04 10:56:11 -05:00
large?: boolean;
2024-04-29 09:59:27 -06:00
};
2026-06-04 10:56:11 -05:00
function StatusAlertNav({ className, large }: StatusAlertNavProps) {
2025-04-19 22:02:15 +08:00
const { t } = useTranslation(["views/system"]);
const { data: initialStats } = useSWR<FrigateStats>("stats", {
revalidateOnFocus: false,
});
2024-07-11 09:25:33 -06:00
const latestStats = useFrigateStats();
2024-04-22 09:20:23 -05:00
const { messages, addMessage, clearMessages } = useContext(
StatusBarMessagesContext,
)!;
const stats = useMemo(() => {
if (latestStats) {
return latestStats;
}
return initialStats;
}, [initialStats, latestStats]);
const { potentialProblems } = useStats(stats);
2024-04-22 09:20:23 -05:00
useEffect(() => {
clearMessages("stats");
potentialProblems.forEach((problem) => {
addMessage(
"stats",
problem.text,
problem.color,
undefined,
problem.relevantLink,
);
2024-04-22 09:20:23 -05:00
});
}, [potentialProblems, addMessage, clearMessages]);
2024-10-22 09:01:01 -05:00
const { payload: reindexState } = useEmbeddingsReindexProgress();
useEffect(() => {
if (reindexState) {
2026-09-19 09:10:36 -05:00
clearMessages("embeddings-reindex");
if (reindexState.status === "indexing") {
2024-10-22 09:01:01 -05:00
addMessage(
"embeddings-reindex",
2025-04-19 22:02:15 +08:00
t("stats.reindexingEmbeddings", {
processed: Math.floor(
(reindexState.processed_objects / reindexState.total_objects) *
100,
),
}),
2024-10-22 09:01:01 -05:00
);
2026-09-19 09:10:36 -05:00
} else if (reindexState.status === "failed") {
addMessage("embeddings-reindex", t("stats.reindexEmbeddingsFailed"));
2024-10-22 09:01:01 -05:00
}
}
2025-04-19 22:02:15 +08:00
}, [reindexState, addMessage, clearMessages, t]);
2024-10-22 09:01:01 -05:00
2024-04-22 09:20:23 -05:00
if (!messages || Object.keys(messages).length === 0) {
return;
}
return (
<Drawer>
2024-05-16 11:51:57 -05:00
<DrawerTrigger asChild>
2026-06-04 10:56:11 -05:00
<div
className={cn(
"flex flex-col items-center justify-center p-2",
large && "size-12",
)}
>
<IoIosWarning
className={cn(
"text-danger md:m-[6px]",
large ? "size-6" : "size-5",
)}
/>
2024-05-16 11:51:57 -05:00
</div>
</DrawerTrigger>
2024-04-29 09:59:27 -06:00
<DrawerContent
className={cn(
2026-08-08 08:20:09 -05:00
"mx-1 max-h-[75dvh] overflow-hidden rounded-t-2xl",
className,
)}
2024-04-29 09:59:27 -06:00
>
2026-08-08 08:20:09 -05:00
<div className="scrollbar-container flex h-auto w-full flex-col items-center gap-2 overflow-y-auto overflow-x-hidden px-2 py-4">
2024-04-22 09:20:23 -05:00
{Object.entries(messages).map(([key, messageArray]) => (
2024-05-14 10:06:44 -05:00
<div key={key} className="flex w-full items-center gap-2">
{messageArray.map(({ id, text, color, link }: StatusMessage) => {
const message = (
2024-05-14 10:06:44 -05:00
<div key={id} className="flex items-center gap-2 text-xs">
<IoIosWarning
className={`size-5 ${color || "text-danger"}`}
/>
{text}
</div>
);
if (link) {
2024-05-16 11:51:57 -05:00
return (
<Link key={id} to={link}>
{message}
</Link>
);
} else {
return message;
}
})}
</div>
))}
</div>
</DrawerContent>
</Drawer>
);
}
2024-02-27 14:39:05 -07:00
export default Bottombar;