Files
CHOMPStation2/tgui/packages/tgui_ch/interfaces/Communicator.tsx
2023-05-23 17:43:01 +02:00

1086 lines
32 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;
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 />;