Files
CHOMPStation2/tgui/packages/tgui_ch/interfaces/Communicator.tsx
2023-07-18 08:15:10 +02:00

1320 lines
35 KiB
TypeScript

import { filter } from 'common/collections';
import { BooleanLike } from 'common/react';
import { decodeHtmlEntities, toTitleCase } from 'common/string';
import { Fragment } from 'inferno';
import { useBackend, useLocalState } from '../backend';
import { Box, ByondUi, Button, Flex, Icon, LabeledList, Input, Section, Table } from '../components';
import { Window } from '../layouts';
import { CrewManifestContent } from './CrewManifest';
const HOMETAB = 1;
const PHONTAB = 2;
const CONTTAB = 3;
const MESSTAB = 4;
const MESSSUBTAB = 40;
const NEWSTAB = 5;
const NOTETAB = 6;
const WTHRTAB = 7;
const MANITAB = 8;
const SETTTAB = 9;
let TabToTemplate = {}; // Populated under each template
type Data = {
// GENERAL
currentTab: number;
video_comm: BooleanLike;
mapRef: string;
// FOOTER
time: string;
connectionStatus: BooleanLike;
owner: string;
occupation: string;
// HEADER
flashlight: BooleanLike;
// NOTIFICATIONS
voice_mobs: [];
communicating: [];
requestsReceived: [];
invitesSent: [];
};
export const Communicator = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { currentTab, video_comm, mapRef } = data;
/* 0: Fullscreen Video
* 1: Popup Video
* 2: Minimized Video
*/
const [videoSetting, setVideoSetting] = useLocalState(
context,
'videoSetting',
0
);
return (
<Window width={475} height={700} resizable>
<Window.Content>
{video_comm && (
<VideoComm
videoSetting={videoSetting}
setVideoSetting={setVideoSetting}
/>
)}
{(!video_comm || videoSetting !== 0) && (
<Fragment>
<CommunicatorHeader />
<Box
height="88%"
mb={1}
style={{
'overflow-y': 'auto',
}}>
{TabToTemplate[currentTab] || <TemplateError />}
</Box>
<CommunicatorFooter
videoSetting={videoSetting}
setVideoSetting={setVideoSetting}
/>
</Fragment>
)}
</Window.Content>
</Window>
);
};
const VideoComm = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { video_comm, mapRef } = data;
const { videoSetting, setVideoSetting } = props;
if (videoSetting === 0) {
return (
<Box width="100%" height="100%">
<ByondUi
width="100%"
height="95%"
params={{
id: mapRef,
type: 'map',
}}
/>
<Flex justify="space-between" spacing={1} mt={0.5}>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
icon="window-minimize"
onClick={() => setVideoSetting(1)}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
color="bad"
icon="video-slash"
onClick={() => act('endvideo')}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
color="bad"
icon="phone-slash"
onClick={() => act('hang_up')}
/>
</Flex.Item>
</Flex>
</Box>
);
} else if (videoSetting === 1) {
return (
<Box
style={{
'position': 'absolute',
'right': '5px',
'bottom': '50px',
'z-index': 1,
}}>
<Section p={0} m={0}>
<Flex justify="space-between" spacing={1}>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
icon="window-minimize"
onClick={() => setVideoSetting(2)}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
icon="window-maximize"
onClick={() => setVideoSetting(0)}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
color="bad"
icon="video-slash"
onClick={() => act('endvideo')}
/>
</Flex.Item>
<Flex.Item grow={1}>
<Button
textAlign="center"
fluid
fontSize={1.5}
color="bad"
icon="phone-slash"
onClick={() => act('hang_up')}
/>
</Flex.Item>
</Flex>
</Section>
<ByondUi
width="200px"
height="200px"
params={{
id: mapRef,
type: 'map',
}}
/>
</Box>
);
}
return null;
};
const TemplateError = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { currentTab } = data;
return (
<Section title="Error!">
You tried to access tab #{currentTab}, but there was no template defined!
</Section>
);
};
const CommunicatorHeader = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { time, connectionStatus, owner, occupation } = data;
return (
<Section>
<Flex align="center" justify="space-between">
<Flex.Item color="average">{time}</Flex.Item>
<Flex.Item>
<Icon
color={connectionStatus === 1 ? 'good' : 'bad'}
name={connectionStatus === 1 ? 'signal' : 'exclamation-triangle'}
/>
</Flex.Item>
<Flex.Item color="average">{decodeHtmlEntities(owner)}</Flex.Item>
<Flex.Item color="average">{decodeHtmlEntities(occupation)}</Flex.Item>
</Flex>
</Section>
);
};
const CommunicatorFooter = (props, context) => {
const { act, data } = useBackend<Data>(context);
const { flashlight } = data;
const { videoSetting, setVideoSetting } = props;
return (
<Flex>
<Flex.Item basis={videoSetting === 2 ? '60%' : '80%'}>
<Button
p={1}
fluid
icon="home"
iconSize={2}
textAlign="center"
onClick={() => act('switch_tab', { switch_tab: HOMETAB })}
/>
</Flex.Item>
<Flex.Item basis="20%">
<Button
icon="lightbulb"
iconSize={2}
p={1}
fluid
textAlign="center"
selected={flashlight}
tooltip="Flashlight"
tooltipPosition="top"
onClick={() => act('Light')}
/>
</Flex.Item>
{videoSetting === 2 && (
<Flex.Item basis="20%">
<Button
icon="video"
iconSize={2}
p={1}
fluid
textAlign="center"
tooltip="Open Video"
tooltipPosition="top"
onClick={() => setVideoSetting(1)}
/>
</Flex.Item>
)}
</Flex>
);
};
/* Helper for notifications (yes this is a mess, but whatever, it works) */
const hasNotifications = (app, context) => {
const { data } = useBackend<Data>(context);
const {
/* Phone Notifications */
voice_mobs,
communicating,
requestsReceived,
invitesSent,
video_comm,
} = data;
if (app === 'Phone') {
if (
voice_mobs.length ||
communicating.length ||
requestsReceived.length ||
invitesSent.length ||
video_comm
) {
return true;
}
}
return false;
};
/* Tabs Below this point */
type HomeTabData = {
homeScreen: { number: number; module: string; icon: string }[];
};
/* Home tab, provides access to all other tabs. */
const HomeTab = (props, context) => {
const { act, data } = useBackend<HomeTabData>(context);
const { homeScreen } = data;
return (
<Flex mt={2} wrap="wrap" align="center" justify="center">
{homeScreen.map((app) => (
<Flex.Item basis="25%" textAlign="center" mb={2} key={app.number}>
<Button
style={{
'border-radius': '10%',
'border': '1px solid #000',
}}
width="64px"
height="64px"
position="relative"
onClick={() => act('switch_tab', { switch_tab: app.number })}>
<Icon
spin={hasNotifications(app.module, context)}
color={hasNotifications(app.module, context) ? 'bad' : null}
name={app.icon}
position="absolute"
size={3}
top="25%"
left="25%"
/>
</Button>
<Box>{app.module}</Box>
</Flex.Item>
))}
</Flex>
);
};
TabToTemplate[HOMETAB] = <HomeTab />;
type PhoneTabData = {
targetAddress: string;
voice_mobs: { name: string; true_name: string; ref: string }[];
communicating: {
address: string;
name: string;
true_name: string;
ref: string;
}[];
requestsReceived: { address: string; name: string; ref: string }[];
invitesSent: { address: string; name: string }[];
video_comm: string;
selfie_mode: BooleanLike;
};
/* Phone tab, provides a phone interface! */
const PhoneTab = (props, context) => {
const { act, data } = useBackend<PhoneTabData>(context);
const {
targetAddress,
voice_mobs,
communicating,
requestsReceived,
invitesSent,
video_comm,
selfie_mode,
} = data;
return (
<Section title="Phone">
<LabeledList>
<LabeledList.Item label="Target EPv2 Address" verticalAlign="middle">
<Flex align="center">
<Flex.Item grow={1}>
<Input
fluid
value={targetAddress}
onInput={(e, val) => act('write_target_address', { val: val })}
/>
</Flex.Item>
<Flex.Item>
<Button
icon="times"
onClick={() => act('clear_target_address')}
/>
</Flex.Item>
</Flex>
</LabeledList.Item>
</LabeledList>
<NumberPad />
<Section title="Connection Management" mt={2}>
<LabeledList>
<LabeledList.Item label="Camera Mode">
<Button
fluid
content={
selfie_mode ? 'Front-facing Camera' : 'Rear-facing Camera'
}
onClick={() => act('selfie_mode')}
/>
</LabeledList.Item>
</LabeledList>
<Section title="External Connections">
{(!!voice_mobs.length && (
<LabeledList>
{voice_mobs.map((mob) => (
<LabeledList.Item
label={decodeHtmlEntities(mob.name)}
key={mob.ref}>
<Button
icon="times"
color="bad"
content="Disconnect"
onClick={() =>
act('disconnect', { disconnect: mob.true_name })
}
/>
</LabeledList.Item>
))}
</LabeledList>
)) || <Box>No connections</Box>}
</Section>
<Section title="Internal Connections">
{(!!communicating.length && (
<Table>
{communicating.map((comm) => (
<Table.Row key={comm.address}>
<Table.Cell color="label">
{decodeHtmlEntities(comm.name)}
</Table.Cell>
<Table.Cell>
<Button
icon="times"
color="bad"
content="Disconnect"
onClick={() =>
act('disconnect', { disconnect: comm.true_name })
}
/>
{(video_comm === null && (
<Button
icon="camera"
content="Start Video"
onClick={() =>
act('startvideo', { startvideo: comm.ref })
}
/>
)) ||
(video_comm === comm.ref && (
<Button
icon="times"
color="bad"
content="Stop Video"
onClick={() =>
act('endvideo', { endvideo: comm.true_name })
}
/>
))}
</Table.Cell>
</Table.Row>
))}
</Table>
)) || <Box>No connections</Box>}
</Section>
<Section title="Requests Received">
{(!!requestsReceived.length && (
<LabeledList>
{requestsReceived.map((request) => (
<LabeledList.Item
label={decodeHtmlEntities(request.name)}
key={request.address}>
<Box>{decodeHtmlEntities(request.address)}</Box>
<Box>
<Button
icon="signal"
content="Accept"
onClick={() => act('dial', { dial: request.address })}
/>
<Button
icon="times"
content="Decline"
onClick={() => act('decline', { decline: request.ref })}
/>
</Box>
</LabeledList.Item>
))}
</LabeledList>
)) || <Box>No requests received.</Box>}
</Section>
<Section title="Invites Sent">
{(!!invitesSent.length && (
<LabeledList>
{invitesSent.map((invite) => (
<LabeledList.Item
label={decodeHtmlEntities(invite.name)}
key={invite.address}>
<Box>{decodeHtmlEntities(invite.address)}</Box>
<Box>
<Button
icon="pen"
onClick={() => {
act('copy', { 'copy': invite.address });
}}
content="Copy"
/>
</Box>
</LabeledList.Item>
))}
</LabeledList>
)) || <Box>No invites sent.</Box>}
</Section>
</Section>
</Section>
);
};
// Subtemplate
const NumberPad = (props, context) => {
const { act, data } = useBackend<PhoneTabData>(context);
const { targetAddress } = data;
const validCharacters = [
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'A',
'B',
'C',
'D',
'E',
'F',
];
let buttonArray = validCharacters.map((char) => (
<Button
key={char}
content={char}
fontSize={2}
fluid
onClick={() => act('add_hex', { add_hex: char })}
/>
));
let finalArray: any[] = [];
for (let i = 0; i < buttonArray.length; i += 4) {
finalArray.push(
<Table.Row>
<Table.Cell>{buttonArray[i]}</Table.Cell>
<Table.Cell>{buttonArray[i + 1]}</Table.Cell>
<Table.Cell>{buttonArray[i + 2]}</Table.Cell>
<Table.Cell>{buttonArray[i + 3]}</Table.Cell>
</Table.Row>
);
}
return (
<Flex align="center" justify="center" mt={1}>
<Flex.Item>
<Table>{finalArray}</Table>
<Flex width="100%" justify="space-between">
{/* Dial */}
<Flex.Item basis="33%">
<Button
width="100%"
height="64px"
position="relative"
onClick={() => act('dial', { dial: targetAddress })}>
<Icon
name="phone"
position="absolute"
size={3}
top="25%"
left="25%"
/>
</Button>
<Box textAlign="center">Dial</Box>
</Flex.Item>
{/* Message */}
<Flex.Item basis="33%">
<Button
width="100%"
height="64px"
position="relative"
onClick={() => {
act('message', { message: targetAddress });
act('switch_tab', { switch_tab: MESSTAB });
}}>
<Icon
name="comment-alt"
position="absolute"
size={3}
top="25%"
left="25%"
/>
</Button>
<Box textAlign="center">Message</Box>
</Flex.Item>
{/* Hang Up */}
<Flex.Item basis="33%">
<Button
width="100%"
height="64px"
position="relative"
onClick={() => act('hang_up')}>
<Icon
name="times"
position="absolute"
size={3}
top="25%"
left="25%"
/>
</Button>
<Box textAlign="center">Hang Up</Box>
</Flex.Item>
</Flex>
</Flex.Item>
</Flex>
);
};
TabToTemplate[PHONTAB] = <PhoneTab />;
type ContactsTabData = {
knownDevices: { address: string; name: string }[];
};
/* Contacts */
const ContactsTab = (props, context) => {
const { act, data } = useBackend<ContactsTabData>(context);
const { knownDevices } = data;
return (
<Section title="Known Devices">
{(knownDevices.length && (
<Table>
{knownDevices.map((device) => (
<Table.Row key={device.address}>
<Table.Cell
color="label"
style={{
'word-break': 'break-all',
}}>
{decodeHtmlEntities(device.name)}
</Table.Cell>
<Table.Cell>
<Box>{device.address}</Box>
<Box>
<Button
icon="pen"
onClick={() => {
act('copy', { 'copy': device.address });
act('switch_tab', { switch_tab: PHONTAB });
}}
content="Copy"
/>
<Button
icon="phone"
onClick={() => {
act('dial', { 'dial': device.address });
act('copy', { 'copy': device.address });
act('switch_tab', { switch_tab: PHONTAB });
}}
content="Call"
/>
<Button
icon="comment-alt"
onClick={() => {
act('copy', { 'copy': device.address });
act('copy_name', { 'copy_name': device.name });
act('switch_tab', { switch_tab: MESSSUBTAB });
}}
content="Msg"
/>
</Box>
</Table.Cell>
</Table.Row>
))}
</Table>
)) || <Box>No devices detected on your local NTNet region.</Box>}
</Section>
);
};
TabToTemplate[CONTTAB] = <ContactsTab />;
type MessagingTabData = {
// TAB
imContacts: { address: string; name: string }[];
// THREAD
targetAddressName: string;
targetAddress: string;
imList: { address: string; to_address: string; im: string }[];
};
/* Messaging */
const MessagingTab = (props, context) => {
const { act, data } = useBackend<MessagingTabData>(context);
const { imContacts } = data;
return (
<Section title="Messaging">
{(imContacts.length && (
<Table>
{imContacts.map((device) => (
<Table.Row key={device.address}>
<Table.Cell
color="label"
style={{
'word-break': 'break-all',
}}>
{decodeHtmlEntities(device.name)}:
</Table.Cell>
<Table.Cell>
<Box>{device.address}</Box>
<Box>
<Button
icon="comment"
onClick={() => {
act('copy', { copy: device.address });
act('copy_name', { copy_name: device.name });
act('switch_tab', { switch_tab: MESSSUBTAB });
}}
content="View Conversation"
/>
</Box>
</Table.Cell>
</Table.Row>
))}
</Table>
)) || (
<Box>
You haven&apos;t sent any messages yet.
<Button
fluid
icon="user"
onClick={() => act('switch_tab', { switch_tab: CONTTAB })}
content="Contacts"
/>
</Box>
)}
</Section>
);
};
TabToTemplate[MESSTAB] = <MessagingTab />;
/* Actual messaging conversation */
const IsIMOurs = (im, targetAddress) => {
return im.address !== targetAddress;
};
const enforceLengthLimit = (prefix: string, name: string, length: number) => {
if ((prefix + name).length > length) {
if (name.length > length) {
return name.slice(0, length) + '...';
}
return name;
}
return prefix + name;
};
const findClassMessage = (im, targetAddress, lastIndex, filterArray) => {
if (lastIndex < 0 || lastIndex > filterArray.length) {
return IsIMOurs(im, targetAddress)
? 'TinderMessage_First_Sent'
: 'TinderMessage_First_Received';
}
let thisSent = IsIMOurs(im, targetAddress);
let lastSent = IsIMOurs(filterArray[lastIndex], targetAddress);
if (thisSent && lastSent) {
return 'TinderMessage_Subsequent_Sent';
} else if (!thisSent && !lastSent) {
return 'TinderMessage_Subsequent_Received';
}
return thisSent ? 'TinderMessage_First_Sent' : 'TinderMessage_First_Received';
};
const MessagingThreadTab = (props, context) => {
const { act, data } = useBackend<MessagingTabData>(context);
const { targetAddressName, targetAddress, imList } = data;
const [clipboardMode, setClipboardMode] = useLocalState(
context,
'clipboardMode',
false
);
if (clipboardMode) {
return (
<Section
title={
<Box
inline
style={{
'white-space': 'nowrap',
'overflow-x': 'hidden',
}}
width="90%">
{enforceLengthLimit(
'Conversation with ',
decodeHtmlEntities(targetAddressName),
30
)}
</Box>
}
buttons={
<Button
icon="eye"
selected={clipboardMode}
tooltip="Exit Clipboard Mode"
tooltipPosition="bottom-end"
onClick={() => setClipboardMode(!clipboardMode)}
/>
}
height="100%"
stretchContents>
<Section
style={{
'height': '95%',
'overflow-y': 'auto',
}}>
{imList.map(
(im, i) =>
(im.to_address === targetAddress ||
im.address === targetAddress) && (
<Box
key={i}
className={
IsIMOurs(im, targetAddress)
? 'ClassicMessage_Sent'
: 'ClassicMessage_Received'
}>
{IsIMOurs(im, targetAddress) ? 'You' : 'Them'}: {im.im}
</Box>
)
)}
</Section>
<Button
icon="comment"
onClick={() => act('message', { 'message': targetAddress })}
content="Message"
/>
</Section>
);
}
return (
<Section
title={
<Box
inline
style={{
'white-space': 'nowrap',
'overflow-x': 'hidden',
}}
width="100%">
{enforceLengthLimit(
'Conversation with ',
decodeHtmlEntities(targetAddressName),
30
)}
</Box>
}
buttons={
<Button
icon="eye"
selected={clipboardMode}
tooltip="Enter Clipboard Mode"
tooltipPosition="bottom-end"
onClick={() => setClipboardMode(!clipboardMode)}
/>
}
height="100%"
stretchContents>
<Section
style={{
'height': '95%',
'overflow-y': 'auto',
}}>
{imList.map(
(im, i, filterArr) =>
(im.to_address === targetAddress ||
im.address === targetAddress) && (
<Box
textAlign={IsIMOurs(im, targetAddress) ? 'right' : 'left'}
mb={1}
key={i}>
<Box
maxWidth="75%"
className={findClassMessage(
im,
targetAddress,
i - 1,
filterArr
)}
inline>
{decodeHtmlEntities(im.im)}
</Box>
</Box>
)
)}
</Section>
<Button
icon="comment"
onClick={() => act('message', { 'message': targetAddress })}
content="Message"
/>
</Section>
);
};
TabToTemplate[MESSSUBTAB] = <MessagingThreadTab />;
type NewsTabData = {
feeds: { index: number; name: string }[];
target_feed: { name: string; author: string; messages: NewsMessage[] };
latest_news: NewsMessage[];
};
type NewsMessage = {
ref: string;
body: string;
img: string;
caption: string;
message_type: string;
author: string;
time_stamp: string;
index: number;
channel: string;
};
/* News */
const NewsTab = (props, context) => {
const { act, data } = useBackend<NewsTabData>(context);
const { feeds, target_feed } = data;
return (
<Section title="News" stretchContents height="100%">
{(!feeds.length && (
<Box color="bad">
Error: No newsfeeds available. Please try again later.
</Box>
)) ||
(target_feed && <NewsTargetFeed />) || <NewsFeed />}
</Section>
);
};
const NewsTargetFeed = (props, context) => {
const { act, data } = useBackend<NewsTabData>(context);
const { target_feed } = data;
return (
<Section
title={
decodeHtmlEntities(target_feed.name) +
' by ' +
decodeHtmlEntities(target_feed.author)
}
buttons={
<Button
content="Back"
icon="chevron-up"
onClick={() => act('newsfeed', { newsfeed: null })}
/>
}>
{target_feed.messages.map((message) => (
<Section key={message.ref}>
- {decodeHtmlEntities(message.body)}
{!!message.img && (
<Box>
<img src={'data:image/png;base64,' + message.img} />
{decodeHtmlEntities(message.caption) || null}
</Box>
)}
<Box color="grey">
[{message.message_type} by {decodeHtmlEntities(message.author)} -{' '}
{message.time_stamp}]
</Box>
</Section>
))}
</Section>
);
};
const NewsFeed = (props, context) => {
const { act, data } = useBackend<NewsTabData>(context);
const { feeds, latest_news } = data;
return (
<Fragment>
<Section title="Recent News">
<Section>
{latest_news.map((news) => (
<Box mb={2} key={news.index}>
<h5>
{decodeHtmlEntities(news.channel)}
<Button
ml={1}
icon="chevron-up"
onClick={() => act('newsfeed', { newsfeed: news.index })}
content="Go to"
/>
</h5>
- {decodeHtmlEntities(news.body)}
{!!news.img && (
<Box>
[image omitted, view story for more details]
{news.caption || null}
</Box>
)}
<Box fontSize={0.9}>
[{news.message_type} by{' '}
<Box inline color="average">
{news.author}
</Box>{' '}
- {news.time_stamp}]
</Box>
</Box>
))}
</Section>
</Section>
<Section title="News Feeds">
{feeds.map((feed) => (
<Button
key={feed.index}
fluid
icon="chevron-up"
onClick={() => act('newsfeed', { newsfeed: feed.index })}
content={feed.name}
/>
))}
</Section>
</Fragment>
);
};
TabToTemplate[NEWSTAB] = <NewsTab />;
type NoteTabData = {
note: string;
};
/* Note Keeper */
const NoteTab = (props, context) => {
const { act, data } = useBackend<NoteTabData>(context);
const { note } = data;
return (
<Section
title="Note Keeper"
height="100%"
stretchContents
buttons={
<Button icon="pen" onClick={() => act('edit')} content="Edit Notes" />
}>
<Section
color="average"
width="100%"
height="100%"
style={{
'word-break': 'break-all',
'overflow-y': 'auto',
}}>
{note}
</Section>
</Section>
);
};
TabToTemplate[NOTETAB] = <NoteTab />;
/* Weather App */
const getItemColor = (value, min2, min1, max1, max2) => {
if (value < min2) {
return 'bad';
} else if (value < min1) {
return 'average';
} else if (value > max1) {
return 'average';
} else if (value > max2) {
return 'bad';
}
return 'good';
};
type WeatherTabData = {
aircontents: AirContent[];
weather: Weather[];
};
type AirContent = {
entry: string;
val;
bad_low: number;
poor_low: number;
poor_high: number;
bad_high: number;
units;
};
type Weather = {
Planet: string;
Time: string;
Weather: string;
Temperature;
High;
Low;
WindDir;
WindSpeed;
Forecast: string;
};
const WeatherTab = (props, context) => {
const { act, data } = useBackend<WeatherTabData>(context);
const { aircontents, weather } = data;
let deg = '\u00B0';
return (
<Section title="Weather">
<Section title="Current Conditions">
<LabeledList>
{filter(
(i: AirContent) =>
i.val !== '0' ||
i.entry === 'Pressure' ||
i.entry === 'Temperature'
)(aircontents).map((item: AirContent) => (
<LabeledList.Item
key={item.entry}
label={item.entry}
color={getItemColor(
item.val,
item.bad_low,
item.poor_low,
item.poor_high,
item.bad_high
)}>
{item.val}
{decodeHtmlEntities(item.units)}
</LabeledList.Item>
))}
</LabeledList>
</Section>
<Section title="Weather Reports">
{(!!weather.length && (
<LabeledList>
{weather.map((wr) => (
<LabeledList.Item label={wr.Planet} key={wr.Planet}>
<LabeledList>
<LabeledList.Item label="Time">{wr.Time}</LabeledList.Item>
<LabeledList.Item label="Weather">
{toTitleCase(wr.Weather)}
</LabeledList.Item>
<LabeledList.Item label="Temperature">
Current: {wr.Temperature.toFixed()} {deg}C | High:{' '}
{wr.High.toFixed()} {deg}C | Low: {wr.Low.toFixed()} {deg}C
</LabeledList.Item>
<LabeledList.Item label="Wind Direction">
{wr.WindDir}
</LabeledList.Item>
<LabeledList.Item label="Wind Speed">
{wr.WindSpeed}
</LabeledList.Item>
<LabeledList.Item label="Forecast">
{decodeHtmlEntities(wr.Forecast)}
</LabeledList.Item>
</LabeledList>
</LabeledList.Item>
))}
</LabeledList>
)) || (
<Box color="bad">
No weather reports available. Please check back later.
</Box>
)}
</Section>
</Section>
);
};
TabToTemplate[WTHRTAB] = <WeatherTab />;
/* Crew Manifest */
// Lol just steal it from the existing template
TabToTemplate[MANITAB] = <CrewManifestContent />;
type SettingsTabData = {
owner: string;
occupation: string;
connectionStatus: BooleanLike;
address: string;
visible: BooleanLike;
ring: BooleanLike;
selfie_mode: BooleanLike;
};
/* Settings */
const SettingsTab = (props, context) => {
const { act, data } = useBackend<SettingsTabData>(context);
const {
owner,
occupation,
connectionStatus,
address,
visible,
ring,
selfie_mode,
} = data;
return (
<Section title="Settings">
<LabeledList>
<LabeledList.Item label="Owner">
<Button
icon="pen"
fluid
content={decodeHtmlEntities(owner)}
onClick={() => act('rename')}
/>
</LabeledList.Item>
<LabeledList.Item label="Camera Mode">
<Button
fluid
content={selfie_mode ? 'Front-facing Camera' : 'Rear-facing Camera'}
onClick={() => act('selfie_mode')}
/>
</LabeledList.Item>
<LabeledList.Item label="Occupation">
{decodeHtmlEntities(occupation)}
</LabeledList.Item>
<LabeledList.Item label="Connection">
{connectionStatus === 1 ? (
<Box color="good">Connected</Box>
) : (
<Box color="bad">Disconnected</Box>
)}
</LabeledList.Item>
<LabeledList.Item label="Device EPv2 Address">
{address}
</LabeledList.Item>
<LabeledList.Item label="Visibility">
<Button.Checkbox
checked={visible}
selected={visible}
fluid
content={
visible
? 'This device can be seen by other devices.'
: 'This device is invisible to other devices.'
}
onClick={() => act('toggle_visibility')}
/>
</LabeledList.Item>
<LabeledList.Item label="Ringer">
<Button.Checkbox
checked={ring}
selected={ring}
fluid
content={ring ? 'Ringer on.' : 'Ringer off.'}
onClick={() => act('toggle_ringer')}
/>
<Button
fluid
content="Set Ringer Tone"
onClick={() => act('set_ringer_tone')}
/>
</LabeledList.Item>
</LabeledList>
</Section>
);
};
TabToTemplate[SETTTAB] = <SettingsTab />;