mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
refactor(web): camera view + bugfixes
This commit is contained in:
parent
b422a83b57
commit
96f87caff0
@ -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>
|
||||
|
@ -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)" />
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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,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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
12
web/src/icons/Settings.jsx
Normal file
12
web/src/icons/Settings.jsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user