sync master

This commit is contained in:
Blake Blackshear
2023-05-19 05:47:49 -05:00
25 changed files with 404 additions and 60 deletions

View File

@@ -16,7 +16,7 @@ export const handlers = [
front: {
name: 'front',
objects: { track: ['taco', 'cat', 'dog'] },
record: { enabled: true },
record: { enabled: true, enabled_in_config: true },
detect: { width: 1280, height: 720 },
snapshots: {},
restream: { enabled: true, jsmpeg: { height: 720 } },
@@ -25,7 +25,7 @@ export const handlers = [
side: {
name: 'side',
objects: { track: ['taco', 'cat', 'dog'] },
record: { enabled: false },
record: { enabled: false, enabled_in_config: true },
detect: { width: 1280, height: 720 },
snapshots: {},
restream: { enabled: true, jsmpeg: { height: 720 } },

View File

@@ -7,54 +7,65 @@ import Button from './Button';
import CameraIcon from '../icons/Camera';
export default function MultiSelect({ className, title, options, selection, onToggle, onShowAll, onSelectSingle }) {
const popupRef = useRef(null);
const [state, setState] = useState({
showMenu: false,
});
const isOptionSelected = (item) => { return selection == "all" || selection.split(',').indexOf(item) > -1; }
const isOptionSelected = (item) => {
return selection == 'all' || selection.split(',').indexOf(item) > -1;
};
const menuHeight = Math.round(window.innerHeight * 0.55);
return (
<div className={`${className} p-2`} ref={popupRef}>
<div
className="flex justify-between min-w-[120px]"
onClick={() => setState({ showMenu: true })}
>
<div className="flex justify-between min-w-[120px]" onClick={() => setState({ showMenu: true })}>
<label>{title}</label>
<ArrowDropdown className="w-6" />
</div>
{state.showMenu ? (
<Menu className={`max-h-[${menuHeight}px] overflow-scroll`} relativeTo={popupRef} onDismiss={() => setState({ showMenu: false })}>
<Menu
className={`max-h-[${menuHeight}px] overflow-auto`}
relativeTo={popupRef}
onDismiss={() => setState({ showMenu: false })}
>
<div className="flex flex-wrap justify-between items-center">
<Heading className="p-4 justify-center" size="md">{title}</Heading>
<Button tabindex="false" className="mx-4" onClick={() => onShowAll() }>
<Heading className="p-4 justify-center" size="md">
{title}
</Heading>
<Button tabindex="false" className="mx-4" onClick={() => onShowAll()}>
Show All
</Button>
</div>
{options.map((item) => (
<div className="flex flex-grow" key={item}>
<label
className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}>
className={`flex flex-shrink space-x-2 p-1 my-1 min-w-[176px] hover:bg-gray-200 dark:hover:bg-gray-800 dark:hover:text-white cursor-pointer capitalize text-sm`}
>
<input
className="mx-4 m-0 align-middle"
type="checkbox"
checked={isOptionSelected(item)}
onChange={() => onToggle(item)} />
{item.replaceAll("_", " ")}
onChange={() => onToggle(item)}
/>
{item.replaceAll('_', ' ')}
</label>
<div className="justify-right">
<Button color={isOptionSelected(item) ? "blue" : "black"} type="text" className="max-h-[35px] mx-2" onClick={() => onSelectSingle(item)}>
<Button
color={isOptionSelected(item) ? 'blue' : 'black'}
type="text"
className="max-h-[35px] mx-2"
onClick={() => onSelectSingle(item)}
>
<CameraIcon />
</Button>
</div>
</div>
))}
</Menu>
): null}
) : null}
</div>
);
}

View File

@@ -57,7 +57,7 @@ export default function RelativeModal({
x: relativeToX,
y: relativeToY,
width: relativeToWidth,
// height: relativeToHeight,
height: relativeToHeight,
} = relativeTo.current.getBoundingClientRect();
const _width = widthRelative ? relativeToWidth : menuWidth;
@@ -78,10 +78,13 @@ export default function RelativeModal({
newLeft = windowWidth - width - WINDOW_PADDING;
}
// too close to bottom
if (top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY) {
// If the pop-up modal would extend beyond the bottom of the visible window,
// reposition the modal to appear above the clicked icon instead
// This condition checks if the menu overflows the bottom of the page and
// if there's enough space to position the menu above the clicked icon.
// If both conditions are met, the menu will be positioned above the clicked icon
if (
top + menuHeight > windowHeight - WINDOW_PADDING + window.scrollY &&
top - menuHeight - relativeToHeight >= WINDOW_PADDING
) {
newTop = top - menuHeight;
}
@@ -89,7 +92,13 @@ export default function RelativeModal({
newTop = WINDOW_PADDING;
}
const maxHeight = windowHeight - WINDOW_PADDING * 2 > menuHeight ? null : windowHeight - WINDOW_PADDING * 2;
// This calculation checks if there's enough space below the clicked icon for the menu to fit.
// If there is, it sets the maxHeight to null(meaning no height constraint). If not, it calculates the maxHeight based on the remaining space in the window
const maxHeight =
windowHeight - WINDOW_PADDING * 2 - top > menuHeight
? null
: windowHeight - WINDOW_PADDING * 2 - top + window.scrollY;
const newPosition = { left: newLeft, top: newTop, maxHeight };
if (widthRelative) {
newPosition.width = relativeToWidth;
@@ -115,7 +124,7 @@ export default function RelativeModal({
<div data-testid="scrim" key="scrim" className="fixed inset-0 z-10" onClick={handleDismiss} />
<div
key="menu"
className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-transform transition-opacity duration-75 transform scale-90 opacity-0 overflow-x-hidden overflow-y-auto ${
className={`z-10 bg-white dark:bg-gray-700 dark:text-white absolute shadow-lg rounded w-auto h-auto transition-transform duration-75 transform scale-90 opacity-0 overflow-x-hidden overflow-y-auto ${
show ? 'scale-100 opacity-100' : ''
} ${className}`}
onKeyDown={handleKeydown}

View File

@@ -16,12 +16,12 @@ export default function Cameras() {
<ActivityIndicator />
) : (
<div className="grid grid-cols-1 3xl:grid-cols-3 md:grid-cols-2 gap-4 p-2 px-4">
<SortedCameras unsortedCameras={config.cameras} />
<SortedCameras config={config} unsortedCameras={config.cameras} />
</div>
);
}
function SortedCameras({ unsortedCameras }) {
function SortedCameras({ config, unsortedCameras }) {
const sortedCameras = useMemo(
() =>
Object.entries(unsortedCameras)
@@ -33,13 +33,13 @@ function SortedCameras({ unsortedCameras }) {
return (
<Fragment>
{sortedCameras.map(([camera, conf]) => (
<Camera key={camera} name={camera} conf={conf} />
<Camera key={camera} name={camera} config={config.cameras[camera]} conf={conf} />
))}
</Fragment>
);
}
function Camera({ name }) {
function Camera({ name, config }) {
const { payload: detectValue, send: sendDetect } = useDetectState(name);
const { payload: recordValue, send: sendRecordings } = useRecordingsState(name);
const { payload: snapshotValue, send: sendSnapshots } = useSnapshotsState(name);
@@ -65,11 +65,13 @@ function Camera({ name }) {
},
},
{
name: `Toggle recordings ${recordValue === 'ON' ? 'off' : 'on'}`,
name: config.record.enabled_in_config ? `Toggle recordings ${recordValue === 'ON' ? 'off' : 'on'}` : 'Recordings must be enabled in the config to be turned on in the UI.',
icon: ClipIcon,
color: recordValue === 'ON' ? 'blue' : 'gray',
color: config.record.enabled_in_config ? (recordValue === 'ON' ? 'blue' : 'gray') : 'red',
onClick: () => {
sendRecordings(recordValue === 'ON' ? 'OFF' : 'ON', true);
if (config.record.enabled_in_config) {
sendRecordings(recordValue === 'ON' ? 'OFF' : 'ON', true);
}
},
},
{
@@ -81,7 +83,7 @@ function Camera({ name }) {
},
},
],
[detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
[config, detectValue, sendDetect, recordValue, sendRecordings, snapshotValue, sendSnapshots]
);
return (

View File

@@ -119,7 +119,7 @@ export default function System() {
{state.showFfprobe && (
<Dialog>
<div className="p-4 mb-2 max-h-96 whitespace-pre-line overflow-scroll">
<div className="p-4 mb-2 max-h-96 whitespace-pre-line overflow-auto">
<Heading size="lg">Ffprobe Output</Heading>
{state.ffprobe != '' ? (
<div>
@@ -183,7 +183,7 @@ export default function System() {
{state.showVainfo && (
<Dialog>
<div className="p-4 overflow-scroll whitespace-pre-line">
<div className="p-4 overflow-auto whitespace-pre-line">
<Heading size="lg">Vainfo Output</Heading>
{state.vainfo != '' ? (
<div className="mb-2 max-h-96 whitespace-pre-line">