diff --git a/frigate/config.py b/frigate/config.py index b71ba1907..14b94a82d 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -85,6 +85,10 @@ 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.", + ) class StatsConfig(FrigateBaseModel): diff --git a/web/src/AppBar.jsx b/web/src/AppBar.jsx index b5bb16c0f..fc57458cb 100644 --- a/web/src/AppBar.jsx +++ b/web/src/AppBar.jsx @@ -5,9 +5,10 @@ import Menu, { MenuItem, MenuSeparator } from './components/Menu'; import AutoAwesomeIcon from './icons/AutoAwesome'; import LightModeIcon from './icons/LightMode'; import DarkModeIcon from './icons/DarkMode'; +import SettingsIcon from './icons/Settings'; import FrigateRestartIcon from './icons/FrigateRestart'; import Prompt from './components/Prompt'; -import { useDarkMode } from './context'; +import { useDarkMode, useAdvOptions } from './context'; import { useCallback, useRef, useState } from 'preact/hooks'; import { useRestart } from './api/ws'; @@ -15,6 +16,7 @@ export default function AppBar() { const [showMoreMenu, setShowMoreMenu] = useState(false); const [showDialog, setShowDialog] = useState(false); const [showDialogWait, setShowDialogWait] = useState(false); + const { showAdvOptions, setShowAdvOptions } = useAdvOptions(); const { setDarkMode } = useDarkMode(); const { send: sendRestart } = useRestart(); @@ -26,6 +28,11 @@ export default function AppBar() { [setDarkMode, setShowMoreMenu] ); + const handleToggleAdvOptions = useCallback(() => { + setShowAdvOptions(showAdvOptions === 1 ? 0 : 1); + setShowMoreMenu(false); + },[showAdvOptions, setShowAdvOptions, setShowMoreMenu]); + const moreRef = useRef(null); const handleShowMenu = useCallback(() => { @@ -61,6 +68,8 @@ export default function AppBar() { + + ) : null} diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index 4497dba15..5dcd15615 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -4,11 +4,13 @@ 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 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,7 +50,7 @@ export default function Sidebar() { - + { showAdvOptions ? : null}
diff --git a/web/src/app.tsx b/web/src/app.tsx index 4e5123563..d21fa4471 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 } from './context'; +import { DarkModeProvider, DrawerProvider, AdvOptionsProvider } from './context'; import useSWR from 'swr'; export default function App() { @@ -16,37 +16,39 @@ export default function App() { return ( -
- - {!config ? ( -
- -
- ) : ( -
- -
- - - - - - - - - - - - - - + +
+ {!config ? ( +
+
-
- )} -
+ ) : ( +
+ + +
+ + + + + + + + + + + + + + +
+
+ )} +
+ ); diff --git a/web/src/components/AppBar.jsx b/web/src/components/AppBar.jsx index 1003fee55..a6870ef2b 100644 --- a/web/src/components/AppBar.jsx +++ b/web/src/components/AppBar.jsx @@ -2,7 +2,8 @@ import { h } from 'preact'; import Button from './Button'; import MenuIcon from '../icons/Menu'; import MoreIcon from '../icons/More'; -import { useDrawer } from '../context'; +import { About } from '../icons/About'; +import { useDrawer, useAdvOptions } from '../context'; import { useLayoutEffect, useCallback, useState } from 'preact/hooks'; // We would typically preserve these in component state @@ -13,6 +14,7 @@ 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; @@ -38,7 +40,7 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) { return (
<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/context/index.jsx b/web/src/context/index.jsx index 416eeb174..5bbcc5123 100644 --- a/web/src/context/index.jsx +++ b/web/src/context/index.jsx @@ -1,6 +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 useSWR from 'swr'; + +const AdvOptions = createContext(null); + +export function AdvOptionsProvider({ children }) { + const [showAdvOptions, setShowAdvOptions] = usePersistence('show-advanced-options', 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); + } + + load(); + }, [setShowAdvOptions, config]); + + return <AdvOptions.Provider value={{ showAdvOptions, setShowAdvOptions }}>{children}</AdvOptions.Provider>; +} + +export function useAdvOptions() { + return useContext(AdvOptions); +} const DarkMode = createContext(null); diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index 7f5e3ca77..a22a696e2 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 } from '../context'; +import { usePersistence, useAdvOptions } from '../context'; import { useCallback, useMemo, useState } from 'preact/hooks'; import { useApiHost } from '../api'; import useSWR from 'swr'; @@ -25,6 +25,7 @@ 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 = @@ -108,7 +109,7 @@ export default function Camera({ camera }) { label="Regions" labelPosition="after" /> - <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> + {showAdvOptions ? <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> : null} </div> ) : null; diff --git a/web/src/routes/Cameras.jsx b/web/src/routes/Cameras.jsx index 1e2bbf903..91cf30ed2 100644 --- a/web/src/routes/Cameras.jsx +++ b/web/src/routes/Cameras.jsx @@ -7,6 +7,7 @@ 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 useSWR from 'swr'; export default function Cameras() { @@ -86,7 +87,9 @@ function Camera({ name, config }) { [config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots] ); + const { showAdvOptions } = useAdvOptions(); + return ( - <Card buttons={buttons} href={href} header={cleanName} icons={icons} media={<CameraImage camera={name} stretch />} /> + <Card buttons={buttons} href={href} header={cleanName} icons={showAdvOptions ? icons : []} media={<CameraImage camera={name} stretch />} /> ); } diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 21b282a53..a69f280a4 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -29,6 +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'; const API_LIMIT = 25; @@ -45,6 +46,7 @@ const monthsAgo = (num) => { }; export default function Events({ path, ...props }) { + const { showAdvOptions } = useAdvOptions(); const apiHost = useApiHost(); const [searchParams, setSearchParams] = useState({ before: null, @@ -662,8 +664,9 @@ export default function Events({ path, ...props }) { <div class="flex flex-col"> <Delete className="h-6 w-6 cursor-pointer" - stroke="#f87171" - onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)} + 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." /> <Download diff --git a/web/src/routes/System.jsx b/web/src/routes/System.jsx index 82c8d619d..e1915e2da 100644 --- a/web/src/routes/System.jsx +++ b/web/src/routes/System.jsx @@ -12,13 +12,15 @@ 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'; 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 }, } = useWs('stats'); @@ -98,14 +100,17 @@ export default function System() { {config && ( <span class="p-1"> go2rtc {go2rtc && `${go2rtc.version} `} - <Link - className="text-blue-500 hover:underline" - target="_blank" - rel="noopener noreferrer" - href="/live/webrtc/" - > - dashboard - </Link> + {showAdvOptions ? + <Link + className="text-blue-500 hover:underline" + target="_blank" + rel="noopener noreferrer" + href="/live/webrtc/" + > + dashboard + </Link> + : null + } </span> )} </div>