Use drawer instead of dropdown menu for mobile settings (#10761)

* Separate settings items so layout is more consistent

* Convert settings on mobile to drawer

* Fix sizing on mobile and make scrollable

* remove padding

* Use dialog instead of popover

* Don't focus on first item

* Simpler tab fix
This commit is contained in:
Nicolas Mowen 2024-04-01 09:31:31 -06:00 committed by GitHub
parent 7fac91dce4
commit 52f65a4dc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 453 additions and 317 deletions

View File

@ -25,7 +25,7 @@ function App() {
<Providers>
<BrowserRouter>
<Wrapper>
<div className="size-full pt-2 overflow-hidden">
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
{isDesktop && <Statusbar />}
{isMobile && <Bottombar />}

View File

@ -1,6 +1,5 @@
import { navbarLinks } from "@/pages/site-navigation";
import NavItem from "./NavItem";
import SettingsNavItems from "../settings/SettingsNavItems";
import { IoIosWarning } from "react-icons/io";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import useSWR from "swr";
@ -8,6 +7,8 @@ import { FrigateStats } from "@/types/stats";
import { useFrigateStats } from "@/api/ws";
import { useMemo } from "react";
import useStats from "@/hooks/use-stats";
import GeneralSettings from "../settings/GeneralSettings";
import AccountSettings from "../settings/AccountSettings";
function Bottombar() {
return (
@ -23,7 +24,8 @@ function Bottombar() {
dev={item.dev}
/>
))}
<SettingsNavItems className="flex flex-shrink-0 justify-between gap-4" />
<GeneralSettings />
<AccountSettings />
<StatusAlertNav />
</div>
);

View File

@ -1,9 +1,10 @@
import Logo from "../Logo";
import { navbarLinks } from "@/pages/site-navigation";
import SettingsNavItems from "../settings/SettingsNavItems";
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";
function Sidebar() {
const location = useLocation();
@ -31,7 +32,10 @@ function Sidebar() {
);
})}
</div>
<SettingsNavItems className="hidden md:flex flex-col items-center mb-8" />
<div className="flex flex-col items-center mb-8">
<GeneralSettings />
<AccountSettings />
</div>
</aside>
);
}

View File

@ -0,0 +1,22 @@
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { VscAccount } from "react-icons/vsc";
import { Button } from "../ui/button";
export default function AccountSettings() {
return (
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<VscAccount />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Account</p>
</TooltipContent>
</Tooltip>
);
}

View File

@ -0,0 +1,420 @@
import {
LuActivity,
LuGithub,
LuHardDrive,
LuLifeBuoy,
LuList,
LuMoon,
LuPenSquare,
LuRotateCw,
LuSettings,
LuSun,
LuSunMoon,
} from "react-icons/lu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import { Link } from "react-router-dom";
import { CgDarkMode } from "react-icons/cg";
import {
colorSchemes,
friendlyColorSchemeName,
useTheme,
} from "@/context/theme-provider";
import { IoColorPalette } from "react-icons/io5";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { useEffect, useState } from "react";
import { useRestart } from "@/api/ws";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../ui/sheet";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import ActivityIndicator from "../indicators/activity-indicator";
import { isDesktop } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import {
Dialog,
DialogClose,
DialogContent,
DialogPortal,
DialogTrigger,
} from "../ui/dialog";
type GeneralSettings = {
className?: string;
};
export default function GeneralSettings({ className }: GeneralSettings) {
const { theme, colorScheme, setTheme, setColorScheme } = useTheme();
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
const { send: sendRestart } = useRestart();
useEffect(() => {
let countdownInterval: NodeJS.Timeout;
if (restartingSheetOpen) {
countdownInterval = setInterval(() => {
setCountdown((prevCountdown) => prevCountdown - 1);
}, 1000);
}
return () => {
clearInterval(countdownInterval);
};
}, [restartingSheetOpen]);
useEffect(() => {
if (countdown === 0) {
window.location.href = "/";
}
}, [countdown]);
const handleForceReload = () => {
window.location.href = "/";
};
const Container = isDesktop ? DropdownMenu : Drawer;
const Trigger = isDesktop ? DropdownMenuTrigger : DrawerTrigger;
const Content = isDesktop ? DropdownMenuContent : DrawerContent;
const MenuItem = isDesktop ? DropdownMenuItem : DialogClose;
const SubItem = isDesktop ? DropdownMenuSub : Dialog;
const SubItemTrigger = isDesktop ? DropdownMenuSubTrigger : DialogTrigger;
const SubItemContent = isDesktop ? DropdownMenuSubContent : DialogContent;
const Portal = isDesktop ? DropdownMenuPortal : DialogPortal;
return (
<>
<div className={className}>
<Container>
<Trigger asChild>
<a href="#">
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<LuSettings />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Settings</p>
</TooltipContent>
</Tooltip>
</a>
</Trigger>
<Content
className={
isDesktop ? "w-72 mr-5" : "max-h-[75dvh] p-2 overflow-hidden"
}
>
<div className="w-full flex-col overflow-y-auto overflow-x-hidden">
<DropdownMenuLabel>System</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup className={isDesktop ? "" : "flex flex-col"}>
<Link to="/storage">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuHardDrive className="mr-2 size-4" />
<span>Storage</span>
</MenuItem>
</Link>
<Link to="/system">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuActivity className="mr-2 size-4" />
<span>System metrics</span>
</MenuItem>
</Link>
<Link to="/logs">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuList className="mr-2 size-4" />
<span>System logs</span>
</MenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
Configuration
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<Link to="/settings">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuSettings className="mr-2 size-4" />
<span>Settings</span>
</MenuItem>
</Link>
<Link to="/config">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuPenSquare className="mr-2 size-4" />
<span>Configuration editor</span>
</MenuItem>
</Link>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
Appearance
</DropdownMenuLabel>
<DropdownMenuSeparator />
<SubItem>
<SubItemTrigger
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuSunMoon className="mr-2 size-4" />
<span>Dark Mode</span>
</SubItemTrigger>
<Portal>
<span tabIndex={0} className="sr-only" />
<SubItemContent
className={isDesktop ? "" : "w-[92%] rounded-2xl"}
>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
onClick={() => setTheme("light")}
>
{theme === "light" ? (
<>
<LuSun className="mr-2 size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
Light
</>
) : (
<span className="mr-2 ml-6">Light</span>
)}
</MenuItem>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
onClick={() => setTheme("dark")}
>
{theme === "dark" ? (
<>
<LuMoon className="mr-2 size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
Dark
</>
) : (
<span className="mr-2 ml-6">Dark</span>
)}
</MenuItem>
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
onClick={() => setTheme("system")}
>
{theme === "system" ? (
<>
<CgDarkMode className="mr-2 size-4 scale-100 transition-all" />
System
</>
) : (
<span className="mr-2 ml-6">System</span>
)}
</MenuItem>
</SubItemContent>
</Portal>
</SubItem>
<SubItem>
<SubItemTrigger
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuSunMoon className="mr-2 size-4" />
<span>Theme</span>
</SubItemTrigger>
<Portal>
<SubItemContent
className={isDesktop ? "" : "w-[92%] rounded-2xl"}
>
<span tabIndex={0} className="sr-only" />
{colorSchemes.map((scheme) => (
<MenuItem
key={scheme}
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
onClick={() => setColorScheme(scheme)}
>
{scheme === colorScheme ? (
<>
<IoColorPalette className="mr-2 size-4 rotate-0 scale-100 transition-all" />
{friendlyColorSchemeName(scheme)}
</>
) : (
<span className="mr-2 ml-6">
{friendlyColorSchemeName(scheme)}
</span>
)}
</MenuItem>
))}
</SubItemContent>
</Portal>
</SubItem>
</DropdownMenuGroup>
<DropdownMenuLabel className={isDesktop ? "mt-3" : "mt-1"}>
Help
</DropdownMenuLabel>
<DropdownMenuSeparator />
<a href="https://docs.frigate.video">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuLifeBuoy className="mr-2 size-4" />
<span>Documentation</span>
</MenuItem>
</a>
<a href="https://github.com/blakeblackshear/frigate">
<MenuItem
className={
isDesktop
? "cursor-pointer"
: "p-2 flex items-center text-sm"
}
>
<LuGithub className="mr-2 size-4" />
<span>GitHub</span>
</MenuItem>
</a>
<DropdownMenuSeparator className={isDesktop ? "mt-3" : "mt-1"} />
<MenuItem
className={
isDesktop ? "cursor-pointer" : "p-2 flex items-center text-sm"
}
onClick={() => setRestartDialogOpen(true)}
>
<LuRotateCw className="mr-2 size-4" />
<span>Restart Frigate</span>
</MenuItem>
</div>
</Content>
</Container>
</div>
{restartDialogOpen && (
<AlertDialog
open={restartDialogOpen}
onOpenChange={() => setRestartDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to restart Frigate?
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setRestartingSheetOpen(true);
sendRestart("restart");
}}
>
Restart
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{restartingSheetOpen && (
<>
<Sheet
open={restartingSheetOpen}
onOpenChange={() => setRestartingSheetOpen(false)}
>
<SheetContent
side="top"
onInteractOutside={(e) => e.preventDefault()}
>
<div className="flex flex-col items-center">
<ActivityIndicator />
<SheetHeader className="mt-5 text-center">
<SheetTitle className="text-center">
Frigate is Restarting
</SheetTitle>
<SheetDescription className="text-center">
<p>This page will reload in {countdown} seconds.</p>
</SheetDescription>
</SheetHeader>
<Button size="lg" className="mt-5" onClick={handleForceReload}>
Force Reload Now
</Button>
</div>
</SheetContent>
</Sheet>
</>
)}
</>
);
}

View File

@ -1,312 +0,0 @@
import {
LuActivity,
LuGithub,
LuHardDrive,
LuLifeBuoy,
LuList,
LuMoon,
LuPenSquare,
LuRotateCw,
LuSettings,
LuSun,
LuSunMoon,
} from "react-icons/lu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Button } from "../ui/button";
import { Link } from "react-router-dom";
import { CgDarkMode } from "react-icons/cg";
import { VscAccount } from "react-icons/vsc";
import {
colorSchemes,
friendlyColorSchemeName,
useTheme,
} from "@/context/theme-provider";
import { IoColorPalette } from "react-icons/io5";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { useEffect, useState } from "react";
import { useRestart } from "@/api/ws";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "../ui/sheet";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import ActivityIndicator from "../indicators/activity-indicator";
type SettingsNavItemsProps = {
className?: string;
};
export default function SettingsNavItems({ className }: SettingsNavItemsProps) {
const { theme, colorScheme, setTheme, setColorScheme } = useTheme();
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
const [restartingSheetOpen, setRestartingSheetOpen] = useState(false);
const [countdown, setCountdown] = useState(60);
const { send: sendRestart } = useRestart();
useEffect(() => {
let countdownInterval: NodeJS.Timeout;
if (restartingSheetOpen) {
countdownInterval = setInterval(() => {
setCountdown((prevCountdown) => prevCountdown - 1);
}, 1000);
}
return () => {
clearInterval(countdownInterval);
};
}, [restartingSheetOpen]);
useEffect(() => {
if (countdown === 0) {
window.location.href = "/";
}
}, [countdown]);
const handleForceReload = () => {
window.location.href = "/";
};
return (
<>
<div className={className}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<a href="#">
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<LuSettings />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Settings</p>
</TooltipContent>
</Tooltip>
</a>
</DropdownMenuTrigger>
<DropdownMenuContent className="md:w-72 mr-5">
<DropdownMenuLabel>System</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<Link to="/storage">
<DropdownMenuItem>
<LuHardDrive className="mr-2 h-4 w-4" />
<span>Storage</span>
</DropdownMenuItem>
</Link>
<Link to="/system">
<DropdownMenuItem>
<LuActivity className="mr-2 h-4 w-4" />
<span>System metrics</span>
</DropdownMenuItem>
</Link>
<Link to="/logs">
<DropdownMenuItem>
<LuList className="mr-2 h-4 w-4" />
<span>System logs</span>
</DropdownMenuItem>
</Link>
</DropdownMenuGroup>
<DropdownMenuLabel className="mt-3">
Configuration
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<Link to="/settings">
<DropdownMenuItem>
<LuSettings className="mr-2 h-4 w-4" />
<span>Settings</span>
</DropdownMenuItem>
</Link>
<Link to="/config">
<DropdownMenuItem>
<LuPenSquare className="mr-2 h-4 w-4" />
<span>Configuration editor</span>
</DropdownMenuItem>
</Link>
<DropdownMenuLabel className="mt-3">Appearance</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<LuSunMoon className="mr-2 h-4 w-4" />
<span>Dark Mode</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => setTheme("light")}>
{theme === "light" ? (
<>
<LuSun className="mr-2 h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
Light
</>
) : (
<span className="mr-2 ml-6">Light</span>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
{theme === "dark" ? (
<>
<LuMoon className="mr-2 h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
Dark
</>
) : (
<span className="mr-2 ml-6">Dark</span>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
{theme === "system" ? (
<>
<CgDarkMode className="mr-2 h-4 w-4 scale-100 transition-all" />
System
</>
) : (
<span className="mr-2 ml-6">System</span>
)}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<LuSunMoon className="mr-2 h-4 w-4" />
<span>Theme</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{colorSchemes.map((scheme) => (
<DropdownMenuItem
key={scheme}
onClick={() => setColorScheme(scheme)}
>
{scheme === colorScheme ? (
<>
<IoColorPalette className="mr-2 h-4 w-4 rotate-0 scale-100 transition-all" />
{friendlyColorSchemeName(scheme)}
</>
) : (
<span className="mr-2 ml-6">
{friendlyColorSchemeName(scheme)}
</span>
)}
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuLabel className="mt-3">Help</DropdownMenuLabel>
<DropdownMenuSeparator />
<a href="https://docs.frigate.video">
<DropdownMenuItem>
<LuLifeBuoy className="mr-2 h-4 w-4" />
<span>Documentation</span>
</DropdownMenuItem>
</a>
<a href="https://github.com/blakeblackshear/frigate">
<DropdownMenuItem>
<LuGithub className="mr-2 h-4 w-4" />
<span>GitHub</span>
</DropdownMenuItem>
</a>
<DropdownMenuSeparator className="mt-3" />
<DropdownMenuItem onClick={() => setRestartDialogOpen(true)}>
<LuRotateCw className="mr-2 h-4 w-4" />
<span>Restart Frigate</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost">
<VscAccount />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Account</p>
</TooltipContent>
</Tooltip>
</div>
{restartDialogOpen && (
<AlertDialog
open={restartDialogOpen}
onOpenChange={() => setRestartDialogOpen(false)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure you want to restart Frigate?
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setRestartingSheetOpen(true);
sendRestart("restart");
}}
>
Restart
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
{restartingSheetOpen && (
<>
<Sheet
open={restartingSheetOpen}
onOpenChange={() => setRestartingSheetOpen(false)}
>
<SheetContent
side="top"
onInteractOutside={(e) => e.preventDefault()}
>
<div className="flex flex-col items-center">
<ActivityIndicator />
<SheetHeader className="mt-5 text-center">
<SheetTitle className="text-center">
Frigate is Restarting
</SheetTitle>
<SheetDescription className="text-center">
<p>This page will reload in {countdown} seconds.</p>
</SheetDescription>
</SheetHeader>
<Button size="lg" className="mt-5" onClick={handleForceReload}>
Force Reload Now
</Button>
</div>
</SheetContent>
</Sheet>
</>
)}
</>
);
}