mirror of
https://github.com/yogstation13/Yogstation.git
synced 2025-02-26 09:04:50 +00:00
143
tgui/packages/tgui/interfaces/CameraConsole.js
Normal file
143
tgui/packages/tgui/interfaces/CameraConsole.js
Normal file
@@ -0,0 +1,143 @@
|
||||
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}
|
||||
resizable>
|
||||
<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,
|
||||
}), document.getElementsByClassName('CameraConsole__left')[0].focus()]}>
|
||||
{camera.name}
|
||||
</div>
|
||||
))}
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,198 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
60
tgui/packages/tgui/interfaces/NtosSecurEye.js
Normal file
60
tgui/packages/tgui/interfaces/NtosSecurEye.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { flow } from 'common/fp';
|
||||
import { classes } from 'common/react';
|
||||
import { createSearch } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, ByondUi, Input, Section } from '../components';
|
||||
import { NtosWindow } from '../layouts';
|
||||
import { prevNextCamera, selectCameras, CameraConsoleContent } from './CameraConsole';
|
||||
import { logger } from "../logging";
|
||||
|
||||
export const NtosSecurEye = (props, context) => {
|
||||
const { act, data, config } = useBackend(context);
|
||||
const { PC_device_theme, mapRef, activeCamera } = data;
|
||||
const cameras = selectCameras(data.cameras);
|
||||
const [
|
||||
prevCameraName,
|
||||
nextCameraName,
|
||||
] = prevNextCamera(cameras, activeCamera);
|
||||
return (
|
||||
<NtosWindow
|
||||
width={800}
|
||||
height={600}
|
||||
theme={PC_device_theme}>
|
||||
<NtosWindow.Content>
|
||||
<div className="CameraConsole__left">
|
||||
<CameraConsoleContent />
|
||||
</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>
|
||||
</NtosWindow.Content>
|
||||
</NtosWindow>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NtosWindow } from '../layouts';
|
||||
import { CameraContent } from './CameraConsole';
|
||||
|
||||
export const NtosSecurEye = (props) => {
|
||||
return (
|
||||
<NtosWindow width={800} height={600}>
|
||||
<NtosWindow.Content>
|
||||
<CameraContent />
|
||||
</NtosWindow.Content>
|
||||
</NtosWindow>
|
||||
);
|
||||
};
|
||||
53
tgui/packages/tgui/styles/interfaces/CameraConsole.scss
Normal file
53
tgui/packages/tgui/styles/interfaces/CameraConsole.scss
Normal file
@@ -0,0 +1,53 @@
|
||||
@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,6 +48,7 @@
|
||||
|
||||
// Interfaces
|
||||
@include meta.load-css('./interfaces/AlertModal.scss');
|
||||
@include meta.load-css('./interfaces/CameraConsole.scss');
|
||||
@include meta.load-css('./interfaces/InspectorBooth.scss');
|
||||
@include meta.load-css('./interfaces/ListInput.scss');
|
||||
@include meta.load-css('./interfaces/HellishRunes.scss');
|
||||
|
||||
Reference in New Issue
Block a user