From 96f87caff02de2afe62f85fa17e27d9481699a60 Mon Sep 17 00:00:00 2001 From: Paul Armstrong Date: Thu, 4 Feb 2021 15:19:47 -0800 Subject: [PATCH] refactor(web): camera view + bugfixes --- web/src/Camera.jsx | 108 +++++++++++++++--------- web/src/CameraMap.jsx | 12 ++- web/src/Sidebar.jsx | 2 +- web/src/components/Button.jsx | 2 +- web/src/components/Card.jsx | 17 ++-- web/src/components/NavigationDrawer.jsx | 6 +- web/src/components/TextField.jsx | 14 ++- web/src/context/index.jsx | 34 ++++++++ web/src/icons/ArrowDropdown.jsx | 2 +- web/src/icons/ArrowDropup.jsx | 2 +- web/src/icons/DarkMode.jsx | 2 +- web/src/icons/Menu.jsx | 2 +- web/src/icons/MenuOpen.jsx | 2 +- web/src/icons/More.jsx | 2 +- web/src/icons/Settings.jsx | 12 +++ 15 files changed, 156 insertions(+), 63 deletions(-) create mode 100644 web/src/icons/Settings.jsx diff --git a/web/src/Camera.jsx b/web/src/Camera.jsx index 43a9bae07..a586e8a21 100644 --- a/web/src/Camera.jsx +++ b/web/src/Camera.jsx @@ -1,73 +1,99 @@ import { h } from 'preact'; import AutoUpdatingCameraImage from './components/AutoUpdatingCameraImage'; +import Button from './components/Button'; import Card from './components/Card'; import Heading from './components/Heading'; import Link from './components/Link'; +import SettingsIcon from './icons/Settings'; import Switch from './components/Switch'; import { route } from 'preact-router'; -import { useCallback, useContext } from 'preact/hooks'; +import { usePersistence } from './context'; +import { useCallback, useContext, useMemo, useState } from 'preact/hooks'; import { useApiHost, useConfig } from './api'; -export default function Camera({ camera, url }) { +export default function Camera({ camera }) { const { data: config } = useConfig(); const apiHost = useApiHost(); + const [showSettings, setShowSettings] = useState(false); if (!config) { return
{`No camera named ${camera}`}
; } const cameraConfig = config.cameras[camera]; - const objectCount = cameraConfig.objects.track.length; + const [options, setOptions, optionsLoaded] = usePersistence(`${camera}-feed`, Object.freeze({})); - const { pathname, searchParams } = new URL(`${window.location.protocol}//${window.location.host}${url}`); - const searchParamsString = searchParams.toString(); + const objectCount = useMemo(() => cameraConfig.objects.track.length, [cameraConfig]); const handleSetOption = useCallback( (id, value) => { - searchParams.set(id, value ? 1 : 0); - route(`${pathname}?${searchParams.toString()}`, true); + const newOptions = { ...options, [id]: value }; + setOptions(newOptions); }, - [searchParams] + [options] ); - function getBoolean(id) { - return Boolean(parseInt(searchParams.get(id), 10)); - } + const searchParams = useMemo( + () => + new URLSearchParams( + Object.keys(options).reduce((memo, key) => { + memo.push([key, options[key] === true ? '1' : '0']); + return memo; + }, []) + ), + [camera, options] + ); + + const handleToggleSettings = useCallback(() => { + setShowSettings(!showSettings); + }, [showSettings, setShowSettings]); + + const optionContent = showSettings ? ( +
+
+ + Bounding box +
+
+ + Timestamp +
+
+ + Zones +
+
+ + Masks +
+
+ + Motion boxes +
+
+ + Regions +
+ Mask & Zone creator +
+ ) : null; return (
{camera} -
- -
+ {optionsLoaded ? ( +
+ +
+ ) : null} -
-
- - Bounding box -
-
- - Timestamp -
-
- - Zones -
-
- - Masks -
-
- - Motion boxes -
-
- - Regions -
- Mask & Zone creator -
+ + {showSettings ? : null}
Tracked objects diff --git a/web/src/CameraMap.jsx b/web/src/CameraMap.jsx index ae817548f..c267a6c57 100644 --- a/web/src/CameraMap.jsx +++ b/web/src/CameraMap.jsx @@ -400,7 +400,10 @@ function EditableMask({ onChange, points, scale, snap, width, height }) { const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]); return ( -
+
{!scaledPoints ? null : scaledPoints.map(([x, y], i) => ( @@ -414,7 +417,12 @@ function EditableMask({ onChange, points, scale, snap, width, height }) { /> ))}
- + {!scaledPoints ? null : ( diff --git a/web/src/Sidebar.jsx b/web/src/Sidebar.jsx index 94aceec30..a1d58385d 100644 --- a/web/src/Sidebar.jsx +++ b/web/src/Sidebar.jsx @@ -13,7 +13,7 @@ export default function Sidebar() { return ( }> - + {({ matches }) => matches ? ( diff --git a/web/src/components/Button.jsx b/web/src/components/Button.jsx index f1b42fd06..9b55b86c1 100644 --- a/web/src/components/Button.jsx +++ b/web/src/components/Button.jsx @@ -55,7 +55,7 @@ export default function Button({ type = 'contained', ...attrs }) { - let classes = `${className} ${ButtonTypes[type]} ${ + let classes = `whitespace-nowrap flex items-center space-x-1 ${className} ${ButtonTypes[type]} ${ ButtonColors[disabled ? 'disabled' : color][type] } font-sans inline-flex font-bold uppercase text-xs px-2 py-2 rounded outline-none focus:outline-none ring-opacity-50 transition-shadow transition-colors ${ disabled ? 'cursor-not-allowed' : 'focus:ring-2 cursor-pointer' diff --git a/web/src/components/Card.jsx b/web/src/components/Card.jsx index f9393a37b..a6bf9678f 100644 --- a/web/src/components/Card.jsx +++ b/web/src/components/Card.jsx @@ -6,6 +6,7 @@ export default function Box({ buttons = [], className = '', content, + elevated = true, header, href, icons, @@ -16,14 +17,16 @@ export default function Box({ }) { const Element = href ? 'a' : 'div'; + const typeClasses = elevated ? 'shadow-md hover:shadow-lg transition-shadow' : 'border border-gray-200'; + return ( -
- - {media} -
{header ? {header} : null}
-
+
+ {media || header ? ( + + {media} +
{header ? {header} : null}
+
+ ) : null} {buttons.length || content ? (
{content || null} diff --git a/web/src/components/NavigationDrawer.jsx b/web/src/components/NavigationDrawer.jsx index fc6d08d6e..768b8db71 100644 --- a/web/src/components/NavigationDrawer.jsx +++ b/web/src/components/NavigationDrawer.jsx @@ -49,10 +49,12 @@ export function Destination({ className = '', href, text, ...other }) { : 'class']: 'block p-2 text-sm font-semibold text-gray-900 rounded hover:bg-blue-500 dark:text-gray-200 hover:text-white dark:hover:text-white focus:outline-none ring-opacity-50 focus:ring-2 ring-blue-300', }; + const El = external ? 'a' : Link; + return ( - +
{text}
- +
); } diff --git a/web/src/components/TextField.jsx b/web/src/components/TextField.jsx index a66159dcc..a57d43d7d 100644 --- a/web/src/components/TextField.jsx +++ b/web/src/components/TextField.jsx @@ -60,8 +60,12 @@ export default function TextField({ }`} ref={inputRef} > -
{helpText ?
{helpText}
: null} diff --git a/web/src/context/index.jsx b/web/src/context/index.jsx index 09ceba80d..cbe8e840d 100644 --- a/web/src/context/index.jsx +++ b/web/src/context/index.jsx @@ -80,3 +80,37 @@ export function DrawerProvider({ children }) { export function useDrawer() { return useContext(Drawer); } + +export function usePersistence(key, defaultValue = undefined) { + const [value, setInternalValue] = useState(defaultValue); + const [loaded, setLoaded] = useState(false); + + const setValue = useCallback( + (value) => { + setInternalValue(value); + async function update() { + await setData(key, value); + } + + update(); + }, + [key] + ); + + useEffect(() => { + setLoaded(false); + setInternalValue(defaultValue); + + async function load() { + const value = await getData(key); + if (typeof value !== 'undefined') { + setValue(value); + } + setLoaded(true); + } + + load(); + }, [key]); + + return [value, setValue, loaded]; +} diff --git a/web/src/icons/ArrowDropdown.jsx b/web/src/icons/ArrowDropdown.jsx index f053006d6..abb59a464 100644 --- a/web/src/icons/ArrowDropdown.jsx +++ b/web/src/icons/ArrowDropdown.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function ArrowDropdown() { return ( - + diff --git a/web/src/icons/ArrowDropup.jsx b/web/src/icons/ArrowDropup.jsx index 3dcb9ecc1..984e820e9 100644 --- a/web/src/icons/ArrowDropup.jsx +++ b/web/src/icons/ArrowDropup.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function ArrowDropup() { return ( - + diff --git a/web/src/icons/DarkMode.jsx b/web/src/icons/DarkMode.jsx index d85691a3a..329449251 100644 --- a/web/src/icons/DarkMode.jsx +++ b/web/src/icons/DarkMode.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function DarkMode() { return ( - + diff --git a/web/src/icons/Menu.jsx b/web/src/icons/Menu.jsx index 5c5a922d5..2fcea205f 100644 --- a/web/src/icons/Menu.jsx +++ b/web/src/icons/Menu.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function Menu() { return ( - + diff --git a/web/src/icons/MenuOpen.jsx b/web/src/icons/MenuOpen.jsx index 852f24c10..cad03fd01 100644 --- a/web/src/icons/MenuOpen.jsx +++ b/web/src/icons/MenuOpen.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function MenuOpen() { return ( - + diff --git a/web/src/icons/More.jsx b/web/src/icons/More.jsx index ccd5d573f..76dacfb5e 100644 --- a/web/src/icons/More.jsx +++ b/web/src/icons/More.jsx @@ -2,7 +2,7 @@ import { h } from 'preact'; export default function More() { return ( - + diff --git a/web/src/icons/Settings.jsx b/web/src/icons/Settings.jsx new file mode 100644 index 000000000..32989fda5 --- /dev/null +++ b/web/src/icons/Settings.jsx @@ -0,0 +1,12 @@ +import { h } from 'preact'; + +export default function DarkMode() { + return ( + + + + + + + ); +}