toggle viewing of advanced options

- fix AppBar render order, its tooltips are visible now
This commit is contained in:
spacebares 2023-06-25 12:05:07 -04:00
parent 9e531b0b5b
commit 160e9ba336
10 changed files with 116 additions and 49 deletions

View File

@ -85,6 +85,10 @@ class UIConfig(FrigateBaseModel):
strftime_fmt: Optional[str] = Field( strftime_fmt: Optional[str] = Field(
default=None, title="Override date and time format using strftime syntax." 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): class StatsConfig(FrigateBaseModel):

View File

@ -5,9 +5,10 @@ import Menu, { MenuItem, MenuSeparator } from './components/Menu';
import AutoAwesomeIcon from './icons/AutoAwesome'; import AutoAwesomeIcon from './icons/AutoAwesome';
import LightModeIcon from './icons/LightMode'; import LightModeIcon from './icons/LightMode';
import DarkModeIcon from './icons/DarkMode'; import DarkModeIcon from './icons/DarkMode';
import SettingsIcon from './icons/Settings';
import FrigateRestartIcon from './icons/FrigateRestart'; import FrigateRestartIcon from './icons/FrigateRestart';
import Prompt from './components/Prompt'; import Prompt from './components/Prompt';
import { useDarkMode } from './context'; import { useDarkMode, useAdvOptions } from './context';
import { useCallback, useRef, useState } from 'preact/hooks'; import { useCallback, useRef, useState } from 'preact/hooks';
import { useRestart } from './api/ws'; import { useRestart } from './api/ws';
@ -15,6 +16,7 @@ export default function AppBar() {
const [showMoreMenu, setShowMoreMenu] = useState(false); const [showMoreMenu, setShowMoreMenu] = useState(false);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [showDialogWait, setShowDialogWait] = useState(false); const [showDialogWait, setShowDialogWait] = useState(false);
const { showAdvOptions, setShowAdvOptions } = useAdvOptions();
const { setDarkMode } = useDarkMode(); const { setDarkMode } = useDarkMode();
const { send: sendRestart } = useRestart(); const { send: sendRestart } = useRestart();
@ -26,6 +28,11 @@ export default function AppBar() {
[setDarkMode, setShowMoreMenu] [setDarkMode, setShowMoreMenu]
); );
const handleToggleAdvOptions = useCallback(() => {
setShowAdvOptions(showAdvOptions === 1 ? 0 : 1);
setShowMoreMenu(false);
},[showAdvOptions, setShowAdvOptions, setShowMoreMenu]);
const moreRef = useRef(null); const moreRef = useRef(null);
const handleShowMenu = useCallback(() => { const handleShowMenu = useCallback(() => {
@ -61,6 +68,8 @@ export default function AppBar() {
<MenuItem icon={LightModeIcon} label="Light" value="light" onSelect={handleSelectDarkMode} /> <MenuItem icon={LightModeIcon} label="Light" value="light" onSelect={handleSelectDarkMode} />
<MenuItem icon={DarkModeIcon} label="Dark" value="dark" onSelect={handleSelectDarkMode} /> <MenuItem icon={DarkModeIcon} label="Dark" value="dark" onSelect={handleSelectDarkMode} />
<MenuSeparator /> <MenuSeparator />
<MenuItem icon={SettingsIcon} label={showAdvOptions ? 'Hide Adv. Options' : 'Show Adv. Options'} onSelect={handleToggleAdvOptions} />
<MenuSeparator />
<MenuItem icon={FrigateRestartIcon} label="Restart Frigate" onSelect={handleRestart} /> <MenuItem icon={FrigateRestartIcon} label="Restart Frigate" onSelect={handleRestart} />
</Menu> </Menu>
) : null} ) : null}

View File

@ -4,11 +4,13 @@ import { Match } from 'preact-router/match';
import { memo } from 'preact/compat'; import { memo } from 'preact/compat';
import { ENV } from './env'; import { ENV } from './env';
import { useMemo } from 'preact/hooks' import { useMemo } from 'preact/hooks'
import { useAdvOptions } from './context';
import useSWR from 'swr'; import useSWR from 'swr';
import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer'; import NavigationDrawer, { Destination, Separator } from './components/NavigationDrawer';
export default function Sidebar() { export default function Sidebar() {
const { data: config } = useSWR('config'); const { data: config } = useSWR('config');
const { showAdvOptions } = useAdvOptions();
const sortedCameras = useMemo(() => { const sortedCameras = useMemo(() => {
if (!config) { if (!config) {
@ -48,7 +50,7 @@ export default function Sidebar() {
<Separator /> <Separator />
<Destination href="/storage" text="Storage" /> <Destination href="/storage" text="Storage" />
<Destination href="/system" text="System" /> <Destination href="/system" text="System" />
<Destination href="/config" text="Config" /> { showAdvOptions ? <Destination href="/config" text="Config" /> : null}
<Destination href="/logs" text="Logs" /> <Destination href="/logs" text="Logs" />
<Separator /> <Separator />
<div className="flex flex-grow" /> <div className="flex flex-grow" />

View File

@ -6,7 +6,7 @@ import AppBar from './AppBar';
import Cameras from './routes/Cameras'; import Cameras from './routes/Cameras';
import { Router } from 'preact-router'; import { Router } from 'preact-router';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import { DarkModeProvider, DrawerProvider } from './context'; import { DarkModeProvider, DrawerProvider, AdvOptionsProvider } from './context';
import useSWR from 'swr'; import useSWR from 'swr';
export default function App() { export default function App() {
@ -16,37 +16,39 @@ export default function App() {
return ( return (
<DarkModeProvider> <DarkModeProvider>
<DrawerProvider> <DrawerProvider>
<div data-testid="app" className="w-full"> <AdvOptionsProvider>
<AppBar /> <div data-testid="app" className="w-full">
{!config ? ( {!config ? (
<div className="flex flex-grow-1 min-h-screen justify-center items-center"> <div className="flex flex-grow-1 min-h-screen justify-center items-center">
<ActivityIndicator /> <ActivityIndicator />
</div>
) : (
<div className="flex flex-row min-h-screen w-full bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
<Sidebar />
<div className="w-full flex-auto mt-16 min-w-0">
<Router>
<AsyncRoute path="/cameras/:camera/editor" getComponent={Routes.getCameraMap} />
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
<AsyncRoute path="/exports" getComponent={Routes.getExports} />
<AsyncRoute
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
getComponent={Routes.getRecording}
/>
<AsyncRoute path="/storage" getComponent={Routes.getStorage} />
<AsyncRoute path="/system" getComponent={Routes.getSystem} />
<AsyncRoute path="/config" getComponent={Routes.getConfig} />
<AsyncRoute path="/logs" getComponent={Routes.getLogs} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" />
</Router>
</div> </div>
</div> ) : (
)} <div className="flex flex-row min-h-screen w-full bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
</div> <Sidebar />
<AppBar />
<div className="w-full flex-auto mt-16 min-w-0">
<Router>
<AsyncRoute path="/cameras/:camera/editor" getComponent={Routes.getCameraMap} />
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
<AsyncRoute path="/exports" getComponent={Routes.getExports} />
<AsyncRoute
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
getComponent={Routes.getRecording}
/>
<AsyncRoute path="/storage" getComponent={Routes.getStorage} />
<AsyncRoute path="/system" getComponent={Routes.getSystem} />
<AsyncRoute path="/config" getComponent={Routes.getConfig} />
<AsyncRoute path="/logs" getComponent={Routes.getLogs} />
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
<Cameras default path="/" />
</Router>
</div>
</div>
)}
</div>
</AdvOptionsProvider>
</DrawerProvider> </DrawerProvider>
</DarkModeProvider> </DarkModeProvider>
); );

View File

@ -2,7 +2,8 @@ import { h } from 'preact';
import Button from './Button'; import Button from './Button';
import MenuIcon from '../icons/Menu'; import MenuIcon from '../icons/Menu';
import MoreIcon from '../icons/More'; 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'; import { useLayoutEffect, useCallback, useState } from 'preact/hooks';
// We would typically preserve these in component state // 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 [show, setShow] = useState(true);
const [atZero, setAtZero] = useState(window.scrollY === 0); const [atZero, setAtZero] = useState(window.scrollY === 0);
const { setShowDrawer } = useDrawer(); const { setShowDrawer } = useDrawer();
const { showAdvOptions } = useAdvOptions();
const scrollListener = useCallback(() => { const scrollListener = useCallback(() => {
const scrollY = window.scrollY; const scrollY = window.scrollY;
@ -38,7 +40,7 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
return ( return (
<div <div
id="appbar" id="appbar"
className={`w-full border-b border-gray-200 dark:border-gray-700 flex items-center align-middle p-2 fixed left-0 right-0 z-10 bg-white dark:bg-gray-900 transform transition-all duration-200 ${ className={`w-full border-b border-gray-200 dark:border-gray-700 flex items-center align-middle p-2 fixed left-0 right-0 bg-white dark:bg-gray-900 transform transition-all duration-200 ${
!show ? '-translate-y-full' : 'translate-y-0' !show ? '-translate-y-full' : 'translate-y-0'
} ${!atZero ? 'shadow-sm' : ''}`} } ${!atZero ? 'shadow-sm' : ''}`}
data-testid="appbar" data-testid="appbar"
@ -50,6 +52,19 @@ export default function AppBar({ title: Title, overflowRef, onOverflowClick }) {
</div> </div>
<Title /> <Title />
<div className="flex-grow-1 flex justify-end w-full"> <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 ? ( {overflowRef && onOverflowClick ? (
<div className="w-auto" ref={overflowRef}> <div className="w-auto" ref={overflowRef}>
<Button <Button

View File

@ -1,6 +1,29 @@
import { h, createContext } from 'preact'; import { h, createContext } from 'preact';
import { get as getData, set as setData } from 'idb-keyval'; import { get as getData, set as setData } from 'idb-keyval';
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'preact/hooks'; 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); const DarkMode = createContext(null);

View File

@ -9,7 +9,7 @@ import Link from '../components/Link';
import SettingsIcon from '../icons/Settings'; import SettingsIcon from '../icons/Settings';
import Switch from '../components/Switch'; import Switch from '../components/Switch';
import ButtonsTabbed from '../components/ButtonsTabbed'; import ButtonsTabbed from '../components/ButtonsTabbed';
import { usePersistence } from '../context'; import { usePersistence, useAdvOptions } from '../context';
import { useCallback, useMemo, useState } from 'preact/hooks'; import { useCallback, useMemo, useState } from 'preact/hooks';
import { useApiHost } from '../api'; import { useApiHost } from '../api';
import useSWR from 'swr'; import useSWR from 'swr';
@ -25,6 +25,7 @@ export default function Camera({ camera }) {
const apiHost = useApiHost(); const apiHost = useApiHost();
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [viewMode, setViewMode] = useState('live'); const [viewMode, setViewMode] = useState('live');
const { showAdvOptions } = useAdvOptions();
const cameraConfig = config?.cameras[camera]; const cameraConfig = config?.cameras[camera];
const restreamEnabled = const restreamEnabled =
@ -108,7 +109,7 @@ export default function Camera({ camera }) {
label="Regions" label="Regions"
labelPosition="after" labelPosition="after"
/> />
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> {showAdvOptions ? <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link> : null}
</div> </div>
) : null; ) : null;

View File

@ -7,6 +7,7 @@ import MotionIcon from '../icons/Motion';
import SnapshotIcon from '../icons/Snapshot'; import SnapshotIcon from '../icons/Snapshot';
import { useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws'; import { useDetectState, useRecordingsState, useSnapshotsState } from '../api/ws';
import { useMemo } from 'preact/hooks'; import { useMemo } from 'preact/hooks';
import { useAdvOptions } from '../context';
import useSWR from 'swr'; import useSWR from 'swr';
export default function Cameras() { export default function Cameras() {
@ -86,7 +87,9 @@ function Camera({ name, config }) {
[config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots] [config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
); );
const { showAdvOptions } = useAdvOptions();
return ( 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 />} />
); );
} }

View File

@ -29,6 +29,7 @@ import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../uti
import TimeAgo from '../components/TimeAgo'; import TimeAgo from '../components/TimeAgo';
import Timepicker from '../components/TimePicker'; import Timepicker from '../components/TimePicker';
import TimelineSummary from '../components/TimelineSummary'; import TimelineSummary from '../components/TimelineSummary';
import { useAdvOptions } from '../context';
const API_LIMIT = 25; const API_LIMIT = 25;
@ -45,6 +46,7 @@ const monthsAgo = (num) => {
}; };
export default function Events({ path, ...props }) { export default function Events({ path, ...props }) {
const { showAdvOptions } = useAdvOptions();
const apiHost = useApiHost(); const apiHost = useApiHost();
const [searchParams, setSearchParams] = useState({ const [searchParams, setSearchParams] = useState({
before: null, before: null,
@ -662,8 +664,9 @@ export default function Events({ path, ...props }) {
<div class="flex flex-col"> <div class="flex flex-col">
<Delete <Delete
className="h-6 w-6 cursor-pointer" className="h-6 w-6 cursor-pointer"
stroke="#f87171" stroke={showAdvOptions ? "#f87171" : "lightgrey"}
onClick={(e) => onDelete(e, event.id, event.retain_indefinitely)} onClick={showAdvOptions ? (e) => onDelete(e, event.id, event.retain_indefinitely) : null}
aria-label="Advanced Options are hidden via the nearby menu."
/> />
<Download <Download

View File

@ -12,13 +12,15 @@ import Dialog from '../components/Dialog';
import TimeAgo from '../components/TimeAgo'; import TimeAgo from '../components/TimeAgo';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { About } from '../icons/About'; import { About } from '../icons/About';
import { useAdvOptions } from '../context';
const emptyObject = Object.freeze({}); const emptyObject = Object.freeze({});
export default function System() { export default function System() {
const [state, setState] = useState({ showFfprobe: false, ffprobe: '' }); const [state, setState] = useState({ showFfprobe: false, ffprobe: '' });
const { data: config } = useSWR('config'); const { data: config } = useSWR('config');
const { showAdvOptions } = useAdvOptions();
const { const {
value: { payload: stats }, value: { payload: stats },
} = useWs('stats'); } = useWs('stats');
@ -98,14 +100,17 @@ export default function System() {
{config && ( {config && (
<span class="p-1"> <span class="p-1">
go2rtc {go2rtc && `${go2rtc.version} `} go2rtc {go2rtc && `${go2rtc.version} `}
<Link {showAdvOptions ?
className="text-blue-500 hover:underline" <Link
target="_blank" className="text-blue-500 hover:underline"
rel="noopener noreferrer" target="_blank"
href="/live/webrtc/" rel="noopener noreferrer"
> href="/live/webrtc/"
dashboard >
</Link> dashboard
</Link>
: null
}
</span> </span>
)} )}
</div> </div>