refactor(web): camera view + bugfixes

This commit is contained in:
Paul Armstrong 2021-02-04 15:19:47 -08:00 committed by Blake Blackshear
parent b422a83b57
commit 96f87caff0
15 changed files with 156 additions and 63 deletions

View File

@ -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 <div>{`No camera named ${camera}`}</div>;
}
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 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<div className="flex space-x-3">
<Switch checked={options['bbox']} id="bbox" onChange={handleSetOption} />
<span class="inline-flex">Bounding box</span>
</div>
<div className="flex space-x-3">
<Switch checked={options['timestamp']} id="timestamp" onChange={handleSetOption} />
<span class="inline-flex">Timestamp</span>
</div>
<div className="flex space-x-3">
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} />
<span class="inline-flex">Zones</span>
</div>
<div className="flex space-x-3">
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} />
<span class="inline-flex">Masks</span>
</div>
<div className="flex space-x-3">
<Switch checked={options['motion']} id="motion" onChange={handleSetOption} />
<span class="inline-flex">Motion boxes</span>
</div>
<div className="flex space-x-3">
<Switch checked={options['regions']} id="regions" onChange={handleSetOption} />
<span class="inline-flex">Regions</span>
</div>
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</div>
) : null;
return (
<div className="space-y-4">
<Heading size="2xl">{camera}</Heading>
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParamsString} />
</div>
{optionsLoaded ? (
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
</div>
) : null}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 p-4">
<div className="flex space-x-3">
<Switch checked={getBoolean('bbox')} id="bbox" onChange={handleSetOption} />
<span class="inline-flex">Bounding box</span>
</div>
<div className="flex space-x-3">
<Switch checked={getBoolean('timestamp')} id="timestamp" onChange={handleSetOption} />
<span class="inline-flex">Timestamp</span>
</div>
<div className="flex space-x-3">
<Switch checked={getBoolean('zones')} id="zones" onChange={handleSetOption} />
<span class="inline-flex">Zones</span>
</div>
<div className="flex space-x-3">
<Switch checked={getBoolean('mask')} id="mask" onChange={handleSetOption} />
<span class="inline-flex">Masks</span>
</div>
<div className="flex space-x-3">
<Switch checked={getBoolean('motion')} id="motion" onChange={handleSetOption} />
<span class="inline-flex">Motion boxes</span>
</div>
<div className="flex space-x-3">
<Switch checked={getBoolean('regions')} id="regions" onChange={handleSetOption} />
<span class="inline-flex">Regions</span>
</div>
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
</div>
<Button onClick={handleToggleSettings} type="text">
<span class="w-5 h-5">
<SettingsIcon />
</span>{' '}
<span>{showSettings ? 'Hide' : 'Show'} Options</span>
</Button>
{showSettings ? <Card header="Options" elevated={false} content={optionContent} /> : null}
<div className="space-y-4">
<Heading size="sm">Tracked objects</Heading>

View File

@ -400,7 +400,10 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
const scaledPoints = useMemo(() => scalePolylinePoints(points, scale), [points, scale]);
return (
<div className="absolute" style={`inset: -${MaskInset}px`}>
<div
className="absolute"
style={`top: -${MaskInset}px; right: -${MaskInset}px; bottom: -${MaskInset}px; left: -${MaskInset}px`}
>
{!scaledPoints
? null
: scaledPoints.map(([x, y], i) => (
@ -414,7 +417,12 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
/>
))}
<div className="absolute inset-0 right-0 bottom-0" onclick={handleAddPoint} ref={boundingRef} />
<svg width="100%" height="100%" className="absolute pointer-events-none" style={`inset: ${MaskInset}px`}>
<svg
width="100%"
height="100%"
className="absolute pointer-events-none"
style={`top: ${MaskInset}px; right: ${MaskInset}px; bottom: ${MaskInset}px; left: ${MaskInset}px`}
>
{!scaledPoints ? null : (
<g>
<polyline points={polylinePointsToPolyline(scaledPoints)} fill="rgba(244,0,0,0.5)" />

View File

@ -13,7 +13,7 @@ export default function Sidebar() {
return (
<NavigationDrawer header={<Header />}>
<Destination href="/" text="Cameras" />
<Match path="/cameras/:camera">
<Match path="/cameras/:camera/:other?">
{({ matches }) =>
matches ? (
<Fragment>

View File

@ -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'

View File

@ -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 (
<div
className={`bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-shadow rounded-lg overflow-hidden ${className}`}
>
<Element href={href} {...props}>
{media}
<div class="p-4 pb-2">{header ? <Heading size="base">{header}</Heading> : null}</div>
</Element>
<div className={`bg-white dark:bg-gray-800 rounded-lg overflow-hidden ${typeClasses} ${className}`}>
{media || header ? (
<Element href={href} {...props}>
{media}
<div class="p-4 pb-2">{header ? <Heading size="base">{header}</Heading> : null}</div>
</Element>
) : null}
{buttons.length || content ? (
<div class="pl-4 pb-2">
{content || null}

View File

@ -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 (
<Link activeClassName="bg-blue-500 bg-opacity-50 text-white" {...styleProps} href={href} {...props} {...other}>
<El activeClassName="bg-blue-500 bg-opacity-50 text-white" {...styleProps} href={href} {...props} {...other}>
<div onClick={handleDismiss}>{text}</div>
</Link>
</El>
);
}

View File

@ -60,8 +60,12 @@ export default function TextField({
}`}
ref={inputRef}
>
<label className="flex space-x-2">
{LeadingIcon ? <LeadingIcon /> : null}
<label className="flex space-x-2 items-center">
{LeadingIcon ? (
<div class="w-10 h-full">
<LeadingIcon />
</div>
) : null}
<div className="relative w-full">
<input
className="h-6 mt-6 w-full bg-transparent focus:outline-none focus:ring-0"
@ -82,7 +86,11 @@ export default function TextField({
{label}
</div>
</div>
{TrailingIcon ? <TrailingIcon /> : null}
{TrailingIcon ? (
<div class="w-10 h-10">
<TrailingIcon />
</div>
) : null}
</label>
</div>
{helpText ? <div className="text-xs pl-3 pt-1">{helpText}</div> : null}

View File

@ -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];
}

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function ArrowDropdown() {
return (
<svg className="w-10 fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M7 10l5 5 5-5z" />
</svg>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function ArrowDropup() {
return (
<svg className="w-10 fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M7 14l5-5 5 5z" />
</svg>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function DarkMode() {
return (
<svg className=" fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<rect fill="none" height="24" width="24" />
<path d="M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36c-0.98,1.37-2.58,2.26-4.4,2.26 c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z" />
</svg>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function Menu() {
return (
<svg className="w-10 fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
</svg>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function MenuOpen() {
return (
<svg className="w-10 fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M3 18h13v-2H3v2zm0-5h10v-2H3v2zm0-7v2h13V6H3zm18 9.59L17.42 12 21 8.41 19.59 7l-5 5 5 5L21 15.59z" />
</svg>

View File

@ -2,7 +2,7 @@ import { h } from 'preact';
export default function More() {
return (
<svg className="w-10 fill-current" viewBox="0 0 24 24">
<svg className="fill-current" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
</svg>

View File

@ -0,0 +1,12 @@
import { h } from 'preact';
export default function DarkMode() {
return (
<svg className="fill-current" viewBox="0 0 24 24">
<g>
<path d="M0,0h24v24H0V0z" fill="none" />
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z" />
</g>
</svg>
);
}