Skip to content

Commit

Permalink
refactor(web): camera view + bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
paularmstrong authored and blakeblackshear committed Feb 20, 2021
1 parent b422a83 commit 96f87ca
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 63 deletions.
108 changes: 67 additions & 41 deletions web/src/Camera.jsx
Original file line number Diff line number Diff line change
@@ -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>

<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>
{optionsLoaded ? (
<div>
<AutoUpdatingCameraImage camera={camera} searchParams={searchParams} />
</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>
) : null}

<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>
Expand Down
12 changes: 10 additions & 2 deletions web/src/CameraMap.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => (
Expand All @@ -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)" />
Expand Down
2 changes: 1 addition & 1 deletion web/src/Sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/Button.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
17 changes: 10 additions & 7 deletions web/src/components/Card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default function Box({
buttons = [],
className = '',
content,
elevated = true,
header,
href,
icons,
Expand All @@ -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}
Expand Down
6 changes: 4 additions & 2 deletions web/src/components/NavigationDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
}

Expand Down
14 changes: 11 additions & 3 deletions web/src/components/TextField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}
Expand Down
34 changes: 34 additions & 0 deletions web/src/context/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
2 changes: 1 addition & 1 deletion web/src/icons/ArrowDropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/icons/ArrowDropup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/icons/DarkMode.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/icons/Menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/icons/MenuOpen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion web/src/icons/More.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions web/src/icons/Settings.jsx
Original file line number Diff line number Diff line change
@@ -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>
);
}

0 comments on commit 96f87ca

Please sign in to comment.