[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

![KaBYZCTqG1](https://github.com/tgstation/tgstation/assets/42397676/4de52792-943c-4b2d-94e5-8ff10b678a91)

![Screenshot 2023-09-17
120550](https://github.com/tgstation/tgstation/assets/42397676/a5370678-ffc0-44b4-94b7-f3de95d8286e)
## 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:
SkyratBot
2023-09-18 23:46:59 -07:00
committed by GitHub
co-authored by Jeremiah
parent f6c9153ece
commit 588d08df5a
5 changed files with 201 additions and 263 deletions
@@ -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>
);
};
+2 -72
View File
@@ -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);
}
}
-1
View File
@@ -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');