mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-28 15:47:15 +01:00
[MIRROR] Typescript camera console [MDB IGNORE] (#23795)
* Typescript camera console (#78412) ## About The Pull Request Refactors the camera console into typescript, including the secureye, removing the need for its custom css file   ## Why It's Good For The Game Spruces up the UI ever so slightly and possibly fixes an issue #78009 Less code is better code ## Changelog 🆑 refactor: Refactors the camera console UI. /🆑 * Typescript camera console --------- Co-authored-by: Jeremiah <42397676+jlsnow301@users.noreply.github.com>
This commit is contained in:
@@ -1,137 +0,0 @@
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { flow } from 'common/fp';
|
||||
import { classes } from 'common/react';
|
||||
import { createSearch } from 'common/string';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, ByondUi, Flex, Input, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
/**
|
||||
* Returns previous and next camera names relative to the currently
|
||||
* active camera.
|
||||
*/
|
||||
export const prevNextCamera = (cameras, activeCamera) => {
|
||||
if (!activeCamera) {
|
||||
return [];
|
||||
}
|
||||
const index = cameras.findIndex(
|
||||
(camera) => camera.name === activeCamera.name
|
||||
);
|
||||
return [cameras[index - 1]?.name, cameras[index + 1]?.name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Camera selector.
|
||||
*
|
||||
* Filters cameras, applies search terms and sorts the alphabetically.
|
||||
*/
|
||||
export const selectCameras = (cameras, searchText = '') => {
|
||||
const testSearch = createSearch(searchText, (camera) => camera.name);
|
||||
return flow([
|
||||
// Null camera filter
|
||||
filter((camera) => camera?.name),
|
||||
// Optional search term
|
||||
searchText && filter(testSearch),
|
||||
// Slightly expensive, but way better than sorting in BYOND
|
||||
sortBy((camera) => camera.name),
|
||||
])(cameras);
|
||||
};
|
||||
|
||||
export const CameraConsole = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { mapRef, activeCamera } = data;
|
||||
const cameras = selectCameras(data.cameras);
|
||||
const [prevCameraName, nextCameraName] = prevNextCamera(
|
||||
cameras,
|
||||
activeCamera
|
||||
);
|
||||
return (
|
||||
<Window width={870} height={708}>
|
||||
<div className="CameraConsole__left">
|
||||
<Window.Content scrollable>
|
||||
<CameraConsoleContent />
|
||||
</Window.Content>
|
||||
</div>
|
||||
<div className="CameraConsole__right">
|
||||
<div className="CameraConsole__toolbar">
|
||||
<b>Camera: </b>
|
||||
{(activeCamera && activeCamera.name) || '—'}
|
||||
</div>
|
||||
<div className="CameraConsole__toolbarRight">
|
||||
<Button
|
||||
icon="chevron-left"
|
||||
disabled={!prevCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: prevCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="chevron-right"
|
||||
disabled={!nextCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: nextCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ByondUi
|
||||
className="CameraConsole__map"
|
||||
params={{
|
||||
id: mapRef,
|
||||
type: 'map',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const CameraConsoleContent = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
|
||||
const { activeCamera } = data;
|
||||
const cameras = selectCameras(data.cameras, searchText);
|
||||
return (
|
||||
<Flex direction={'column'} height="100%">
|
||||
<Flex.Item>
|
||||
<Input
|
||||
autoFocus
|
||||
fluid
|
||||
mt={1}
|
||||
placeholder="Search for a camera"
|
||||
onInput={(e, value) => setSearchText(value)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item height="100%">
|
||||
<Section fill scrollable>
|
||||
{cameras.map((camera) => (
|
||||
// We're not using the component here because performance
|
||||
// would be absolutely abysmal (50+ ms for each re-render).
|
||||
<div
|
||||
key={camera.name}
|
||||
title={camera.name}
|
||||
className={classes([
|
||||
'Button',
|
||||
'Button--fluid',
|
||||
'Button--color--transparent',
|
||||
'Button--ellipsis',
|
||||
activeCamera &&
|
||||
camera.name === activeCamera.name &&
|
||||
'Button--selected',
|
||||
])}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: camera.name,
|
||||
})
|
||||
}>
|
||||
{camera.name}
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { flow } from 'common/fp';
|
||||
import { BooleanLike, classes } from 'common/react';
|
||||
import { createSearch } from 'common/string';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, ByondUi, Input, NoticeBox, Section, Stack } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
can_spy: BooleanLike;
|
||||
mapRef: string;
|
||||
cameras: Camera[];
|
||||
activeCamera: Camera & { status: BooleanLike };
|
||||
network: string[];
|
||||
};
|
||||
|
||||
type Camera = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns previous and next camera names relative to the currently
|
||||
* active camera.
|
||||
*/
|
||||
const prevNextCamera = (
|
||||
cameras: Camera[],
|
||||
activeCamera: Camera & { status: BooleanLike }
|
||||
) => {
|
||||
if (!activeCamera) {
|
||||
return [];
|
||||
}
|
||||
const index = cameras.findIndex(
|
||||
(camera) => camera?.name === activeCamera.name
|
||||
);
|
||||
return [cameras[index - 1]?.name, cameras[index + 1]?.name];
|
||||
};
|
||||
|
||||
/**
|
||||
* Camera selector.
|
||||
*
|
||||
* Filters cameras, applies search terms and sorts the alphabetically.
|
||||
*/
|
||||
const selectCameras = (cameras: Camera[], searchText = ''): Camera[] => {
|
||||
const testSearch = createSearch(searchText, (camera: Camera) => camera.name);
|
||||
|
||||
return flow([
|
||||
// Null camera filter
|
||||
filter((camera: Camera) => !!camera?.name),
|
||||
// Optional search term
|
||||
searchText && filter(testSearch),
|
||||
// Slightly expensive, but way better than sorting in BYOND
|
||||
sortBy((camera: Camera) => camera.name),
|
||||
])(cameras);
|
||||
};
|
||||
|
||||
export const CameraConsole = (props, context) => {
|
||||
return (
|
||||
<Window width={850} height={708}>
|
||||
<Window.Content>
|
||||
<CameraContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const CameraContent = (props, context) => {
|
||||
return (
|
||||
<Stack fill>
|
||||
<Stack.Item grow>
|
||||
<CameraSelector />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow={3}>
|
||||
<CameraControls />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const CameraSelector = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
const [searchText, setSearchText] = useLocalState(context, 'searchText', '');
|
||||
const { activeCamera } = data;
|
||||
const cameras = selectCameras(data.cameras, searchText);
|
||||
|
||||
return (
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Input
|
||||
autoFocus
|
||||
fluid
|
||||
mt={1}
|
||||
placeholder="Search for a camera"
|
||||
onInput={(e, value) => setSearchText(value)}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<Section fill scrollable>
|
||||
{cameras.map((camera) => (
|
||||
// We're not using the component here because performance
|
||||
// would be absolutely abysmal (50+ ms for each re-render).
|
||||
<div
|
||||
key={camera.name}
|
||||
title={camera.name}
|
||||
className={classes([
|
||||
'candystripe',
|
||||
'Button',
|
||||
'Button--fluid',
|
||||
'Button--color--transparent',
|
||||
'Button--ellipsis',
|
||||
activeCamera &&
|
||||
camera.name === activeCamera.name &&
|
||||
'Button--selected',
|
||||
])}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: camera.name,
|
||||
})
|
||||
}>
|
||||
{camera.name}
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const CameraControls = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
const { activeCamera, can_spy, mapRef } = data;
|
||||
const cameras = selectCameras(data.cameras);
|
||||
|
||||
const [prevCameraName, nextCameraName] = prevNextCamera(
|
||||
cameras,
|
||||
activeCamera
|
||||
);
|
||||
|
||||
return (
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item>
|
||||
<Stack fill>
|
||||
<Stack.Item grow>
|
||||
{activeCamera?.name ? (
|
||||
<NoticeBox info>{activeCamera.name}</NoticeBox>
|
||||
) : (
|
||||
<NoticeBox danger>No input signal</NoticeBox>
|
||||
)}
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
{!!can_spy && (
|
||||
<Button
|
||||
icon="magnifying-glass"
|
||||
tooltip="Track Person"
|
||||
onClick={() => act('start_tracking')}
|
||||
/>
|
||||
)}
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="chevron-left"
|
||||
disabled={!prevCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: prevCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Button
|
||||
icon="chevron-right"
|
||||
disabled={!nextCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: nextCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<ByondUi
|
||||
height="100%"
|
||||
width="100%"
|
||||
params={{
|
||||
id: mapRef,
|
||||
type: 'map',
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -1,82 +1,12 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, ByondUi } from '../components';
|
||||
import { NtosWindow } from '../layouts';
|
||||
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';
|
||||
|
||||
type Data = {
|
||||
mapRef: string;
|
||||
can_spy: boolean;
|
||||
activeCamera: Camera;
|
||||
cameras: Camera[];
|
||||
};
|
||||
|
||||
type Camera = {
|
||||
name: string;
|
||||
};
|
||||
import { CameraContent } from './CameraConsole';
|
||||
|
||||
export const NtosSecurEye = (props, context) => {
|
||||
return (
|
||||
<NtosWindow width={800} height={600}>
|
||||
<NtosWindow.Content>
|
||||
<div className="CameraConsole__left">
|
||||
<CameraConsoleContent />
|
||||
</div>
|
||||
<CameraControls />
|
||||
<CameraContent />
|
||||
</NtosWindow.Content>
|
||||
</NtosWindow>
|
||||
);
|
||||
};
|
||||
|
||||
/** Displays info and controls for the current camera */
|
||||
const CameraControls = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
const { can_spy, activeCamera, mapRef } = data;
|
||||
const cameras = selectCameras(data.cameras);
|
||||
const [prevCameraName, nextCameraName] = prevNextCamera(
|
||||
cameras,
|
||||
activeCamera
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="CameraConsole__right">
|
||||
<div className="CameraConsole__toolbar">
|
||||
<b>Camera: </b>
|
||||
{(activeCamera && activeCamera.name) || '—'}
|
||||
</div>
|
||||
<div className="CameraConsole__toolbarRight">
|
||||
{can_spy && (
|
||||
<Button
|
||||
icon="magnifying-glass"
|
||||
tooltip="Track Person"
|
||||
onClick={() => act('start_tracking')}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon="chevron-left"
|
||||
disabled={!prevCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: prevCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="chevron-right"
|
||||
disabled={!nextCameraName}
|
||||
onClick={() =>
|
||||
act('switch_camera', {
|
||||
name: nextCameraName,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ByondUi
|
||||
className="CameraConsole__map"
|
||||
params={{
|
||||
id: mapRef,
|
||||
type: 'map',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
@use '../base.scss';
|
||||
|
||||
$background-color: rgba(0, 0, 0, 0.33) !default;
|
||||
|
||||
.CameraConsole__left {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: base.em(220px);
|
||||
}
|
||||
|
||||
.CameraConsole__right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: base.em(220px);
|
||||
right: 0;
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.CameraConsole__toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
margin: 0.25em 1em 0;
|
||||
}
|
||||
|
||||
.CameraConsole__toolbarRight {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
margin: 0.33em 0.5em 0;
|
||||
}
|
||||
|
||||
.CameraConsole__map {
|
||||
position: absolute;
|
||||
top: base.em(26px);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0.5em;
|
||||
text-align: center;
|
||||
|
||||
.NoticeBox {
|
||||
margin-top: calc(50% - 2em);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@
|
||||
|
||||
// Interfaces
|
||||
@include meta.load-css('./interfaces/AlertModal.scss');
|
||||
@include meta.load-css('./interfaces/CameraConsole.scss');
|
||||
@include meta.load-css('./interfaces/Changelog.scss');
|
||||
@include meta.load-css('./interfaces/CrewManifest.scss');
|
||||
@include meta.load-css('./interfaces/Emojipedia.scss');
|
||||
|
||||
Reference in New Issue
Block a user