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"; import { useEmbeddingsReindexProgress, useFrigateStats } from "@/api/ws"; import { useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, } from "react"; import useStats from "@/hooks/use-stats"; import GeneralSettings from "../menu/GeneralSettings"; import useNavigation from "@/hooks/use-navigation"; import { StatusBarMessagesContext, StatusMessage, } from "@/context/statusbar-provider"; import { Link } from "react-router-dom"; import { cn } from "@/lib/utils"; import { isMobile } from "react-device-detect"; import { isPWA } from "@/utils/isPWA"; import { useTranslation } from "react-i18next"; function Bottombar() { const navItems = useNavigation("secondary"); // 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(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]); return (
{navItems.map((item) => ( ))}
); } type StatusAlertNavProps = { className?: string; large?: boolean; }; function StatusAlertNav({ className, large }: StatusAlertNavProps) { const { t } = useTranslation(["views/system"]); const { data: initialStats } = useSWR("stats", { revalidateOnFocus: false, }); const latestStats = useFrigateStats(); const { messages, addMessage, clearMessages } = useContext( StatusBarMessagesContext, )!; const stats = useMemo(() => { if (latestStats) { return latestStats; } return initialStats; }, [initialStats, latestStats]); const { potentialProblems } = useStats(stats); useEffect(() => { clearMessages("stats"); potentialProblems.forEach((problem) => { addMessage( "stats", problem.text, problem.color, undefined, problem.relevantLink, ); }); }, [potentialProblems, addMessage, clearMessages]); const { payload: reindexState } = useEmbeddingsReindexProgress(); useEffect(() => { if (reindexState) { if (reindexState.status == "indexing") { clearMessages("embeddings-reindex"); addMessage( "embeddings-reindex", t("stats.reindexingEmbeddings", { processed: Math.floor( (reindexState.processed_objects / reindexState.total_objects) * 100, ), }), ); } if (reindexState.status === "completed") { clearMessages("embeddings-reindex"); } } }, [reindexState, addMessage, clearMessages, t]); if (!messages || Object.keys(messages).length === 0) { return; } return (
{Object.entries(messages).map(([key, messageArray]) => (
{messageArray.map(({ id, text, color, link }: StatusMessage) => { const message = (
{text}
); if (link) { return ( {message} ); } else { return message; } })}
))}
); } export default Bottombar;