diff --git a/web/src/components/mobile/MobileSettingsMenu.tsx b/web/src/components/mobile/MobileSettingsMenu.tsx new file mode 100644 index 000000000..159ee3473 --- /dev/null +++ b/web/src/components/mobile/MobileSettingsMenu.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import useOptimisticState from "@/hooks/use-optimistic-state"; +import { motion, AnimatePresence } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { LuChevronLeft, LuChevronRight } from "react-icons/lu"; +import Logo from "@/components/Logo"; +import { SettingsType } from "@/types/settings"; + +type SettingsGroup = { + label: string; + items: { key: string }[]; +}; + +type MobileSettingsMenuProps = { + settingsGroups: SettingsGroup[]; + visibleSettingsViews: readonly string[]; + onSelect: (key: string) => void; + isAdmin: boolean; + allowedViewsForViewer: readonly string[]; + setPageToggle: (page: SettingsType) => void; +}; + +export default function MobileSettingsMenu({ + settingsGroups, + visibleSettingsViews, + onSelect, + isAdmin, + allowedViewsForViewer, + setPageToggle, +}: MobileSettingsMenuProps) { + const { t } = useTranslation(["views/settings"]); + const [currentGroup, setCurrentGroup] = useState(null); + const [currentGroupToggle, setCurrentGroupToggle] = useOptimisticState( + currentGroup, + setCurrentGroup, + 100, + ); + + useEffect(() => { + const handlePopState = (event: PopStateEvent) => { + // only handle the pop state for our submenu nav + // mobile pages are handled separately + if (currentGroupToggle && !event.state?.submenu) { + event.preventDefault(); + setCurrentGroupToggle(null); + window.history.replaceState(null, "", window.location.pathname); + } + }; + + window.addEventListener("popstate", handlePopState); + return () => window.removeEventListener("popstate", handlePopState); + }, [currentGroupToggle, setCurrentGroupToggle]); + + const handleGroupClick = (group: SettingsGroup) => { + const filteredItems = group.items.filter((item) => + visibleSettingsViews.includes(item.key), + ); + if (filteredItems.length === 1) { + // Navigate directly + const key = filteredItems[0].key; + if (!isAdmin && !allowedViewsForViewer.includes(key)) { + setPageToggle("ui"); + } else { + setPageToggle(key as SettingsType); + } + onSelect(key); + } else { + // Show submenu + window.history.pushState({ submenu: true }, "", window.location.pathname); + setCurrentGroupToggle(group.label); + } + }; + + const handleItemClick = (key: string) => { + if (!isAdmin && !allowedViewsForViewer.includes(key)) { + setPageToggle("ui"); + } else { + setPageToggle(key as SettingsType); + } + onSelect(key); + }; + + return ( +
+
+
+ +
+
+ {currentGroupToggle ? ( + + ) : ( +

+ {t("menu.settings", { ns: "common" })} +

+ )} +
+
+ +
+ + {currentGroupToggle ? ( + + {(() => { + const group = settingsGroups.find( + (g) => g.label === currentGroupToggle, + ); + if (!group) return null; + const filteredItems = group.items.filter((item) => + visibleSettingsViews.includes(item.key), + ); + return filteredItems.map((item) => ( + + )); + })()} + + ) : ( + + {settingsGroups.map((group) => { + const filteredItems = group.items.filter((item) => + visibleSettingsViews.includes(item.key), + ); + if (filteredItems.length === 0) return null; + return ( + + ); + })} + + )} + +
+
+ ); +} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index aebbe1692..445885244 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -58,29 +58,14 @@ import { } from "@/components/ui/sidebar"; import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; -import { LuChevronRight } from "react-icons/lu"; -import Logo from "@/components/Logo"; import { MobilePage, MobilePageContent, MobilePageHeader, MobilePageTitle, } from "@/components/mobile/MobilePage"; - -const allSettingsViews = [ - "ui", - "enrichments", - "cameras", - "masksAndZones", - "motionTuner", - "triggers", - "debug", - "users", - "roles", - "notifications", - "frigateplus", -] as const; -type SettingsType = (typeof allSettingsViews)[number]; +import MobileSettingsMenu from "@/components/mobile/MobileSettingsMenu"; +import { allSettingsViews, SettingsType } from "@/types/settings"; const settingsGroups = [ { @@ -130,34 +115,6 @@ const getCurrentComponent = (page: SettingsType) => { return null; }; -function MobileMenuItem({ - item, - onSelect, - onClose, - className, -}: { - item: { key: string }; - onSelect: (key: string) => void; - onClose?: () => void; - className?: string; -}) { - const { t } = useTranslation(["views/settings"]); - - return ( - - ); -} - export default function Settings() { const { t } = useTranslation(["views/settings"]); const [page, setPage] = useState("ui"); @@ -282,61 +239,29 @@ export default function Settings() { } }, [t, contentMobileOpen]); + // mobile + if (isMobile) { return ( <> - {!contentMobileOpen && ( -
-
-
- -
-
-

- {t("menu.settings", { ns: "common" })} -

-
-
- -
- {settingsGroups.map((group) => { - const filteredItems = group.items.filter((item) => - visibleSettingsViews.includes(item.key as SettingsType), - ); - if (filteredItems.length === 0) return null; - return ( -
- {filteredItems.length > 1 && ( -

-
- {t("menu." + group.label)} -
-

- )} - {filteredItems.map((item) => ( - { - if ( - !isAdmin && - !allowedViewsForViewer.includes(key as SettingsType) - ) { - setPageToggle("ui"); - } else { - setPageToggle(key as SettingsType); - } - setContentMobileOpen(true); - }} - /> - ))} -
- ); - })} -
-
- )} + { + if ( + !isAdmin && + !allowedViewsForViewer.includes(key as SettingsType) + ) { + setPageToggle("ui"); + } else { + setPageToggle(key as SettingsType); + } + setContentMobileOpen(true); + }} + isAdmin={isAdmin} + allowedViewsForViewer={allowedViewsForViewer} + setPageToggle={setPageToggle} + />
diff --git a/web/src/types/settings.ts b/web/src/types/settings.ts new file mode 100644 index 000000000..2b7e2df88 --- /dev/null +++ b/web/src/types/settings.ts @@ -0,0 +1,15 @@ +export const allSettingsViews = [ + "ui", + "enrichments", + "cameras", + "masksAndZones", + "motionTuner", + "triggers", + "debug", + "users", + "roles", + "notifications", + "frigateplus", +] as const; + +export type SettingsType = (typeof allSettingsViews)[number];