From fff14f7dc61382804f7a27e333d2e8fbbef6d681 Mon Sep 17 00:00:00 2001 From: spacebares <57186372+spacebares@users.noreply.github.com> Date: Tue, 27 Jun 2023 17:27:49 -0400 Subject: [PATCH] support multiple viewmodes for UI options and menus --- frigate/config.py | 12 +++++++++--- web/src/AppBar.jsx | 26 +++++++++++++++++++------- web/src/Sidebar.jsx | 19 ++++++++++++------- web/src/app.tsx | 6 +++--- web/src/components/AppBar.jsx | 17 +---------------- web/src/components/Menu.jsx | 3 ++- web/src/components/ViewOption.jsx | 12 ++++++++++++ web/src/components/ViewOptionEnum.tsx | 5 +++++ web/src/context/index.jsx | 19 ++++++++++--------- web/src/routes/Camera.jsx | 8 +++++--- web/src/routes/Cameras.jsx | 7 ++++--- web/src/routes/Events.jsx | 16 ++++++++-------- web/src/routes/System.jsx | 8 +++----- 13 files changed, 93 insertions(+), 65 deletions(-) create mode 100644 web/src/components/ViewOption.jsx create mode 100644 web/src/components/ViewOptionEnum.tsx diff --git a/frigate/config.py b/frigate/config.py index 14b94a82d..c91e75da4 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -67,6 +67,12 @@ class DateTimeStyleEnum(str, Enum): short = "short" +class ViewModeEnum(str, Enum): + user = "user" + advanced = "advanced" + admin = "admin" + + class UIConfig(FrigateBaseModel): live_mode: LiveModeEnum = Field( default=LiveModeEnum.mse, title="Default Live Mode." @@ -85,9 +91,9 @@ class UIConfig(FrigateBaseModel): strftime_fmt: Optional[str] = Field( default=None, title="Override date and time format using strftime syntax." ) - show_advanced_options: bool = Field( - default=True, - title="Default setting to show Advanced Options, such as Config, Camera Rec/Motion/Snap buttons, go2rtc dashboard and more.", + viewmode: ViewModeEnum = Field( + default=ViewModeEnum.admin, + title="Default setting to display or hide certain options and menus in Frigate.", ) diff --git a/web/src/AppBar.jsx b/web/src/AppBar.jsx index fc57458cb..6c4b047ce 100644 --- a/web/src/AppBar.jsx +++ b/web/src/AppBar.jsx @@ -8,15 +8,16 @@ import DarkModeIcon from './icons/DarkMode'; import SettingsIcon from './icons/Settings'; import FrigateRestartIcon from './icons/FrigateRestart'; import Prompt from './components/Prompt'; -import { useDarkMode, useAdvOptions } from './context'; +import { useDarkMode, useViewMode } from './context'; import { useCallback, useRef, useState } from 'preact/hooks'; import { useRestart } from './api/ws'; +import { ViewModeTypes } from './components/ViewOptionEnum' export default function AppBar() { const [showMoreMenu, setShowMoreMenu] = useState(false); const [showDialog, setShowDialog] = useState(false); const [showDialogWait, setShowDialogWait] = useState(false); - const { showAdvOptions, setShowAdvOptions } = useAdvOptions(); + const { viewMode, setViewMode } = useViewMode(); const { setDarkMode } = useDarkMode(); const { send: sendRestart } = useRestart(); @@ -28,10 +29,13 @@ export default function AppBar() { [setDarkMode, setShowMoreMenu] ); - const handleToggleAdvOptions = useCallback(() => { - setShowAdvOptions(showAdvOptions === 1 ? 0 : 1); - setShowMoreMenu(false); - },[showAdvOptions, setShowAdvOptions, setShowMoreMenu]); + const handleSetViewMode = useCallback( + (value) => { + setViewMode(value); + setShowMoreMenu(false); + }, + [setViewMode, setShowMoreMenu] + ); const moreRef = useRef(null); @@ -68,7 +72,15 @@ export default function AppBar() { - + + + diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index 5dcd15615..c19c9a2f0 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -4,13 +4,12 @@ import { Match } from 'preact-router/match'; import { memo } from 'preact/compat'; import { ENV } from './env'; import { useMemo } from 'preact/hooks' -import { useAdvOptions } from './context'; +import ViewOption from './components/ViewOption' import useSWR from 'swr'; import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer'; export default function Sidebar() { const { data: config } = useSWR('config'); - const { showAdvOptions } = useAdvOptions(); const sortedCameras = useMemo(() => { if (!config) { @@ -48,11 +47,17 @@ export default function Sidebar() { - - - { showAdvOptions ? : null} - - + + + + + + + + + + +
{ENV !== 'production' ? ( diff --git a/web/src/app.tsx b/web/src/app.tsx index d21fa4471..dc4a85938 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -6,7 +6,7 @@ import AppBar from './AppBar'; import Cameras from './routes/Cameras'; import { Router } from 'preact-router'; import Sidebar from './Sidebar'; -import { DarkModeProvider, DrawerProvider, AdvOptionsProvider } from './context'; +import { DarkModeProvider, DrawerProvider, ViewModeProvider } from './context'; import useSWR from 'swr'; export default function App() { @@ -16,7 +16,7 @@ export default function App() { return ( - +
{!config ? (
@@ -48,7 +48,7 @@ export default function App() {
)}
-
+
); diff --git a/web/src/components/AppBar.jsx b/web/src/components/AppBar.jsx index a6870ef2b..78711a45d 100644 --- a/web/src/components/AppBar.jsx +++ b/web/src/components/AppBar.jsx @@ -2,8 +2,7 @@ import { h } from 'preact'; import Button from './Button'; import MenuIcon from '../icons/Menu'; import MoreIcon from '../icons/More'; -import { About } from '../icons/About'; -import { useDrawer, useAdvOptions } from '../context'; +import { useDrawer } from '../context'; import { useLayoutEffect, useCallback, useState } from 'preact/hooks'; // We would typically preserve these in component state @@ -14,7 +13,6 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) { const [show, setShow] = useState(true); const [atZero, setAtZero] = useState(window.scrollY === 0); const { setShowDrawer } = useDrawer(); - const { showAdvOptions } = useAdvOptions(); const scrollListener = useCallback(() => { const scrollY = window.scrollY; @@ -52,19 +50,6 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
<div className="flex-grow-1 flex justify-end w-full"> - { !showAdvOptions ? - <Button - className="rounded-full" - type="text" - color="gray" - aria-label="Advanced Options are hidden via the nearby menu." - > - <About className="w-6" /> - </Button> - : null - } - </div> - <div className="flex-grow-1 flex"> {overflowRef && onOverflowClick ? ( <div className="w-auto" ref={overflowRef}> <Button diff --git a/web/src/components/Menu.jsx b/web/src/components/Menu.jsx index 34ff20326..173c25612 100644 --- a/web/src/components/Menu.jsx +++ b/web/src/components/Menu.jsx @@ -16,7 +16,7 @@ export default function Menu({ className, children, onDismiss, relativeTo, width ) : null; } -export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...attrs }) { +export function MenuItem({ children, focus, icon: Icon, label, href, onSelect, value, ...attrs }) { const handleClick = useCallback(() => { onSelect && onSelect(value, label); }, [onSelect, value, label]); @@ -39,6 +39,7 @@ export function MenuItem({ focus, icon: Icon, label, href, onSelect, value, ...a </div> ) : null} <div className="whitespace-nowrap">{label}</div> + {children} </Element> ); } diff --git a/web/src/components/ViewOption.jsx b/web/src/components/ViewOption.jsx new file mode 100644 index 000000000..0d9132d0c --- /dev/null +++ b/web/src/components/ViewOption.jsx @@ -0,0 +1,12 @@ +import { h } from 'preact'; +import { useViewMode } from '../context'; +import { ViewModeTypes } from './ViewOptionEnum'; + +export default function ViewOption({children, requiredmode }) { + const { viewMode } = useViewMode(); + + return viewMode >= ViewModeTypes[requiredmode] ? ( + <>{children}</> + + ) : null; +} \ No newline at end of file diff --git a/web/src/components/ViewOptionEnum.tsx b/web/src/components/ViewOptionEnum.tsx new file mode 100644 index 000000000..5cb9c7a54 --- /dev/null +++ b/web/src/components/ViewOptionEnum.tsx @@ -0,0 +1,5 @@ +export enum ViewModeTypes { + "user", + "advanced", + "admin", + } \ No newline at end of file diff --git a/web/src/context/index.jsx b/web/src/context/index.jsx index 5bbcc5123..8e4ddcf1c 100644 --- a/web/src/context/index.jsx +++ b/web/src/context/index.jsx @@ -1,28 +1,29 @@ import { h, createContext } from 'preact'; import { get as getData, set as setData } from 'idb-keyval'; import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'preact/hooks'; +import { ViewModeTypes } from '../components/ViewOptionEnum'; import useSWR from 'swr'; -const AdvOptions = createContext(null); +const ViewMode = createContext(null); -export function AdvOptionsProvider({ children }) { - const [showAdvOptions, setShowAdvOptions] = usePersistence('show-advanced-options', null); +export function ViewModeProvider({ children }) { + const [viewMode, setViewMode] = usePersistence('view-mode', null); const { data: config } = useSWR('config'); useEffect(() => { async function load() { - const configValue = config.ui.show_advanced_options == true? 1 : 0; //fixes a load error - setShowAdvOptions(showAdvOptions || configValue); + const configValue = ViewModeTypes[config.ui.viewmode]; //fixes a load error + setViewMode(viewMode || configValue); } load(); - }, [setShowAdvOptions, config]); + }, [setViewMode, config]); - return <AdvOptions.Provider value={{ showAdvOptions, setShowAdvOptions }}>{children}</AdvOptions.Provider>; + return <ViewMode.Provider value={{ viewMode, setViewMode }}>{children}</ViewMode.Provider>; } -export function useAdvOptions() { - return useContext(AdvOptions); +export function useViewMode() { + return useContext(ViewMode); } const DarkMode = createContext(null); diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index a22a696e2..0e29efddb 100644 --- a/web/src/routes/Camera.jsx +++ b/web/src/routes/Camera.jsx @@ -9,7 +9,7 @@ import Link from '../components/Link'; import SettingsIcon from '../icons/Settings'; import Switch from '../components/Switch'; import ButtonsTabbed from '../components/ButtonsTabbed'; -import { usePersistence, useAdvOptions } from '../context'; +import { usePersistence } from '../context'; import { useCallback, useMemo, useState } from 'preact/hooks'; import { useApiHost } from '../api'; import useSWR from 'swr'; @@ -17,6 +17,7 @@ import WebRtcPlayer from '../components/WebRtcPlayer'; import '../components/MsePlayer'; import CameraControlPanel from '../components/CameraControlPanel'; import { baseUrl } from '../api/baseUrl'; +import ViewOption from '../components/ViewOption' const emptyObject = Object.freeze({}); @@ -25,7 +26,6 @@ export default function Camera({ camera }) { const apiHost = useApiHost(); const [showSettings, setShowSettings] = useState(false); const [viewMode, setViewMode] = useState('live'); - const { showAdvOptions } = useAdvOptions(); const cameraConfig = config?.cameras[camera]; const restreamEnabled = @@ -109,7 +109,9 @@ export default function Camera({ camera }) { label="Regions" labelPosition="after" /> - {showAdvOptions ? <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> : null} + <ViewOption requiredmode="admin"> + <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> + </ViewOption> </div> ) : null; diff --git a/web/src/routes/Cameras.jsx b/web/src/routes/Cameras.jsx index 91cf30ed2..97457b8cf 100644 --- a/web/src/routes/Cameras.jsx +++ b/web/src/routes/Cameras.jsx @@ -7,7 +7,8 @@ import MotionIcon from '../icons/Motion'; import SnapshotIcon from '../icons/Snapshot'; import { useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws'; import { useMemo } from 'preact/hooks'; -import { useAdvOptions } from '../context'; +import { useViewMode } from '../context' +import { ViewModeTypes } from '../components/ViewOptionEnum'; import useSWR from 'swr'; export default function Cameras() { @@ -87,9 +88,9 @@ function Camera({ name, config }) { [config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots] ); - const { showAdvOptions } = useAdvOptions(); + const { viewMode } = useViewMode(); return ( - <Card buttons={buttons} href={href} header={cleanName} icons={showAdvOptions ? icons : []} media={<CameraImage camera={name} stretch />} /> + <Card buttons={buttons} href={href} header={cleanName} icons={viewMode >= ViewModeTypes["admin"] ? icons : []} media={<CameraImage camera={name} stretch />} /> ); } diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index a69f280a4..59066eb98 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -29,7 +29,7 @@ import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../uti import TimeAgo from '../components/TimeAgo'; import Timepicker from '../components/TimePicker'; import TimelineSummary from '../components/TimelineSummary'; -import { useAdvOptions } from '../context'; +import ViewOption from '../components/ViewOption'; const API_LIMIT = 25; @@ -46,7 +46,6 @@ const monthsAgo = (num) => { }; export default function Events({ path, ...props }) { - const { showAdvOptions } = useAdvOptions(); const apiHost = useApiHost(); const [searchParams, setSearchParams] = useState({ before: null, @@ -662,12 +661,13 @@ export default function Events({ path, ...props }) { )} </div> <div class="flex flex-col"> - <Delete - className="h-6 w-6 cursor-pointer" - stroke={showAdvOptions ? "#f87171" : "lightgrey"} - onClick={showAdvOptions ? (e) => onDelete(e, event.id, event.retain_indefinitely) : null} - aria-label="Advanced Options are hidden via the nearby menu." - /> + <ViewOption requiredmode="admin"> + <Delete + className="h-6 w-6 cursor-pointer" + stroke="#f87171" + onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)} + /> + </ViewOption> <Download className="h-6 w-6 mt-auto" diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index e1915e2da..ae384a66a 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -12,14 +12,13 @@ import Dialog from '../components/Dialog'; import TimeAgo from '../components/TimeAgo'; import copy from 'copy-to-clipboard'; import { About } from '../icons/About'; -import { useAdvOptions } from '../context'; +import ViewOption from '../components/ViewOption'; const emptyObject = Object.freeze({}); export default function System() { const [state, setState] = useState({ showFfprobe: false, ffprobe: '' }); const { data: config } = useSWR('config'); - const { showAdvOptions } = useAdvOptions(); const { value: { payload: stats }, @@ -100,7 +99,7 @@ export default function System() { {config && ( <span class="p-1"> go2rtc {go2rtc && `${go2rtc.version} `} - {showAdvOptions ? + <ViewOption requiredmode="admin"> <Link className="text-blue-500 hover:underline" target="_blank" @@ -109,8 +108,7 @@ export default function System() { > dashboard </Link> - : null - } + </ViewOption> </span> )} </div>