mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 02:09:41 +00:00
Delete tgui/packages/tgui_ch (#8006)
This commit is contained in:
@@ -14,6 +14,3 @@
|
||||
**.woff2
|
||||
**.eot
|
||||
**.ttf
|
||||
|
||||
# CHOMPEdit - Until removed
|
||||
/packages/tgui_ch/**
|
||||
|
||||
@@ -18,6 +18,3 @@
|
||||
## Build artifacts
|
||||
/public/.tmp/**/*
|
||||
/public/*.map
|
||||
|
||||
## CHOMPEdit - Until removed
|
||||
/packages/tgui_ch
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Action, AnyAction, Middleware } from '../common/redux';
|
||||
|
||||
import { Dispatch } from 'common/redux';
|
||||
|
||||
const EXCLUDED_PATTERNS = [/v4shim/i];
|
||||
const loadedMappings: Record<string, string> = {};
|
||||
|
||||
export const resolveAsset = (name: string): string =>
|
||||
loadedMappings[name] || name;
|
||||
|
||||
export const assetMiddleware: Middleware =
|
||||
(storeApi) =>
|
||||
<ActionType extends Action = AnyAction>(next: Dispatch<ActionType>) =>
|
||||
(action: ActionType) => {
|
||||
const { type, payload } = action as AnyAction;
|
||||
if (type === 'asset/stylesheet') {
|
||||
Byond.loadCss(payload);
|
||||
return;
|
||||
}
|
||||
if (type === 'asset/mappings') {
|
||||
for (const name of Object.keys(payload)) {
|
||||
// Skip anything that matches excluded patterns
|
||||
if (EXCLUDED_PATTERNS.some((regex) => regex.test(name))) {
|
||||
continue;
|
||||
}
|
||||
const url = payload[name];
|
||||
const ext = name.split('.').pop();
|
||||
loadedMappings[name] = url;
|
||||
if (ext === 'css') {
|
||||
Byond.loadCss(url);
|
||||
}
|
||||
if (ext === 'js') {
|
||||
Byond.loadJs(url);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 425 200" opacity=".33">
|
||||
<path d="m 178.00399,0.03869 -71.20393,0 a 6.7613422,6.0255495 0 0 0 -6.76134,6.02555 l 0,187.87147 a 6.7613422,6.0255495 0 0 0 6.76134,6.02554 l 53.1072,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-101.544018 72.21628,104.699398 a 6.7613422,6.0255495 0 0 0 5.76015,2.87016 l 73.55487,0 a 6.7613422,6.0255495 0 0 0 6.76135,-6.02554 l 0,-187.87147 a 6.7613422,6.0255495 0 0 0 -6.76135,-6.02555 l -54.71644,0 a 6.7613422,6.0255495 0 0 0 -6.76133,6.02555 l 0,102.61935 L 183.76413,2.90886 a 6.7613422,6.0255495 0 0 0 -5.76014,-2.87017 z" />
|
||||
<path d="M 4.8446333,22.10875 A 13.412039,12.501842 0 0 1 13.477588,0.03924 l 66.118315,0 a 5.3648158,5.000737 0 0 1 5.364823,5.00073 l 0,79.87931 z" />
|
||||
<path d="m 420.15535,177.89119 a 13.412038,12.501842 0 0 1 -8.63295,22.06951 l -66.11832,0 a 5.3648152,5.000737 0 0 1 -5.36482,-5.00074 l 0,-79.87931 z" />
|
||||
</svg>
|
||||
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
|
||||
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="user-secret" class="svg-inline--fa fa-user-secret fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" opacity=".33">
|
||||
<path fill="currentColor" d="M383.9 308.3l23.9-62.6c4-10.5-3.7-21.7-15-21.7h-58.5c11-18.9 17.8-40.6 17.8-64v-.3c39.2-7.8 64-19.1 64-31.7 0-13.3-27.3-25.1-70.1-33-9.2-32.8-27-65.8-40.6-82.8-9.5-11.9-25.9-15.6-39.5-8.8l-27.6 13.8c-9 4.5-19.6 4.5-28.6 0L182.1 3.4c-13.6-6.8-30-3.1-39.5 8.8-13.5 17-31.4 50-40.6 82.8-42.7 7.9-70 19.7-70 33 0 12.6 24.8 23.9 64 31.7v.3c0 23.4 6.8 45.1 17.8 64H56.3c-11.5 0-19.2 11.7-14.7 22.3l25.8 60.2C27.3 329.8 0 372.7 0 422.4v44.8C0 491.9 20.1 512 44.8 512h358.4c24.7 0 44.8-20.1 44.8-44.8v-44.8c0-48.4-25.8-90.4-64.1-114.1zM176 480l-41.6-192 49.6 32 24 40-32 120zm96 0l-32-120 24-40 49.6-32L272 480zm41.7-298.5c-3.9 11.9-7 24.6-16.5 33.4-10.1 9.3-48 22.4-64-25-2.8-8.4-15.4-8.4-18.3 0-17 50.2-56 32.4-64 25-9.5-8.8-12.7-21.5-16.5-33.4-.8-2.5-6.3-5.7-6.3-5.8v-10.8c28.3 3.6 61 5.8 96 5.8s67.7-2.1 96-5.8v10.8c-.1.1-5.6 3.2-6.4 5.8z"></path>
|
||||
</svg>
|
||||
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
|
||||
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 200 289.742" opacity=".33">
|
||||
<path d="m 93.537677,0 c -18.113125,0 -34.220133,3.11164 -48.323484,9.33437 -13.965092,6.22167 -24.612442,15.07114 -31.940651,26.5471 -7.1899398,11.33789 -10.3012266,24.74911 -10.3012266,40.23478 0,10.64662 2.7250026,20.46465 8.1751116,29.45258 5.615277,8.98686 14.038277,17.35204 25.268821,25.09436 11.230544,7.60531 26.507421,15.41835 45.830514,23.43782 19.983748,8.29557 34.848848,15.55471 44.592998,21.77638 9.74414,6.22273 16.7617,12.8585 21.05572,19.90951 4.29404,7.05208 6.44193,15.76408 6.44193,26.13459 0,16.17702 -5.20196,28.48222 -15.60673,36.91682 -10.2396,8.4347 -25.02203,12.6523 -44.345169,12.6523 -14.038171,0 -25.515247,-1.6594 -34.433618,-4.9777 -8.91837,-3.4566 -16.185572,-8.7113 -21.800839,-15.7633 -5.615277,-7.0521 -10.074795,-16.66088 -13.377899,-28.82812 l -24.7731626293945,0 0,56.82632 C 33.856769,286.07601 63.74904,289.74201 89.678383,289.74201 c 16.020027,0 30.719787,-1.3827 44.097337,-4.1479 13.54272,-2.9043 25.1041,-7.4676 34.68309,-13.6893 9.74413,-6.3597 17.34042,-14.5195 22.79052,-24.4748 5.4501,-10.09332 8.17511,-22.39959 8.17511,-36.91682 0,-12.99764 -3.3021,-24.33539 -9.90829,-34.0146 -6.44105,-9.81725 -15.52545,-18.52707 -27.25146,-26.13133 -11.56085,-7.60427 -27.91083,-15.83142 -49.05066,-24.68022 -17.50644,-7.19012 -30.719668,-13.68948 -39.638038,-19.49701 -8.918371,-5.80752 -18.607474,-12.43409 -24.096524,-18.87417 -5.426043,-6.36616 -9.658826,-15.07003 -9.658826,-24.88729 0,-9.26401 2.075414,-17.21345 6.223454,-23.85033 11.098298,-14.39748 41.286638,-1.79507 45.075609,24.34762 4.839392,6.77491 8.84935,16.24729 12.029515,28.4156 l 20.53234,0 0,-55.99967 c -4.47825,-5.92448 -9.95488,-10.63222 -15.90837,-14.37411 1.64055,0.47905 3.19039,1.02376 4.63865,1.64024 6.49861,2.62607 12.16793,7.32747 17.0073,14.10345 4.83939,6.77491 8.84935,16.24567 12.02952,28.41397 0,0 8.48128,-0.12894 8.48978,-0.002 0.41776,6.41494 -1.75339,9.45286 -4.12342,12.56104 -2.4174,3.16978 -5.14486,6.78973 -4.00278,13.0029 1.50786,8.20318 10.18354,10.59642 14.62194,9.31154 -3.31842,-0.49911 -5.31855,-1.74948 -5.31855,-1.74948 0,0 1.87646,0.99868 5.65117,-1.35981 -3.27695,0.95571 -10.70529,-0.79738 -11.80125,-6.76313 -0.95752,-5.20861 0.94654,-7.29514 3.40113,-10.51482 2.45462,-3.21968 5.28426,-6.95831 4.6843,-14.48824 l 0.003,0.002 8.92676,0 0,-55.99967 c -15.07125,-3.87168 -27.65314,-6.36042 -37.74671,-7.46586 -9.95531,-1.10755 -20.18823,-1.65981 -30.696613,-1.65981 z m 70.321603,17.30893 0.23805,40.3049 c 1.31808,1.22666 2.43965,2.27815 3.34081,3.10602 4.83939,6.77491 8.84934,16.24566 12.02951,28.41397 l 20.53234,0 0,-55.99967 c -6.67731,-4.59381 -19.83643,-10.47309 -36.14071,-15.82522 z m -28.12049,5.60551 8.56479,17.71655 c -11.97037,-6.46697 -13.84678,-9.71726 -8.56479,-17.71655 z m 22.79705,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z m 15.22195,24.00848 8.56479,17.71655 c -11.97038,-6.46697 -13.84679,-9.71726 -8.56479,-17.71655 z m 22.79704,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z m -99.11384,2.20764 8.56479,17.71655 c -11.970382,-6.46697 -13.846782,-9.71726 -8.56479,-17.71655 z m 22.79542,0 c 2.7715,7.99929 1.78741,11.24958 -4.49354,17.71655 l 4.49354,-17.71655 z" />
|
||||
</svg>
|
||||
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
|
||||
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->
|
||||
|
Before Width: | Height: | Size: 3.4 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="meteor" class="svg-inline--fa fa-meteor fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" opacity=".33"><path fill="currentColor" d="M511.328,20.8027c-11.60759,38.70264-34.30724,111.70173-61.30311,187.70077,6.99893,2.09372,13.4042,4,18.60653,5.59368a16.06158,16.06158,0,0,1,9.49854,22.906c-22.106,42.29635-82.69047,152.795-142.47819,214.40356-.99984,1.09373-1.99969,2.5-2.99954,3.49995A194.83046,194.83046,0,1,1,57.085,179.41009c.99985-1,2.40588-2,3.49947-3,61.59994-59.90549,171.97367-120.40473,214.37343-142.4982a16.058,16.058,0,0,1,22.90274,9.49988c1.59351,5.09368,3.49947,11.5936,5.5929,18.59351C379.34818,35.00565,452.43074,12.30281,491.12794.70921A16.18325,16.18325,0,0,1,511.328,20.8027ZM319.951,320.00207A127.98041,127.98041,0,1,0,191.97061,448.00046,127.97573,127.97573,0,0,0,319.951,320.00207Zm-127.98041-31.9996a31.9951,31.9951,0,1,1-31.9951-31.9996A31.959,31.959,0,0,1,191.97061,288.00247Zm31.9951,79.999a15.99755,15.99755,0,1,1-15.99755-15.9998A16.04975,16.04975,0,0,1,223.96571,368.00147Z"></path></svg>
|
||||
<!-- This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. -->
|
||||
<!-- http://creativecommons.org/licenses/by-sa/4.0/ -->
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,373 +0,0 @@
|
||||
/**
|
||||
* This file provides a clear separation layer between backend updates
|
||||
* and what state our React app sees.
|
||||
*
|
||||
* Sometimes backend can response without a "data" field, but our final
|
||||
* state will still contain previous "data" because we are merging
|
||||
* the response with already existing state.
|
||||
*
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perf } from 'common/perf';
|
||||
import { createAction } from 'common/redux';
|
||||
import { setupDrag } from './drag';
|
||||
import { globalEvents } from './events';
|
||||
import { focusMap } from './focus';
|
||||
import { createLogger } from './logging';
|
||||
import { resumeRenderer, suspendRenderer } from './renderer';
|
||||
|
||||
const logger = createLogger('backend');
|
||||
|
||||
export const backendUpdate = createAction('backend/update');
|
||||
export const backendSetSharedState = createAction('backend/setSharedState');
|
||||
export const backendSuspendStart = createAction('backend/suspendStart');
|
||||
|
||||
export const backendSuspendSuccess = () => ({
|
||||
type: 'backend/suspendSuccess',
|
||||
payload: {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
config: {},
|
||||
data: {},
|
||||
shared: {},
|
||||
// Start as suspended
|
||||
suspended: Date.now(),
|
||||
suspending: false,
|
||||
};
|
||||
|
||||
export const backendReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'backend/update') {
|
||||
// Merge config
|
||||
const config = {
|
||||
...state.config,
|
||||
...payload.config,
|
||||
};
|
||||
// Merge data
|
||||
const data = {
|
||||
...state.data,
|
||||
...payload.static_data,
|
||||
...payload.data,
|
||||
};
|
||||
// Merge shared states
|
||||
const shared = { ...state.shared };
|
||||
if (payload.shared) {
|
||||
for (let key of Object.keys(payload.shared)) {
|
||||
const value = payload.shared[key];
|
||||
if (value === '') {
|
||||
shared[key] = undefined;
|
||||
} else {
|
||||
shared[key] = JSON.parse(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return new state
|
||||
return {
|
||||
...state,
|
||||
config,
|
||||
data,
|
||||
shared,
|
||||
suspended: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/setSharedState') {
|
||||
const { key, nextState } = payload;
|
||||
return {
|
||||
...state,
|
||||
shared: {
|
||||
...state.shared,
|
||||
[key]: nextState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'byond/ctrldown') {
|
||||
globalEvents.emit('byond/ctrldown');
|
||||
}
|
||||
|
||||
if (type === 'byond/ctrlup') {
|
||||
globalEvents.emit('byond/ctrlup');
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart') {
|
||||
return {
|
||||
...state,
|
||||
suspending: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
const { timestamp } = payload;
|
||||
return {
|
||||
...state,
|
||||
data: {},
|
||||
shared: {},
|
||||
config: {
|
||||
...state.config,
|
||||
title: '',
|
||||
status: 1,
|
||||
},
|
||||
suspending: false,
|
||||
suspended: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const backendMiddleware = (store) => {
|
||||
let fancyState;
|
||||
let suspendInterval;
|
||||
|
||||
return (next) => (action) => {
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'update') {
|
||||
store.dispatch(backendUpdate(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'suspend') {
|
||||
store.dispatch(backendSuspendSuccess());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ping') {
|
||||
Byond.sendMessage('ping/reply');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'byond/mousedown') {
|
||||
globalEvents.emit('byond/mousedown');
|
||||
}
|
||||
|
||||
if (type === 'byond/mouseup') {
|
||||
globalEvents.emit('byond/mouseup');
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart' && !suspendInterval) {
|
||||
logger.log(`suspending (${Byond.windowId})`);
|
||||
// Keep sending suspend messages until it succeeds.
|
||||
// It may fail multiple times due to topic rate limiting.
|
||||
const suspendFn = () => Byond.sendMessage('suspend');
|
||||
suspendFn();
|
||||
suspendInterval = setInterval(suspendFn, 2000);
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
suspendRenderer();
|
||||
clearInterval(suspendInterval);
|
||||
suspendInterval = undefined;
|
||||
Byond.winset(Byond.windowId, {
|
||||
'is-visible': false,
|
||||
});
|
||||
setImmediate(() => focusMap());
|
||||
}
|
||||
|
||||
if (type === 'backend/update') {
|
||||
const fancy = payload.config?.window?.fancy;
|
||||
// Initialize fancy state
|
||||
if (fancyState === undefined) {
|
||||
fancyState = fancy;
|
||||
}
|
||||
// React to changes in fancy
|
||||
else if (fancyState !== fancy) {
|
||||
logger.log('changing fancy mode to', fancy);
|
||||
fancyState = fancy;
|
||||
Byond.winset(Byond.windowId, {
|
||||
titlebar: !fancy,
|
||||
'can-resize': !fancy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resume on incoming update
|
||||
if (type === 'backend/update' && suspended) {
|
||||
// Show the payload
|
||||
logger.log('backend/update', payload);
|
||||
// Signal renderer that we have resumed
|
||||
resumeRenderer();
|
||||
// Setup drag
|
||||
setupDrag();
|
||||
// We schedule this for the next tick here because resizing and unhiding
|
||||
// during the same tick will flash with a white background.
|
||||
setImmediate(() => {
|
||||
perf.mark('resume/start');
|
||||
// Doublecheck if we are not re-suspended.
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
if (suspended) {
|
||||
return;
|
||||
}
|
||||
Byond.winset(Byond.windowId, {
|
||||
'is-visible': true,
|
||||
});
|
||||
perf.mark('resume/finish');
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.log(
|
||||
'visible in',
|
||||
perf.measure('render/finish', 'resume/finish')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends an action to `ui_act` on `src_object` that this tgui window
|
||||
* is associated with.
|
||||
*/
|
||||
export const sendAct = (action: string, payload: object = {}) => {
|
||||
// Validate that payload is an object
|
||||
// prettier-ignore
|
||||
const isObject = typeof payload === 'object'
|
||||
&& payload !== null
|
||||
&& !Array.isArray(payload);
|
||||
if (!isObject) {
|
||||
logger.error(`Payload for act() must be an object, got this:`, payload);
|
||||
return;
|
||||
}
|
||||
Byond.sendMessage('act/' + action, payload);
|
||||
};
|
||||
|
||||
type BackendState<TData> = {
|
||||
config: {
|
||||
title: string;
|
||||
status: number;
|
||||
interface: string;
|
||||
refreshing: boolean;
|
||||
window: {
|
||||
key: string;
|
||||
size: [number, number];
|
||||
fancy: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
client: {
|
||||
ckey: string;
|
||||
address: string;
|
||||
computer_id: string;
|
||||
};
|
||||
user: {
|
||||
name: string;
|
||||
observer: number;
|
||||
};
|
||||
};
|
||||
data: TData;
|
||||
shared: Record<string, any>;
|
||||
suspending: boolean;
|
||||
suspended: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects a backend-related slice of Redux state
|
||||
*/
|
||||
export const selectBackend = <TData>(state: any): BackendState<TData> =>
|
||||
state.backend || {};
|
||||
|
||||
/**
|
||||
* Get data from tgui backend.
|
||||
*
|
||||
* Includes the `act` function for performing DM actions.
|
||||
*/
|
||||
export const useBackend = <TData>(context: any) => {
|
||||
const { store } = context;
|
||||
const state = selectBackend<TData>(store.getState());
|
||||
return {
|
||||
...state,
|
||||
act: sendAct,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A tuple that contains the state and a setter function for it.
|
||||
*/
|
||||
type StateWithSetter<T> = [T, (nextState: T) => void];
|
||||
|
||||
/**
|
||||
* Allocates state on Redux store without sharing it with other clients.
|
||||
*
|
||||
* Use it when you want to have a stateful variable in your component
|
||||
* that persists between renders, but will be forgotten after you close
|
||||
* the UI.
|
||||
*
|
||||
* It is a lot more performant than `setSharedState`.
|
||||
*
|
||||
* @param context React context.
|
||||
* @param key Key which uniquely identifies this state in Redux store.
|
||||
* @param initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useLocalState = <T>(
|
||||
context: any,
|
||||
key: string,
|
||||
initialState: T
|
||||
): StateWithSetter<T> => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = key in sharedStates ? sharedStates[key] : initialState;
|
||||
return [
|
||||
sharedState,
|
||||
(nextState) => {
|
||||
store.dispatch(
|
||||
backendSetSharedState({
|
||||
key,
|
||||
nextState:
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: nextState,
|
||||
})
|
||||
);
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates state on Redux store, and **shares** it with other clients
|
||||
* in the game.
|
||||
*
|
||||
* Use it when you want to have a stateful variable in your component
|
||||
* that persists not only between renders, but also gets pushed to other
|
||||
* clients that observe this UI.
|
||||
*
|
||||
* This makes creation of observable s
|
||||
*
|
||||
* @param context React context.
|
||||
* @param key Key which uniquely identifies this state in Redux store.
|
||||
* @param initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useSharedState = <T>(
|
||||
context: any,
|
||||
key: string,
|
||||
initialState: T
|
||||
): StateWithSetter<T> => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = key in sharedStates ? sharedStates[key] : initialState;
|
||||
return [
|
||||
sharedState,
|
||||
(nextState) => {
|
||||
// prettier-ignore
|
||||
Byond.sendMessage({
|
||||
type: 'setSharedState',
|
||||
key,
|
||||
value: JSON.stringify(
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: nextState
|
||||
) || '',
|
||||
});
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,189 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp, toFixed } from 'common/math';
|
||||
import { Component, createRef } from 'inferno';
|
||||
|
||||
const isSafeNumber = (value: number) => {
|
||||
// prettier-ignore
|
||||
return typeof value === 'number'
|
||||
&& Number.isFinite(value)
|
||||
&& !Number.isNaN(value);
|
||||
};
|
||||
|
||||
export type AnimatedNumberProps = {
|
||||
/**
|
||||
* The target value to approach.
|
||||
*/
|
||||
value: number;
|
||||
|
||||
/**
|
||||
* If provided, the initial value displayed. By default, the same as `value`.
|
||||
* If `initial` and `value` are different, the component immediately starts
|
||||
* animating.
|
||||
*/
|
||||
initial?: number;
|
||||
|
||||
/**
|
||||
* If provided, a function that formats the inner string. By default,
|
||||
* attempts to match the numeric precision of `value`.
|
||||
*/
|
||||
format?: (value: number) => string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Animated numbers are animated at roughly 60 frames per second.
|
||||
*/
|
||||
const SIXTY_HZ = 1_000.0 / 60.0;
|
||||
|
||||
/**
|
||||
* The exponential moving average coefficient. Larger values result in a faster
|
||||
* convergence.
|
||||
*/
|
||||
const Q = 0.8333;
|
||||
|
||||
/**
|
||||
* A small number.
|
||||
*/
|
||||
const EPSILON = 10e-4;
|
||||
|
||||
/**
|
||||
* An animated number label. Shows a number, formatted with an optionally
|
||||
* provided function, and animates it towards its target value.
|
||||
*/
|
||||
export class AnimatedNumber extends Component<AnimatedNumberProps> {
|
||||
/**
|
||||
* The inner `<span/>` being updated sixty times per second.
|
||||
*/
|
||||
ref = createRef<HTMLSpanElement>();
|
||||
|
||||
/**
|
||||
* The interval being used to update the inner span.
|
||||
*/
|
||||
interval?: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
* The current value. This values approaches the target value.
|
||||
*/
|
||||
currentValue: number = 0;
|
||||
|
||||
constructor(props: AnimatedNumberProps) {
|
||||
super(props);
|
||||
|
||||
const { initial, value } = props;
|
||||
|
||||
if (initial !== undefined && isSafeNumber(initial)) {
|
||||
this.currentValue = initial;
|
||||
} else if (isSafeNumber(value)) {
|
||||
this.currentValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.currentValue !== this.props.value) {
|
||||
this.startTicking();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// Stop animating when the component is unmounted.
|
||||
this.stopTicking();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(newProps: AnimatedNumberProps) {
|
||||
if (newProps.value !== this.props.value) {
|
||||
// The target value has been adjusted; start animating if we aren't
|
||||
// already.
|
||||
this.startTicking();
|
||||
}
|
||||
|
||||
// We render the inner `span` directly using a ref to bypass inferno diffing
|
||||
// and reach 60 frames per second--tell inferno not to re-render this tree.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts animating the inner span. If the inner span is already animating,
|
||||
* this is a no-op.
|
||||
*/
|
||||
startTicking() {
|
||||
if (this.interval !== undefined) {
|
||||
// We're already ticking; do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
this.interval = setInterval(() => this.tick(), SIXTY_HZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops animating the inner span.
|
||||
*/
|
||||
stopTicking() {
|
||||
if (this.interval === undefined) {
|
||||
// We're not ticking; do nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.interval);
|
||||
|
||||
this.interval = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steps forward one frame.
|
||||
*/
|
||||
tick() {
|
||||
const { currentValue } = this;
|
||||
const { value } = this.props;
|
||||
|
||||
if (isSafeNumber(value)) {
|
||||
// Converge towards the value.
|
||||
this.currentValue = currentValue * Q + value * (1 - Q);
|
||||
} else {
|
||||
// If the value is unsafe, we're never going to converge, so stop ticking.
|
||||
this.stopTicking();
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(value - this.currentValue) < Math.max(EPSILON, EPSILON * value)
|
||||
) {
|
||||
// We're about as close as we're going to get--snap to the value and
|
||||
// stop ticking.
|
||||
this.currentValue = value;
|
||||
this.stopTicking();
|
||||
}
|
||||
|
||||
if (this.ref.current) {
|
||||
// Directly update the inner span, without bothering inferno.
|
||||
this.ref.current.textContent = this.getText();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the inner text of the span.
|
||||
*/
|
||||
getText() {
|
||||
const { props, currentValue } = this;
|
||||
const { format, value } = props;
|
||||
|
||||
if (!isSafeNumber(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
if (format) {
|
||||
return format(this.currentValue);
|
||||
}
|
||||
|
||||
const fraction = String(value).split('.')[1];
|
||||
const precision = fraction ? fraction.length : 0;
|
||||
|
||||
return toFixed(currentValue, clamp(precision, 0, 8));
|
||||
}
|
||||
|
||||
render() {
|
||||
return <span ref={this.ref}>{this.getText()}</span>;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Component, createRef } from 'inferno';
|
||||
|
||||
export class Autofocus extends Component {
|
||||
ref = createRef<HTMLDivElement>();
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.ref.current?.focus();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.ref} tabIndex={-1}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { Component } from 'inferno';
|
||||
|
||||
const DEFAULT_BLINKING_INTERVAL = 1000;
|
||||
const DEFAULT_BLINKING_TIME = 1000;
|
||||
|
||||
export class Blink extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
hidden: false,
|
||||
};
|
||||
}
|
||||
|
||||
createTimer() {
|
||||
const {
|
||||
interval = DEFAULT_BLINKING_INTERVAL,
|
||||
time = DEFAULT_BLINKING_TIME,
|
||||
} = this.props;
|
||||
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timer);
|
||||
|
||||
this.setState({
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
this.interval = setInterval(() => {
|
||||
this.setState({
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({
|
||||
hidden: false,
|
||||
});
|
||||
}, time);
|
||||
}, interval + time);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.createTimer();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.interval !== this.props.interval ||
|
||||
prevProps.time !== this.props.time
|
||||
) {
|
||||
this.createTimer();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
render(props) {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
visibility: this.state.hidden ? 'hidden' : 'visible',
|
||||
}}>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { Box } from './Box';
|
||||
|
||||
export const BlockQuote = (props) => {
|
||||
const { className, ...rest } = props;
|
||||
return <Box className={classes(['BlockQuote', className])} {...rest} />;
|
||||
};
|
||||
@@ -1,153 +0,0 @@
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { resolveAsset } from '../assets';
|
||||
import { Box } from './Box';
|
||||
|
||||
export enum BodyZone {
|
||||
Head = 'head',
|
||||
Chest = 'chest',
|
||||
LeftArm = 'l_arm',
|
||||
RightArm = 'r_arm',
|
||||
LeftLeg = 'l_leg',
|
||||
RightLeg = 'r_leg',
|
||||
Eyes = 'eyes',
|
||||
Mouth = 'mouth',
|
||||
Groin = 'groin',
|
||||
}
|
||||
|
||||
const bodyZonePixelToZone = (x: number, y: number): BodyZone | null => {
|
||||
// TypeScript translation of /atom/movable/screen/zone_sel/proc/get_zone_at
|
||||
if (y < 1) {
|
||||
return null;
|
||||
} else if (y < 10) {
|
||||
if (x > 10 && x < 15) {
|
||||
return BodyZone.RightLeg;
|
||||
} else if (x > 17 && x < 22) {
|
||||
return BodyZone.LeftLeg;
|
||||
}
|
||||
} else if (y < 13) {
|
||||
if (x > 8 && x < 11) {
|
||||
return BodyZone.RightArm;
|
||||
} else if (x > 12 && x < 20) {
|
||||
return BodyZone.Groin;
|
||||
} else if (x > 21 && x < 24) {
|
||||
return BodyZone.LeftArm;
|
||||
}
|
||||
} else if (y < 22) {
|
||||
if (x > 8 && x < 11) {
|
||||
return BodyZone.RightArm;
|
||||
} else if (x > 12 && x < 20) {
|
||||
return BodyZone.Chest;
|
||||
} else if (x > 21 && x < 24) {
|
||||
return BodyZone.LeftArm;
|
||||
}
|
||||
} else if (y < 30 && x > 12 && x < 20) {
|
||||
if (y > 23 && y < 24 && x > 15 && x < 17) {
|
||||
return BodyZone.Mouth;
|
||||
} else if (y > 25 && y < 27 && x > 14 && x < 18) {
|
||||
return BodyZone.Eyes;
|
||||
} else {
|
||||
return BodyZone.Head;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type BodyZoneSelectorProps = {
|
||||
onClick?: (zone: BodyZone) => void;
|
||||
scale?: number;
|
||||
selectedZone: BodyZone | null;
|
||||
theme?: string;
|
||||
};
|
||||
|
||||
type BodyZoneSelectorState = {
|
||||
hoverZone: BodyZone | null;
|
||||
};
|
||||
|
||||
export class BodyZoneSelector extends Component<
|
||||
BodyZoneSelectorProps,
|
||||
BodyZoneSelectorState
|
||||
> {
|
||||
ref = createRef<HTMLDivElement>();
|
||||
state: BodyZoneSelectorState = {
|
||||
hoverZone: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { hoverZone } = this.state;
|
||||
const { scale = 3, selectedZone, theme = 'midnight' } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.ref}
|
||||
style={{
|
||||
width: `${32 * scale}px`,
|
||||
height: `${32 * scale}px`,
|
||||
position: 'relative',
|
||||
}}>
|
||||
<Box
|
||||
as="img"
|
||||
src={resolveAsset(`body_zones.base_${theme}.png`)}
|
||||
onClick={() => {
|
||||
const onClick = this.props.onClick;
|
||||
if (onClick && this.state.hoverZone) {
|
||||
onClick(this.state.hoverZone);
|
||||
}
|
||||
}}
|
||||
onMouseMove={(event) => {
|
||||
if (!this.props.onClick) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = this.ref.current?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const x = event.clientX - rect.left;
|
||||
const y = 32 * scale - (event.clientY - rect.top);
|
||||
|
||||
this.setState({
|
||||
hoverZone: bodyZonePixelToZone(x / scale, y / scale),
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
'-ms-interpolation-mode': 'nearest-neighbor',
|
||||
'position': 'absolute',
|
||||
'width': `${32 * scale}px`,
|
||||
'height': `${32 * scale}px`,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedZone && (
|
||||
<Box
|
||||
as="img"
|
||||
src={resolveAsset(`body_zones.${selectedZone}.png`)}
|
||||
style={{
|
||||
'-ms-interpolation-mode': 'nearest-neighbor',
|
||||
'pointer-events': 'none',
|
||||
'position': 'absolute',
|
||||
'width': `${32 * scale}px`,
|
||||
'height': `${32 * scale}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hoverZone && hoverZone !== selectedZone && (
|
||||
<Box
|
||||
as="img"
|
||||
src={resolveAsset(`body_zones.${hoverZone}.png`)}
|
||||
style={{
|
||||
'-ms-interpolation-mode': 'nearest-neighbor',
|
||||
'opacity': 0.5,
|
||||
'pointer-events': 'none',
|
||||
'position': 'absolute',
|
||||
'width': `${32 * scale}px`,
|
||||
'height': `${32 * scale}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,294 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
|
||||
import { createVNode, InfernoNode, SFC } from 'inferno';
|
||||
import { ChildFlags, VNodeFlags } from 'inferno-vnode-flags';
|
||||
import { CSS_COLORS } from '../constants';
|
||||
|
||||
export type BoxProps = {
|
||||
[key: string]: any;
|
||||
as?: string;
|
||||
className?: string | BooleanLike;
|
||||
children?: InfernoNode;
|
||||
position?: string | BooleanLike;
|
||||
overflow?: string | BooleanLike;
|
||||
overflowX?: string | BooleanLike;
|
||||
overflowY?: string | BooleanLike;
|
||||
top?: string | BooleanLike;
|
||||
bottom?: string | BooleanLike;
|
||||
left?: string | BooleanLike;
|
||||
right?: string | BooleanLike;
|
||||
width?: string | BooleanLike;
|
||||
minWidth?: string | BooleanLike;
|
||||
maxWidth?: string | BooleanLike;
|
||||
height?: string | BooleanLike;
|
||||
minHeight?: string | BooleanLike;
|
||||
maxHeight?: string | BooleanLike;
|
||||
fontSize?: string | BooleanLike;
|
||||
fontFamily?: string;
|
||||
lineHeight?: string | BooleanLike;
|
||||
opacity?: number;
|
||||
textAlign?: string | BooleanLike;
|
||||
verticalAlign?: string | BooleanLike;
|
||||
textTransform?: string | BooleanLike; // VOREStation Addition
|
||||
inline?: BooleanLike;
|
||||
bold?: BooleanLike;
|
||||
italic?: BooleanLike;
|
||||
nowrap?: BooleanLike;
|
||||
preserveWhitespace?: BooleanLike;
|
||||
m?: string | BooleanLike;
|
||||
mx?: string | BooleanLike;
|
||||
my?: string | BooleanLike;
|
||||
mt?: string | BooleanLike;
|
||||
mb?: string | BooleanLike;
|
||||
ml?: string | BooleanLike;
|
||||
mr?: string | BooleanLike;
|
||||
p?: string | BooleanLike;
|
||||
px?: string | BooleanLike;
|
||||
py?: string | BooleanLike;
|
||||
pt?: string | BooleanLike;
|
||||
pb?: string | BooleanLike;
|
||||
pl?: string | BooleanLike;
|
||||
pr?: string | BooleanLike;
|
||||
color?: string | BooleanLike;
|
||||
textColor?: string | BooleanLike;
|
||||
backgroundColor?: string | BooleanLike;
|
||||
// VOREStation Addition Start
|
||||
// Flex props
|
||||
flexGrow?: string | BooleanLike;
|
||||
flexWrap?: string | BooleanLike;
|
||||
flexBasis?: string | BooleanLike;
|
||||
flex?: string | BooleanLike;
|
||||
// VOREStation Addition End
|
||||
fillPositionedParent?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Coverts our rem-like spacing unit into a CSS unit.
|
||||
*/
|
||||
export const unit = (value: unknown): string | undefined => {
|
||||
if (typeof value === 'string') {
|
||||
// Transparently convert pixels into rem units
|
||||
if (value.endsWith('px') && !Byond.IS_LTE_IE8) {
|
||||
return parseFloat(value) / 12 + 'rem';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
if (Byond.IS_LTE_IE8) {
|
||||
return value * 12 + 'px';
|
||||
}
|
||||
return value + 'rem';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as `unit`, but half the size for integers numbers.
|
||||
*/
|
||||
export const halfUnit = (value: unknown): string | undefined => {
|
||||
if (typeof value === 'string') {
|
||||
return unit(value);
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return unit(value * 0.5);
|
||||
}
|
||||
};
|
||||
|
||||
const isColorCode = (str: unknown) => !isColorClass(str);
|
||||
|
||||
const isColorClass = (str: unknown): boolean => {
|
||||
return typeof str === 'string' && CSS_COLORS.includes(str);
|
||||
};
|
||||
|
||||
const mapRawPropTo = (attrName) => (style, value) => {
|
||||
if (typeof value === 'number' || typeof value === 'string') {
|
||||
style[attrName] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const mapUnitPropTo = (attrName, unit) => (style, value) => {
|
||||
if (typeof value === 'number' || typeof value === 'string') {
|
||||
style[attrName] = unit(value);
|
||||
}
|
||||
};
|
||||
|
||||
const mapBooleanPropTo = (attrName, attrValue) => (style, value) => {
|
||||
if (value) {
|
||||
style[attrName] = attrValue;
|
||||
}
|
||||
};
|
||||
|
||||
const mapDirectionalUnitPropTo = (attrName, unit, dirs) => (style, value) => {
|
||||
if (typeof value === 'number' || typeof value === 'string') {
|
||||
for (let i = 0; i < dirs.length; i++) {
|
||||
style[attrName + '-' + dirs[i]] = unit(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mapColorPropTo = (attrName) => (style, value) => {
|
||||
if (isColorCode(value)) {
|
||||
style[attrName] = value;
|
||||
}
|
||||
};
|
||||
|
||||
const styleMapperByPropName = {
|
||||
// Direct mapping
|
||||
position: mapRawPropTo('position'),
|
||||
overflow: mapRawPropTo('overflow'),
|
||||
overflowX: mapRawPropTo('overflow-x'),
|
||||
overflowY: mapRawPropTo('overflow-y'),
|
||||
top: mapUnitPropTo('top', unit),
|
||||
bottom: mapUnitPropTo('bottom', unit),
|
||||
left: mapUnitPropTo('left', unit),
|
||||
right: mapUnitPropTo('right', unit),
|
||||
width: mapUnitPropTo('width', unit),
|
||||
minWidth: mapUnitPropTo('min-width', unit),
|
||||
maxWidth: mapUnitPropTo('max-width', unit),
|
||||
height: mapUnitPropTo('height', unit),
|
||||
minHeight: mapUnitPropTo('min-height', unit),
|
||||
maxHeight: mapUnitPropTo('max-height', unit),
|
||||
fontSize: mapUnitPropTo('font-size', unit),
|
||||
fontFamily: mapRawPropTo('font-family'),
|
||||
lineHeight: (style, value) => {
|
||||
if (typeof value === 'number') {
|
||||
style['line-height'] = value;
|
||||
} else if (typeof value === 'string') {
|
||||
style['line-height'] = unit(value);
|
||||
}
|
||||
},
|
||||
opacity: mapRawPropTo('opacity'),
|
||||
textAlign: mapRawPropTo('text-align'),
|
||||
verticalAlign: mapRawPropTo('vertical-align'),
|
||||
textTransform: mapRawPropTo('text-transform'), // VOREStation Addition
|
||||
// Boolean props
|
||||
inline: mapBooleanPropTo('display', 'inline-block'),
|
||||
bold: mapBooleanPropTo('font-weight', 'bold'),
|
||||
italic: mapBooleanPropTo('font-style', 'italic'),
|
||||
nowrap: mapBooleanPropTo('white-space', 'nowrap'),
|
||||
preserveWhitespace: mapBooleanPropTo('white-space', 'pre-wrap'),
|
||||
// Margins
|
||||
m: mapDirectionalUnitPropTo('margin', halfUnit, [
|
||||
'top',
|
||||
'bottom',
|
||||
'left',
|
||||
'right',
|
||||
]),
|
||||
mx: mapDirectionalUnitPropTo('margin', halfUnit, ['left', 'right']),
|
||||
my: mapDirectionalUnitPropTo('margin', halfUnit, ['top', 'bottom']),
|
||||
mt: mapUnitPropTo('margin-top', halfUnit),
|
||||
mb: mapUnitPropTo('margin-bottom', halfUnit),
|
||||
ml: mapUnitPropTo('margin-left', halfUnit),
|
||||
mr: mapUnitPropTo('margin-right', halfUnit),
|
||||
// Margins
|
||||
p: mapDirectionalUnitPropTo('padding', halfUnit, [
|
||||
'top',
|
||||
'bottom',
|
||||
'left',
|
||||
'right',
|
||||
]),
|
||||
px: mapDirectionalUnitPropTo('padding', halfUnit, ['left', 'right']),
|
||||
py: mapDirectionalUnitPropTo('padding', halfUnit, ['top', 'bottom']),
|
||||
pt: mapUnitPropTo('padding-top', halfUnit),
|
||||
pb: mapUnitPropTo('padding-bottom', halfUnit),
|
||||
pl: mapUnitPropTo('padding-left', halfUnit),
|
||||
pr: mapUnitPropTo('padding-right', halfUnit),
|
||||
// Color props
|
||||
color: mapColorPropTo('color'),
|
||||
textColor: mapColorPropTo('color'),
|
||||
backgroundColor: mapColorPropTo('background-color'),
|
||||
// VOREStation Addition Start
|
||||
// Flex props
|
||||
flexGrow: mapRawPropTo('flex-grow'),
|
||||
flexWrap: mapRawPropTo('flex-wrap'),
|
||||
flexBasis: mapRawPropTo('flex-basis'),
|
||||
flex: mapRawPropTo('flex'),
|
||||
// VOREStation Addition End
|
||||
// Utility props
|
||||
fillPositionedParent: (style, value) => {
|
||||
if (value) {
|
||||
style['position'] = 'absolute';
|
||||
style['top'] = 0;
|
||||
style['bottom'] = 0;
|
||||
style['left'] = 0;
|
||||
style['right'] = 0;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const computeBoxProps = (props: BoxProps) => {
|
||||
const computedProps: HTMLAttributes<any> = {};
|
||||
const computedStyles = {};
|
||||
// Compute props
|
||||
for (let propName of Object.keys(props)) {
|
||||
if (propName === 'style') {
|
||||
continue;
|
||||
}
|
||||
// IE8: onclick workaround
|
||||
if (Byond.IS_LTE_IE8 && propName === 'onClick') {
|
||||
computedProps.onclick = props[propName];
|
||||
continue;
|
||||
}
|
||||
const propValue = props[propName];
|
||||
const mapPropToStyle = styleMapperByPropName[propName];
|
||||
if (mapPropToStyle) {
|
||||
mapPropToStyle(computedStyles, propValue);
|
||||
} else {
|
||||
computedProps[propName] = propValue;
|
||||
}
|
||||
}
|
||||
// Concatenate styles
|
||||
let style = '';
|
||||
for (let attrName of Object.keys(computedStyles)) {
|
||||
const attrValue = computedStyles[attrName];
|
||||
style += attrName + ':' + attrValue + ';';
|
||||
}
|
||||
if (props.style) {
|
||||
for (let attrName of Object.keys(props.style)) {
|
||||
const attrValue = props.style[attrName];
|
||||
style += attrName + ':' + attrValue + ';';
|
||||
}
|
||||
}
|
||||
if (style.length > 0) {
|
||||
computedProps.style = style;
|
||||
}
|
||||
return computedProps;
|
||||
};
|
||||
|
||||
export const computeBoxClassName = (props: BoxProps) => {
|
||||
const color = props.textColor || props.color;
|
||||
const backgroundColor = props.backgroundColor;
|
||||
return classes([
|
||||
isColorClass(color) && 'color-' + color,
|
||||
isColorClass(backgroundColor) && 'color-bg-' + backgroundColor,
|
||||
]);
|
||||
};
|
||||
|
||||
export const Box: SFC<BoxProps> = (props: BoxProps) => {
|
||||
const { as = 'div', className, children, ...rest } = props;
|
||||
// Render props
|
||||
if (typeof children === 'function') {
|
||||
return children(computeBoxProps(props));
|
||||
}
|
||||
const computedClassName =
|
||||
typeof className === 'string'
|
||||
? className + ' ' + computeBoxClassName(rest)
|
||||
: computeBoxClassName(rest);
|
||||
const computedProps = computeBoxProps(rest);
|
||||
// Render a wrapper element
|
||||
return createVNode(
|
||||
VNodeFlags.HtmlElement,
|
||||
as,
|
||||
computedClassName,
|
||||
children,
|
||||
ChildFlags.UnknownChildren,
|
||||
computedProps,
|
||||
undefined
|
||||
);
|
||||
};
|
||||
|
||||
Box.defaultHooks = pureComponentHooks;
|
||||
@@ -1,365 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_ENTER, KEY_ESCAPE, KEY_SPACE } from 'common/keycodes';
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { createLogger } from '../logging';
|
||||
import { Box, computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { Icon } from './Icon';
|
||||
import { Tooltip } from './Tooltip';
|
||||
|
||||
const logger = createLogger('Button');
|
||||
|
||||
export const Button = (props) => {
|
||||
const {
|
||||
className,
|
||||
fluid,
|
||||
icon,
|
||||
iconRotation,
|
||||
iconSpin,
|
||||
iconColor,
|
||||
iconPosition,
|
||||
iconSize, // VOREStation Addition
|
||||
color,
|
||||
disabled,
|
||||
selected,
|
||||
tooltip,
|
||||
tooltipPosition,
|
||||
ellipsis,
|
||||
compact,
|
||||
circular,
|
||||
content,
|
||||
children,
|
||||
onclick,
|
||||
onClick,
|
||||
verticalAlignContent,
|
||||
...rest
|
||||
} = props;
|
||||
const hasContent = !!(content || children);
|
||||
// A warning about the lowercase onclick
|
||||
if (onclick) {
|
||||
logger.warn(
|
||||
`Lowercase 'onclick' is not supported on Button and lowercase` +
|
||||
` prop names are discouraged in general. Please use a camelCase` +
|
||||
`'onClick' instead and read: ` +
|
||||
`https://infernojs.org/docs/guides/event-handling`
|
||||
);
|
||||
}
|
||||
rest.onClick = (e) => {
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
// IE8: Use "unselectable" because "user-select" doesn't work.
|
||||
if (Byond.IS_LTE_IE8) {
|
||||
rest.unselectable = true;
|
||||
}
|
||||
let buttonContent = (
|
||||
<div
|
||||
className={classes([
|
||||
'Button',
|
||||
fluid && 'Button--fluid',
|
||||
disabled && 'Button--disabled',
|
||||
selected && 'Button--selected',
|
||||
hasContent && 'Button--hasContent',
|
||||
ellipsis && 'Button--ellipsis',
|
||||
circular && 'Button--circular',
|
||||
compact && 'Button--compact',
|
||||
iconPosition && 'Button--iconPosition--' + iconPosition,
|
||||
verticalAlignContent && 'Button--flex',
|
||||
verticalAlignContent && fluid && 'Button--flex--fluid',
|
||||
verticalAlignContent &&
|
||||
'Button--verticalAlignContent--' + verticalAlignContent,
|
||||
color && typeof color === 'string'
|
||||
? 'Button--color--' + color
|
||||
: 'Button--color--default',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
tabIndex={!disabled && '0'}
|
||||
onKeyDown={(e) => {
|
||||
if (props.captureKeys === false) {
|
||||
return;
|
||||
}
|
||||
const keyCode = window.event ? e.which : e.keyCode;
|
||||
// Simulate a click when pressing space or enter.
|
||||
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
|
||||
e.preventDefault();
|
||||
if (!disabled && onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Refocus layout on pressing escape.
|
||||
if (keyCode === KEY_ESCAPE) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}}
|
||||
{...computeBoxProps(rest)}>
|
||||
<div className="Button__content">
|
||||
{icon && iconPosition !== 'right' && (
|
||||
<Icon
|
||||
name={icon}
|
||||
color={iconColor}
|
||||
rotation={iconRotation}
|
||||
spin={iconSpin}
|
||||
/>
|
||||
)}
|
||||
{content}
|
||||
{children}
|
||||
{icon && iconPosition === 'right' && (
|
||||
<Icon
|
||||
name={icon}
|
||||
color={iconColor}
|
||||
rotation={iconRotation}
|
||||
spin={iconSpin}
|
||||
fontSize={iconSize} // VOREStation Addition
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
buttonContent = (
|
||||
<Tooltip content={tooltip} position={tooltipPosition}>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return buttonContent;
|
||||
};
|
||||
|
||||
Button.defaultHooks = pureComponentHooks;
|
||||
|
||||
export const ButtonCheckbox = (props) => {
|
||||
const { checked, ...rest } = props;
|
||||
return (
|
||||
<Button
|
||||
color="transparent"
|
||||
icon={checked ? 'check-square-o' : 'square-o'}
|
||||
selected={checked}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Button.Checkbox = ButtonCheckbox;
|
||||
|
||||
export class ButtonConfirm extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
clickedOnce: false,
|
||||
};
|
||||
this.handleClick = () => {
|
||||
if (this.state.clickedOnce) {
|
||||
this.setClickedOnce(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setClickedOnce(clickedOnce) {
|
||||
this.setState({
|
||||
clickedOnce,
|
||||
});
|
||||
if (clickedOnce) {
|
||||
setTimeout(() => window.addEventListener('click', this.handleClick));
|
||||
} else {
|
||||
window.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
confirmContent = 'Confirm?',
|
||||
confirmColor = 'bad',
|
||||
confirmIcon,
|
||||
icon,
|
||||
color,
|
||||
content,
|
||||
onClick,
|
||||
...rest
|
||||
} = this.props;
|
||||
return (
|
||||
<Button
|
||||
content={this.state.clickedOnce ? confirmContent : content}
|
||||
icon={this.state.clickedOnce ? confirmIcon : icon}
|
||||
color={this.state.clickedOnce ? confirmColor : color}
|
||||
onClick={() =>
|
||||
this.state.clickedOnce ? onClick() : this.setClickedOnce(true)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.Confirm = ButtonConfirm;
|
||||
|
||||
export class ButtonInput extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
inInput: false,
|
||||
};
|
||||
}
|
||||
|
||||
setInInput(inInput) {
|
||||
this.setState({
|
||||
inInput,
|
||||
});
|
||||
if (this.inputRef) {
|
||||
const input = this.inputRef.current;
|
||||
if (inInput) {
|
||||
input.value = this.props.currentValue || '';
|
||||
try {
|
||||
input.focus();
|
||||
input.select();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commitResult(e) {
|
||||
if (this.inputRef) {
|
||||
const input = this.inputRef.current;
|
||||
const hasValue = input.value !== '';
|
||||
if (hasValue) {
|
||||
this.props.onCommit(e, input.value);
|
||||
return;
|
||||
} else {
|
||||
if (!this.props.defaultValue) {
|
||||
return;
|
||||
}
|
||||
this.props.onCommit(e, this.props.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
fluid,
|
||||
content,
|
||||
icon,
|
||||
iconRotation,
|
||||
iconSpin,
|
||||
tooltip,
|
||||
tooltipPosition,
|
||||
color = 'default',
|
||||
placeholder,
|
||||
maxLength,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
let buttonContent = (
|
||||
<Box
|
||||
className={classes([
|
||||
'Button',
|
||||
fluid && 'Button--fluid',
|
||||
'Button--color--' + color,
|
||||
])}
|
||||
{...rest}
|
||||
onClick={() => this.setInInput(true)}>
|
||||
{icon && <Icon name={icon} rotation={iconRotation} spin={iconSpin} />}
|
||||
<div>{content}</div>
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="NumberInput__input"
|
||||
style={{
|
||||
'display': !this.state.inInput ? 'none' : undefined,
|
||||
'text-align': 'left',
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (!this.state.inInput) {
|
||||
return;
|
||||
}
|
||||
this.setInInput(false);
|
||||
this.commitResult(e);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === KEY_ENTER) {
|
||||
this.setInInput(false);
|
||||
this.commitResult(e);
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === KEY_ESCAPE) {
|
||||
this.setInInput(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
buttonContent = (
|
||||
<Tooltip content={tooltip} position={tooltipPosition}>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return buttonContent;
|
||||
}
|
||||
}
|
||||
|
||||
Button.Input = ButtonInput;
|
||||
|
||||
export class ButtonFile extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.inputRef = createRef();
|
||||
}
|
||||
|
||||
async read(files) {
|
||||
const promises = Array.from(files).map((file) => {
|
||||
let reader = new FileReader();
|
||||
return new Promise((resolve) => {
|
||||
reader.onload = () => resolve(reader.result);
|
||||
reader.readAsText(file);
|
||||
});
|
||||
});
|
||||
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onSelectFiles, accept, multiple, ...rest } = this.props;
|
||||
const filePicker = (
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
ref={this.inputRef}
|
||||
accept={accept}
|
||||
multiple={multiple}
|
||||
onChange={async () => {
|
||||
const files = this.inputRef.current.files;
|
||||
if (files.length) {
|
||||
const readFiles = await this.read(files);
|
||||
onSelectFiles(multiple ? readFiles : readFiles[0]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
{...rest}
|
||||
onClick={() => {
|
||||
this.inputRef.current.click();
|
||||
}}
|
||||
/>
|
||||
{filePicker}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.File = ButtonFile;
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { shallowDiffers } from 'common/react';
|
||||
import { debounce } from 'common/timer';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { createLogger } from '../logging';
|
||||
import { computeBoxProps } from './Box';
|
||||
|
||||
const logger = createLogger('ByondUi');
|
||||
|
||||
// Stack of currently allocated BYOND UI element ids.
|
||||
const byondUiStack = [];
|
||||
|
||||
const createByondUiElement = (elementId) => {
|
||||
// Reserve an index in the stack
|
||||
const index = byondUiStack.length;
|
||||
byondUiStack.push(null);
|
||||
// Get a unique id
|
||||
const id = elementId || 'byondui_' + index;
|
||||
logger.log(`allocated '${id}'`);
|
||||
// Return a control structure
|
||||
return {
|
||||
render: (params) => {
|
||||
logger.log(`rendering '${id}'`);
|
||||
byondUiStack[index] = id;
|
||||
Byond.winset(id, params);
|
||||
},
|
||||
unmount: () => {
|
||||
logger.log(`unmounting '${id}'`);
|
||||
byondUiStack[index] = null;
|
||||
Byond.winset(id, {
|
||||
parent: '',
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// Cleanly unmount all visible UI elements
|
||||
for (let index = 0; index < byondUiStack.length; index++) {
|
||||
const id = byondUiStack[index];
|
||||
if (typeof id === 'string') {
|
||||
logger.log(`unmounting '${id}' (beforeunload)`);
|
||||
byondUiStack[index] = null;
|
||||
Byond.winset(id, {
|
||||
parent: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the bounding box of the DOM element in display-pixels.
|
||||
*/
|
||||
const getBoundingBox = (element) => {
|
||||
const pixelRatio = window.devicePixelRatio ?? 1;
|
||||
const rect = element.getBoundingClientRect();
|
||||
// prettier-ignore
|
||||
return {
|
||||
pos: [
|
||||
rect.left * pixelRatio,
|
||||
rect.top * pixelRatio,
|
||||
],
|
||||
size: [
|
||||
(rect.right - rect.left) * pixelRatio,
|
||||
(rect.bottom - rect.top) * pixelRatio,
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export class ByondUi extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.containerRef = createRef();
|
||||
this.byondUiElement = createByondUiElement(props.params?.id);
|
||||
this.handleResize = debounce(() => {
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const { params: prevParams = {}, ...prevRest } = this.props;
|
||||
const { params: nextParams = {}, ...nextRest } = nextProps;
|
||||
return (
|
||||
shallowDiffers(prevParams, nextParams) ||
|
||||
shallowDiffers(prevRest, nextRest)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// IE8: It probably works, but fuck you anyway.
|
||||
if (Byond.IS_LTE_IE10) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.componentDidUpdate();
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// IE8: It probably works, but fuck you anyway.
|
||||
if (Byond.IS_LTE_IE10) {
|
||||
return;
|
||||
}
|
||||
const { params = {} } = this.props;
|
||||
const box = getBoundingBox(this.containerRef.current);
|
||||
logger.debug('bounding box', box);
|
||||
this.byondUiElement.render({
|
||||
parent: Byond.windowId,
|
||||
...params,
|
||||
pos: box.pos[0] + ',' + box.pos[1],
|
||||
size: box.size[0] + 'x' + box.size[1],
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// IE8: It probably works, but fuck you anyway.
|
||||
if (Byond.IS_LTE_IE10) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
this.byondUiElement.unmount();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { params, ...rest } = this.props;
|
||||
return (
|
||||
<div ref={this.containerRef} {...computeBoxProps(rest)}>
|
||||
{/* Filler */}
|
||||
<div style={{ 'min-height': '22px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { map, zipWith } from 'common/collections';
|
||||
import { pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
|
||||
const normalizeData = (data, scale, rangeX, rangeY) => {
|
||||
if (data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const min = zipWith(Math.min)(...data);
|
||||
const max = zipWith(Math.max)(...data);
|
||||
if (rangeX !== undefined) {
|
||||
min[0] = rangeX[0];
|
||||
max[0] = rangeX[1];
|
||||
}
|
||||
if (rangeY !== undefined) {
|
||||
min[1] = rangeY[0];
|
||||
max[1] = rangeY[1];
|
||||
}
|
||||
const normalized = map((point) => {
|
||||
return zipWith((value, min, max, scale) => {
|
||||
return ((value - min) / (max - min)) * scale;
|
||||
})(point, min, max, scale);
|
||||
})(data);
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const dataToPolylinePoints = (data) => {
|
||||
let points = '';
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const point = data[i];
|
||||
points += point[0] + ',' + point[1] + ' ';
|
||||
}
|
||||
return points;
|
||||
};
|
||||
|
||||
class LineChart extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.ref = createRef();
|
||||
this.state = {
|
||||
// Initial guess
|
||||
viewBox: [600, 200],
|
||||
};
|
||||
this.handleResize = () => {
|
||||
const element = this.ref.current;
|
||||
this.setState({
|
||||
viewBox: [element.offsetWidth, element.offsetHeight],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data = [],
|
||||
rangeX,
|
||||
rangeY,
|
||||
fillColor = 'none',
|
||||
strokeColor = '#ffffff',
|
||||
strokeWidth = 2,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { viewBox } = this.state;
|
||||
const normalized = normalizeData(data, viewBox, rangeX, rangeY);
|
||||
// Push data outside viewBox and form a fillable polygon
|
||||
if (normalized.length > 0) {
|
||||
const first = normalized[0];
|
||||
const last = normalized[normalized.length - 1];
|
||||
normalized.push([viewBox[0] + strokeWidth, last[1]]);
|
||||
normalized.push([viewBox[0] + strokeWidth, -strokeWidth]);
|
||||
normalized.push([-strokeWidth, -strokeWidth]);
|
||||
normalized.push([-strokeWidth, first[1]]);
|
||||
}
|
||||
const points = dataToPolylinePoints(normalized);
|
||||
return (
|
||||
<Box position="relative" {...rest}>
|
||||
{(props) => (
|
||||
<div ref={this.ref} {...props}>
|
||||
<svg
|
||||
viewBox={`0 0 ${viewBox[0]} ${viewBox[1]}`}
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<polyline
|
||||
transform={`scale(1, -1) translate(0, -${viewBox[1]})`}
|
||||
fill={fillColor}
|
||||
stroke={strokeColor}
|
||||
strokeWidth={strokeWidth}
|
||||
points={points}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.defaultHooks = pureComponentHooks;
|
||||
|
||||
const Stub = (props) => null;
|
||||
|
||||
// IE8: No inline svg support
|
||||
export const Chart = {
|
||||
Line: Byond.IS_LTE_IE8 ? Stub : LineChart,
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Component } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
import { Button } from './Button';
|
||||
|
||||
export class Collapsible extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { open } = props;
|
||||
this.state = {
|
||||
open: open || false,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { open } = this.state;
|
||||
const { children, color = 'default', title, buttons, ...rest } = props;
|
||||
return (
|
||||
<Box mb={1}>
|
||||
<div className="Table">
|
||||
<div className="Table__cell">
|
||||
<Button
|
||||
fluid
|
||||
color={color}
|
||||
icon={open ? 'chevron-down' : 'chevron-right'}
|
||||
onClick={() => this.setState({ open: !open })}
|
||||
{...rest}>
|
||||
{title}
|
||||
</Button>
|
||||
</div>
|
||||
{buttons && (
|
||||
<div className="Table__cell Table__cell--collapsing">{buttons}</div>
|
||||
)}
|
||||
</div>
|
||||
{open && <Box mt={1}>{children}</Box>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
|
||||
export const ColorBox = (props) => {
|
||||
// prettier-ignore
|
||||
const {
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
color,
|
||||
backgroundColor,
|
||||
...rest
|
||||
} = props;
|
||||
rest.color = content ? null : 'transparent';
|
||||
rest.backgroundColor = color || backgroundColor;
|
||||
return (
|
||||
<div
|
||||
className={classes(['ColorBox', className, computeBoxClassName(rest)])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{content || '.'}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ColorBox.defaultHooks = pureComponentHooks;
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2022 raffclar
|
||||
* @license MIT
|
||||
*/
|
||||
import { Box } from './Box';
|
||||
import { Button } from './Button';
|
||||
|
||||
type DialogProps = {
|
||||
title: any;
|
||||
onClose: () => void;
|
||||
children: any;
|
||||
width?: string;
|
||||
height?: string;
|
||||
};
|
||||
|
||||
export const Dialog = (props: DialogProps) => {
|
||||
const { title, onClose, children, width, height } = props;
|
||||
return (
|
||||
<div className="Dialog">
|
||||
<Box className="Dialog__content" width={width || '370px'} height={height}>
|
||||
<div className="Dialog__header">
|
||||
<div className="Dialog__title">{title}</div>
|
||||
<Box mr={2}>
|
||||
<Button
|
||||
mr="-3px"
|
||||
width="26px"
|
||||
lineHeight="22px"
|
||||
textAlign="center"
|
||||
color="transparent"
|
||||
icon="window-close-o"
|
||||
tooltip="Close"
|
||||
tooltipPosition="bottom-start"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
{children}
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type DialogButtonProps = {
|
||||
onClick: () => void;
|
||||
children: any;
|
||||
};
|
||||
|
||||
const DialogButton = (props: DialogButtonProps) => {
|
||||
const { onClick, children } = props;
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
className="Dialog__button"
|
||||
verticalAlignContent="middle">
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
Dialog.Button = DialogButton;
|
||||
|
||||
type UnsavedChangesDialogProps = {
|
||||
documentName: string;
|
||||
onSave: () => void;
|
||||
onDiscard: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const UnsavedChangesDialog = (props: UnsavedChangesDialogProps) => {
|
||||
const { documentName, onSave, onDiscard, onClose } = props;
|
||||
return (
|
||||
<Dialog title="Notepad" onClose={onClose}>
|
||||
<div className="Dialog__body">
|
||||
Do you want to save changes to {documentName}?
|
||||
</div>
|
||||
<div className="Dialog__footer">
|
||||
<DialogButton onClick={onSave}>Save</DialogButton>
|
||||
<DialogButton onClick={onDiscard}>Don't Save</DialogButton>
|
||||
<DialogButton onClick={onClose}>Cancel</DialogButton>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { Box } from './Box';
|
||||
|
||||
export const Dimmer = (props) => {
|
||||
const { className, children, ...rest } = props;
|
||||
return (
|
||||
<Box className={classes(['Dimmer', ...className])} {...rest}>
|
||||
<div className="Dimmer__inner">{children}</div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
|
||||
export const Divider = (props) => {
|
||||
const { vertical, hidden } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Divider',
|
||||
hidden && 'Divider--hidden',
|
||||
vertical ? 'Divider--vertical' : 'Divider--horizontal',
|
||||
])}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,287 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp } from 'common/math';
|
||||
import { pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
|
||||
const DEFAULT_UPDATE_RATE = 400;
|
||||
|
||||
/**
|
||||
* Reduces screen offset to a single number based on the matrix provided.
|
||||
*/
|
||||
const getScalarScreenOffset = (e, matrix) => {
|
||||
return e.screenX * matrix[0] + e.screenY * matrix[1];
|
||||
};
|
||||
|
||||
export class DraggableControl extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
value: props.value,
|
||||
dragging: false,
|
||||
editing: false,
|
||||
internalValue: null,
|
||||
origin: null,
|
||||
suppressingFlicker: false,
|
||||
};
|
||||
|
||||
// Suppresses flickering while the value propagates through the backend
|
||||
this.flickerTimer = null;
|
||||
this.suppressFlicker = () => {
|
||||
const { suppressFlicker } = this.props;
|
||||
if (suppressFlicker > 0) {
|
||||
this.setState({
|
||||
suppressingFlicker: true,
|
||||
});
|
||||
clearTimeout(this.flickerTimer);
|
||||
this.flickerTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
suppressingFlicker: false,
|
||||
});
|
||||
}, suppressFlicker);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleDragStart = (e) => {
|
||||
const { value, dragMatrix } = this.props;
|
||||
const { editing } = this.state;
|
||||
if (editing) {
|
||||
return;
|
||||
}
|
||||
document.body.style['pointer-events'] = 'none';
|
||||
this.ref = e.target;
|
||||
this.setState({
|
||||
dragging: false,
|
||||
origin: getScalarScreenOffset(e, dragMatrix),
|
||||
value,
|
||||
internalValue: value,
|
||||
});
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({
|
||||
dragging: true,
|
||||
});
|
||||
}, 250);
|
||||
this.dragInterval = setInterval(() => {
|
||||
const { dragging, value } = this.state;
|
||||
const { onDrag } = this.props;
|
||||
if (dragging && onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
}, this.props.updateRate || DEFAULT_UPDATE_RATE);
|
||||
document.addEventListener('mousemove', this.handleDragMove);
|
||||
document.addEventListener('mouseup', this.handleDragEnd);
|
||||
};
|
||||
|
||||
this.handleDragMove = (e) => {
|
||||
// prettier-ignore
|
||||
const {
|
||||
minValue,
|
||||
maxValue,
|
||||
step,
|
||||
stepPixelSize,
|
||||
dragMatrix,
|
||||
} = this.props;
|
||||
this.setState((prevState) => {
|
||||
const state = { ...prevState };
|
||||
const offset = getScalarScreenOffset(e, dragMatrix) - state.origin;
|
||||
if (prevState.dragging) {
|
||||
const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
|
||||
// Translate mouse movement to value
|
||||
// Give it some headroom (by increasing clamp range by 1 step)
|
||||
state.internalValue = clamp(
|
||||
state.internalValue + (offset * step) / stepPixelSize,
|
||||
minValue - step,
|
||||
maxValue + step
|
||||
);
|
||||
// Clamp the final value
|
||||
state.value = clamp(
|
||||
state.internalValue - (state.internalValue % step) + stepOffset,
|
||||
minValue,
|
||||
maxValue
|
||||
);
|
||||
state.origin = getScalarScreenOffset(e, dragMatrix);
|
||||
} else if (Math.abs(offset) > 4) {
|
||||
state.dragging = true;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
this.handleDragEnd = (e) => {
|
||||
const { onChange, onDrag } = this.props;
|
||||
const { dragging, value, internalValue } = this.state;
|
||||
document.body.style['pointer-events'] = 'auto';
|
||||
clearTimeout(this.timer);
|
||||
clearInterval(this.dragInterval);
|
||||
this.setState({
|
||||
dragging: false,
|
||||
editing: !dragging,
|
||||
origin: null,
|
||||
});
|
||||
document.removeEventListener('mousemove', this.handleDragMove);
|
||||
document.removeEventListener('mouseup', this.handleDragEnd);
|
||||
if (dragging) {
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
} else if (this.inputRef) {
|
||||
const input = this.inputRef.current;
|
||||
input.value = internalValue;
|
||||
// IE8: Dies when trying to focus a hidden element
|
||||
// (Error: Object does not support this action)
|
||||
try {
|
||||
input.focus();
|
||||
input.select();
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dragging,
|
||||
editing,
|
||||
value: intermediateValue,
|
||||
suppressingFlicker,
|
||||
} = this.state;
|
||||
const {
|
||||
animated,
|
||||
value,
|
||||
unit,
|
||||
minValue,
|
||||
maxValue,
|
||||
unclamped,
|
||||
format,
|
||||
onChange,
|
||||
onDrag,
|
||||
children,
|
||||
// Input props
|
||||
height,
|
||||
lineHeight,
|
||||
fontSize,
|
||||
} = this.props;
|
||||
let displayValue = value;
|
||||
if (dragging || suppressingFlicker) {
|
||||
displayValue = intermediateValue;
|
||||
}
|
||||
// prettier-ignore
|
||||
const displayElement = (
|
||||
<>
|
||||
{
|
||||
(animated && !dragging && !suppressingFlicker) ?
|
||||
(<AnimatedNumber value={displayValue} format={format} />) :
|
||||
(format ? format(displayValue) : displayValue)
|
||||
}
|
||||
|
||||
{ (unit ? ' ' + unit : '') }
|
||||
</>
|
||||
);
|
||||
|
||||
// Setup an input element
|
||||
// Handles direct input via the keyboard
|
||||
const inputElement = (
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="NumberInput__input"
|
||||
style={{
|
||||
display: !editing ? 'none' : undefined,
|
||||
height: height,
|
||||
'line-height': lineHeight,
|
||||
'font-size': fontSize,
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (!editing) {
|
||||
return;
|
||||
}
|
||||
let value;
|
||||
if (unclamped) {
|
||||
value = parseFloat(e.target.value);
|
||||
} else {
|
||||
value = clamp(parseFloat(e.target.value), minValue, maxValue);
|
||||
}
|
||||
if (Number.isNaN(value)) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
editing: false,
|
||||
value,
|
||||
});
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
let value;
|
||||
if (unclamped) {
|
||||
value = parseFloat(e.target.value);
|
||||
} else {
|
||||
value = clamp(parseFloat(e.target.value), minValue, maxValue);
|
||||
}
|
||||
if (Number.isNaN(value)) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
editing: false,
|
||||
value,
|
||||
});
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
// Return a part of the state for higher-level components to use.
|
||||
return children({
|
||||
dragging,
|
||||
editing,
|
||||
value,
|
||||
displayValue,
|
||||
displayElement,
|
||||
inputElement,
|
||||
handleDragStart: this.handleDragStart,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
DraggableControl.defaultHooks = pureComponentHooks;
|
||||
DraggableControl.defaultProps = {
|
||||
minValue: -Infinity,
|
||||
maxValue: +Infinity,
|
||||
step: 1,
|
||||
stepPixelSize: 1,
|
||||
suppressFlicker: 50,
|
||||
dragMatrix: [1, 0],
|
||||
};
|
||||
@@ -1,395 +0,0 @@
|
||||
import { createPopper, VirtualElement } from '@popperjs/core';
|
||||
import { classes } from 'common/react';
|
||||
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
|
||||
import { Box, BoxProps } from './Box';
|
||||
import { Button } from './Button';
|
||||
import { Icon } from './Icon';
|
||||
import { Stack } from './Stack';
|
||||
|
||||
export interface DropdownEntry {
|
||||
displayText: string | number | InfernoNode;
|
||||
value: string | number | Enumerator;
|
||||
}
|
||||
|
||||
type DropdownUniqueProps = {
|
||||
options: string[] | DropdownEntry[];
|
||||
icon?: string;
|
||||
iconRotation?: number;
|
||||
clipSelectedText?: boolean;
|
||||
width?: string;
|
||||
menuWidth?: string;
|
||||
over?: boolean;
|
||||
color?: string;
|
||||
nochevron?: boolean;
|
||||
displayText?: string | number | InfernoNode;
|
||||
onClick?: (event) => void;
|
||||
// you freaks really are just doing anything with this shit
|
||||
selected?: any;
|
||||
onSelected?: (selected: any) => void;
|
||||
buttons?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownProps = BoxProps & DropdownUniqueProps;
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
placement: 'left-start',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'eventListeners',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
const NULL_RECT: DOMRect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => null,
|
||||
} as const;
|
||||
|
||||
type DropdownState = {
|
||||
selected?: string;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const DROPDOWN_DEFAULT_CLASSNAMES = 'Layout Dropdown__menu';
|
||||
const DROPDOWN_SCROLL_CLASSNAMES = 'Layout Dropdown__menu-scroll';
|
||||
|
||||
export class Dropdown extends Component<DropdownProps, DropdownState> {
|
||||
static renderedMenu: HTMLDivElement | undefined;
|
||||
static singletonPopper: ReturnType<typeof createPopper> | undefined;
|
||||
static currentOpenMenu: Element | undefined;
|
||||
static virtualElement: VirtualElement = {
|
||||
getBoundingClientRect: () =>
|
||||
Dropdown.currentOpenMenu?.getBoundingClientRect() ?? NULL_RECT,
|
||||
};
|
||||
menuContents: any;
|
||||
state: DropdownState = {
|
||||
open: false,
|
||||
selected: this.props.selected,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (this.state.open) {
|
||||
this.setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
getDOMNode() {
|
||||
return findDOMfromVNode(this.$LI, true);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const domNode = this.getDOMNode();
|
||||
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openMenu() {
|
||||
let renderedMenu = Dropdown.renderedMenu;
|
||||
if (renderedMenu === undefined) {
|
||||
renderedMenu = document.createElement('div');
|
||||
renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
|
||||
document.body.appendChild(renderedMenu);
|
||||
Dropdown.renderedMenu = renderedMenu;
|
||||
}
|
||||
|
||||
const domNode = this.getDOMNode()!;
|
||||
Dropdown.currentOpenMenu = domNode;
|
||||
|
||||
renderedMenu.scrollTop = 0;
|
||||
renderedMenu.style.width =
|
||||
this.props.menuWidth ||
|
||||
// Hack, but domNode should *always* be the parent control meaning it will have width
|
||||
// @ts-ignore
|
||||
`${domNode.offsetWidth}px`;
|
||||
renderedMenu.style.opacity = '1';
|
||||
renderedMenu.style.pointerEvents = 'auto';
|
||||
|
||||
// ie hack
|
||||
// ie has this bizarre behavior where focus just silently fails if the
|
||||
// element being targeted "isn't ready"
|
||||
// 400 is probably way too high, but the lack of hotloading is testing my
|
||||
// patience on tuning it
|
||||
// I'm beyond giving a shit at this point it fucking works whatever
|
||||
setTimeout(() => {
|
||||
Dropdown.renderedMenu?.focus();
|
||||
}, 400);
|
||||
this.renderMenuContent();
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
if (Dropdown.currentOpenMenu !== this.getDOMNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Dropdown.currentOpenMenu = undefined;
|
||||
Dropdown.renderedMenu!.style.opacity = '0';
|
||||
Dropdown.renderedMenu!.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.closeMenu();
|
||||
this.setOpen(false);
|
||||
}
|
||||
|
||||
renderMenuContent() {
|
||||
const renderedMenu = Dropdown.renderedMenu;
|
||||
if (!renderedMenu) {
|
||||
return;
|
||||
}
|
||||
if (renderedMenu.offsetHeight > 200) {
|
||||
renderedMenu.className = DROPDOWN_SCROLL_CLASSNAMES;
|
||||
} else {
|
||||
renderedMenu.className = DROPDOWN_DEFAULT_CLASSNAMES;
|
||||
}
|
||||
|
||||
const { options = [] } = this.props;
|
||||
const ops = options.map((option) => {
|
||||
let value, displayText;
|
||||
|
||||
if (typeof option === 'string') {
|
||||
displayText = option;
|
||||
value = option;
|
||||
} else if (option !== null) {
|
||||
displayText = option.displayText;
|
||||
value = option.value;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className={classes([
|
||||
'Dropdown__menuentry',
|
||||
this.state.selected === value && 'selected',
|
||||
])}
|
||||
onClick={() => {
|
||||
this.setSelected(value);
|
||||
}}>
|
||||
{displayText}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const to_render = ops.length ? ops : 'No Options Found';
|
||||
|
||||
render(
|
||||
<div>{to_render}</div>,
|
||||
renderedMenu,
|
||||
() => {
|
||||
let singletonPopper = Dropdown.singletonPopper;
|
||||
if (singletonPopper === undefined) {
|
||||
singletonPopper = createPopper(
|
||||
Dropdown.virtualElement,
|
||||
renderedMenu!,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
placement: 'bottom-start',
|
||||
}
|
||||
);
|
||||
|
||||
Dropdown.singletonPopper = singletonPopper;
|
||||
} else {
|
||||
singletonPopper.setOptions({
|
||||
...DEFAULT_OPTIONS,
|
||||
placement: 'bottom-start',
|
||||
});
|
||||
|
||||
singletonPopper.update();
|
||||
}
|
||||
},
|
||||
this.context
|
||||
);
|
||||
}
|
||||
|
||||
setOpen(open: boolean) {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
open,
|
||||
}));
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
this.openMenu();
|
||||
window.addEventListener('click', this.handleClick);
|
||||
});
|
||||
} else {
|
||||
this.closeMenu();
|
||||
window.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(selected: string) {
|
||||
this.setState((state) => ({
|
||||
...state,
|
||||
selected,
|
||||
}));
|
||||
this.setOpen(false);
|
||||
if (this.props.onSelected) {
|
||||
this.props.onSelected(selected);
|
||||
}
|
||||
}
|
||||
|
||||
getOptionValue(option): string {
|
||||
return typeof option === 'string' ? option : option.value;
|
||||
}
|
||||
|
||||
getSelectedIndex(): number {
|
||||
const selected = this.state.selected || this.props.selected;
|
||||
const { options = [] } = this.props;
|
||||
|
||||
return options.findIndex((option) => {
|
||||
return this.getOptionValue(option) === selected;
|
||||
});
|
||||
}
|
||||
|
||||
toPrevious(): void {
|
||||
if (this.props.options.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedIndex = this.getSelectedIndex();
|
||||
const startIndex = 0;
|
||||
const endIndex = this.props.options.length - 1;
|
||||
|
||||
const hasSelected = selectedIndex >= 0;
|
||||
if (!hasSelected) {
|
||||
selectedIndex = startIndex;
|
||||
}
|
||||
|
||||
const previousIndex =
|
||||
selectedIndex === startIndex ? endIndex : selectedIndex - 1;
|
||||
|
||||
this.setSelected(this.getOptionValue(this.props.options[previousIndex]));
|
||||
}
|
||||
|
||||
toNext(): void {
|
||||
if (this.props.options.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedIndex = this.getSelectedIndex();
|
||||
const startIndex = 0;
|
||||
const endIndex = this.props.options.length - 1;
|
||||
|
||||
const hasSelected = selectedIndex >= 0;
|
||||
if (!hasSelected) {
|
||||
selectedIndex = endIndex;
|
||||
}
|
||||
|
||||
const nextIndex =
|
||||
selectedIndex === endIndex ? startIndex : selectedIndex + 1;
|
||||
|
||||
this.setSelected(this.getOptionValue(this.props.options[nextIndex]));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const {
|
||||
icon,
|
||||
iconRotation,
|
||||
iconSpin,
|
||||
clipSelectedText = true,
|
||||
color = 'default',
|
||||
dropdownStyle,
|
||||
over,
|
||||
nochevron,
|
||||
width,
|
||||
onClick,
|
||||
onSelected,
|
||||
selected,
|
||||
disabled,
|
||||
displayText,
|
||||
buttons,
|
||||
...boxProps
|
||||
} = props;
|
||||
const { className, ...rest } = boxProps;
|
||||
|
||||
const adjustedOpen = over ? !this.state.open : this.state.open;
|
||||
|
||||
return (
|
||||
<Stack fill>
|
||||
<Stack.Item width={width}>
|
||||
<Box
|
||||
width={'100%'}
|
||||
className={classes([
|
||||
'Dropdown__control',
|
||||
'Button',
|
||||
'Button--color--' + color,
|
||||
disabled && 'Button--disabled',
|
||||
className,
|
||||
])}
|
||||
onClick={(event) => {
|
||||
if (disabled && !this.state.open) {
|
||||
return;
|
||||
}
|
||||
this.setOpen(!this.state.open);
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
}}
|
||||
{...rest}>
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
rotation={iconRotation}
|
||||
spin={iconSpin}
|
||||
mr={1}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="Dropdown__selected-text"
|
||||
style={{
|
||||
overflow: clipSelectedText ? 'hidden' : 'visible',
|
||||
}}>
|
||||
{displayText || this.state.selected}
|
||||
</span>
|
||||
{nochevron || (
|
||||
<span className="Dropdown__arrow-button">
|
||||
<Icon name={adjustedOpen ? 'chevron-up' : 'chevron-down'} />
|
||||
</span>
|
||||
)}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
{buttons && (
|
||||
<>
|
||||
<Stack.Item height={'100%'}>
|
||||
<Button
|
||||
height={'100%'}
|
||||
icon="chevron-left"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toPrevious();
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
<Stack.Item height={'100%'}>
|
||||
<Button
|
||||
height={'100%'}
|
||||
icon="chevron-right"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toNext();
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Box } from './Box';
|
||||
import { Component, Fragment } from 'inferno';
|
||||
|
||||
export class FakeTerminal extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.timer = null;
|
||||
this.state = {
|
||||
currentIndex: 0,
|
||||
currentDisplay: [],
|
||||
};
|
||||
}
|
||||
|
||||
tick() {
|
||||
const { props, state } = this;
|
||||
if (state.currentIndex <= props.allMessages.length) {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
currentIndex: prevState.currentIndex + 1,
|
||||
};
|
||||
});
|
||||
const { currentDisplay } = state;
|
||||
currentDisplay.push(props.allMessages[state.currentIndex]);
|
||||
} else {
|
||||
clearTimeout(this.timer);
|
||||
setTimeout(props.onFinished, props.finishedTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { linesPerSecond = 2.5 } = this.props;
|
||||
this.timer = setInterval(() => this.tick(), 1000 / linesPerSecond);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Box m={1}>
|
||||
{this.state.currentDisplay.map((value) => (
|
||||
<Fragment key={value}>
|
||||
{value}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Component, createRef, RefObject } from 'inferno';
|
||||
|
||||
const DEFAULT_ACCEPTABLE_DIFFERENCE = 5;
|
||||
|
||||
type Props = {
|
||||
acceptableDifference?: number;
|
||||
maxWidth: number;
|
||||
maxFontSize: number;
|
||||
native?: HTMLAttributes<HTMLDivElement>;
|
||||
};
|
||||
|
||||
type State = {
|
||||
fontSize: number;
|
||||
};
|
||||
|
||||
export class FitText extends Component<Props, State> {
|
||||
ref: RefObject<HTMLDivElement> = createRef();
|
||||
state: State = {
|
||||
fontSize: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.resize = this.resize.bind(this);
|
||||
|
||||
window.addEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.children !== this.props.children) {
|
||||
this.resize();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.resize);
|
||||
}
|
||||
|
||||
resize() {
|
||||
const element = this.ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxWidth = this.props.maxWidth;
|
||||
|
||||
let start = 0;
|
||||
let end = this.props.maxFontSize;
|
||||
|
||||
for (let _ = 0; _ < 10; _++) {
|
||||
const middle = Math.round((start + end) / 2);
|
||||
element.style.fontSize = `${middle}px`;
|
||||
|
||||
const difference = element.offsetWidth - maxWidth;
|
||||
|
||||
if (difference > 0) {
|
||||
end = middle;
|
||||
} else if (
|
||||
difference <
|
||||
(this.props.acceptableDifference ?? DEFAULT_ACCEPTABLE_DIFFERENCE)
|
||||
) {
|
||||
start = middle;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fontSize: Math.round((start + end) / 2),
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.resize();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span
|
||||
ref={this.ref}
|
||||
style={{
|
||||
'font-size': `${this.state.fontSize}px`,
|
||||
...(typeof this.props.native?.style === 'object' &&
|
||||
this.props.native.style),
|
||||
}}>
|
||||
{this.props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
|
||||
import { BoxProps, computeBoxClassName, computeBoxProps, unit } from './Box';
|
||||
|
||||
export type FlexProps = BoxProps & {
|
||||
direction?: string | BooleanLike;
|
||||
wrap?: string | BooleanLike;
|
||||
align?: string | BooleanLike;
|
||||
justify?: string | BooleanLike;
|
||||
inline?: BooleanLike;
|
||||
};
|
||||
|
||||
export const computeFlexClassName = (props: FlexProps) => {
|
||||
return classes([
|
||||
'Flex',
|
||||
props.inline && 'Flex--inline',
|
||||
Byond.IS_LTE_IE10 && 'Flex--iefix',
|
||||
Byond.IS_LTE_IE10 && props.direction === 'column' && 'Flex--iefix--column',
|
||||
computeBoxClassName(props),
|
||||
]);
|
||||
};
|
||||
|
||||
export const computeFlexProps = (props: FlexProps) => {
|
||||
const { className, direction, wrap, align, justify, inline, ...rest } = props;
|
||||
return computeBoxProps({
|
||||
style: {
|
||||
...rest.style,
|
||||
'flex-direction': direction,
|
||||
'flex-wrap': wrap === true ? 'wrap' : wrap,
|
||||
'align-items': align,
|
||||
'justify-content': justify,
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
};
|
||||
|
||||
export const Flex = (props) => {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([className, computeFlexClassName(rest)])}
|
||||
{...computeFlexProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Flex.defaultHooks = pureComponentHooks;
|
||||
|
||||
export type FlexItemProps = BoxProps & {
|
||||
grow?: number;
|
||||
order?: number;
|
||||
shrink?: number;
|
||||
basis?: string | BooleanLike;
|
||||
align?: string | BooleanLike;
|
||||
};
|
||||
|
||||
export const computeFlexItemClassName = (props: FlexItemProps) => {
|
||||
return classes([
|
||||
'Flex__item',
|
||||
Byond.IS_LTE_IE10 && 'Flex__item--iefix',
|
||||
computeBoxClassName(props),
|
||||
]);
|
||||
};
|
||||
|
||||
export const computeFlexItemProps = (props: FlexItemProps) => {
|
||||
// prettier-ignore
|
||||
const {
|
||||
className,
|
||||
style,
|
||||
grow,
|
||||
order,
|
||||
shrink,
|
||||
basis,
|
||||
align,
|
||||
...rest
|
||||
} = props;
|
||||
// prettier-ignore
|
||||
const computedBasis = basis
|
||||
// IE11: Set basis to specified width if it's known, which fixes certain
|
||||
// bugs when rendering tables inside the flex.
|
||||
?? props.width
|
||||
// If grow is used, basis should be set to 0 to be consistent with
|
||||
// flex css shorthand `flex: 1`.
|
||||
?? (grow !== undefined ? 0 : undefined);
|
||||
return computeBoxProps({
|
||||
style: {
|
||||
...style,
|
||||
'flex-grow': grow !== undefined && Number(grow),
|
||||
'flex-shrink': shrink !== undefined && Number(shrink),
|
||||
'flex-basis': unit(computedBasis),
|
||||
'order': order,
|
||||
'align-self': align,
|
||||
},
|
||||
...rest,
|
||||
});
|
||||
};
|
||||
|
||||
const FlexItem = (props) => {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([className, computeFlexItemClassName(props)])}
|
||||
{...computeFlexItemProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
FlexItem.defaultHooks = pureComponentHooks;
|
||||
|
||||
Flex.Item = FlexItem;
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Table } from './Table';
|
||||
import { pureComponentHooks } from 'common/react';
|
||||
|
||||
/** @deprecated */
|
||||
export const Grid = (props) => {
|
||||
const { children, ...rest } = props;
|
||||
return (
|
||||
<Table {...rest}>
|
||||
<Table.Row>{children}</Table.Row>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
Grid.defaultHooks = pureComponentHooks;
|
||||
|
||||
/** @deprecated */
|
||||
export const GridColumn = (props) => {
|
||||
const { size = 1, style, ...rest } = props;
|
||||
return (
|
||||
<Table.Cell
|
||||
style={{
|
||||
width: size + '%',
|
||||
...style,
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Grid.defaultHooks = pureComponentHooks;
|
||||
|
||||
Grid.Column = GridColumn;
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @author Original Aleksej Komarov
|
||||
* @author Changes ThePotato97
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { InfernoNode } from 'inferno';
|
||||
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
|
||||
|
||||
const FA_OUTLINE_REGEX = /-o$/;
|
||||
|
||||
type IconPropsUnique = {
|
||||
name: string;
|
||||
size?: number;
|
||||
spin?: boolean;
|
||||
className?: string;
|
||||
rotation?: number;
|
||||
style?: string | CSSProperties;
|
||||
};
|
||||
|
||||
export type IconProps = IconPropsUnique & BoxProps;
|
||||
|
||||
export const Icon = (props: IconProps) => {
|
||||
let { style, ...restlet } = props;
|
||||
const { name, size, spin, className, rotation, ...rest } = restlet;
|
||||
|
||||
if (size) {
|
||||
if (!style) {
|
||||
style = {};
|
||||
}
|
||||
style['font-size'] = size * 100 + '%';
|
||||
}
|
||||
if (rotation) {
|
||||
if (!style) {
|
||||
style = {};
|
||||
}
|
||||
style['transform'] = `rotate(${rotation}deg)`;
|
||||
}
|
||||
rest.style = style;
|
||||
|
||||
const boxProps = computeBoxProps(rest);
|
||||
|
||||
let iconClass = '';
|
||||
if (name.startsWith('tg-')) {
|
||||
// tgfont icon
|
||||
iconClass = name;
|
||||
} else {
|
||||
// font awesome icon
|
||||
const faRegular = FA_OUTLINE_REGEX.test(name);
|
||||
const faName = name.replace(FA_OUTLINE_REGEX, '');
|
||||
const preprendFa = !faName.startsWith('fa-');
|
||||
|
||||
iconClass = faRegular ? 'far ' : 'fas ';
|
||||
if (preprendFa) {
|
||||
iconClass += 'fa-';
|
||||
}
|
||||
iconClass += faName;
|
||||
if (spin) {
|
||||
iconClass += ' fa-spin';
|
||||
}
|
||||
}
|
||||
return (
|
||||
<i
|
||||
className={classes([
|
||||
'Icon',
|
||||
iconClass,
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...boxProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Icon.defaultHooks = pureComponentHooks;
|
||||
|
||||
type IconStackUnique = {
|
||||
children: InfernoNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export type IconStackProps = IconStackUnique & BoxProps;
|
||||
|
||||
export const IconStack = (props: IconStackProps) => {
|
||||
const { className, children, ...rest } = props;
|
||||
return (
|
||||
<span
|
||||
class={classes(['IconStack', className, computeBoxClassName(rest)])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
Icon.Stack = IconStack;
|
||||
@@ -1,188 +0,0 @@
|
||||
import { computeBoxProps } from './Box';
|
||||
import { Stack } from './Stack';
|
||||
import { ProgressBar } from './ProgressBar';
|
||||
import { Button } from './Button';
|
||||
import { Component } from 'inferno';
|
||||
|
||||
const ZOOM_MIN_VAL = 0.5;
|
||||
const ZOOM_MAX_VAL = 1.5;
|
||||
|
||||
const ZOOM_INCREMENT = 0.1;
|
||||
|
||||
export class InfinitePlane extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
mouseDown: false,
|
||||
|
||||
left: 0,
|
||||
top: 0,
|
||||
|
||||
lastLeft: 0,
|
||||
lastTop: 0,
|
||||
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.handleZoomIncrease = this.handleZoomIncrease.bind(this);
|
||||
this.handleZoomDecrease = this.handleZoomDecrease.bind(this);
|
||||
this.onMouseUp = this.onMouseUp.bind(this);
|
||||
|
||||
this.doOffsetMouse = this.doOffsetMouse.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
|
||||
window.addEventListener('mousedown', this.doOffsetMouse);
|
||||
window.addEventListener('mousemove', this.doOffsetMouse);
|
||||
window.addEventListener('mouseup', this.doOffsetMouse);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
|
||||
window.removeEventListener('mousedown', this.doOffsetMouse);
|
||||
window.removeEventListener('mousemove', this.doOffsetMouse);
|
||||
window.removeEventListener('mouseup', this.doOffsetMouse);
|
||||
}
|
||||
|
||||
doOffsetMouse(event) {
|
||||
const { zoom } = this.state;
|
||||
event.screenZoomX = event.screenX * Math.pow(zoom, -1);
|
||||
event.screenZoomY = event.screenY * Math.pow(zoom, -1);
|
||||
}
|
||||
|
||||
handleMouseDown(event) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
mouseDown: true,
|
||||
lastLeft: event.clientX - state.left,
|
||||
lastTop: event.clientY - state.top,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
this.setState({
|
||||
mouseDown: false,
|
||||
});
|
||||
}
|
||||
|
||||
handleZoomIncrease(event) {
|
||||
const { onZoomChange } = this.props;
|
||||
const { zoom } = this.state;
|
||||
const newZoomValue = Math.min(zoom + ZOOM_INCREMENT, ZOOM_MAX_VAL);
|
||||
this.setState({
|
||||
zoom: newZoomValue,
|
||||
});
|
||||
if (onZoomChange) {
|
||||
onZoomChange(newZoomValue);
|
||||
}
|
||||
}
|
||||
|
||||
handleZoomDecrease(event) {
|
||||
const { onZoomChange } = this.props;
|
||||
const { zoom } = this.state;
|
||||
const newZoomValue = Math.max(zoom - ZOOM_INCREMENT, ZOOM_MIN_VAL);
|
||||
this.setState({
|
||||
zoom: newZoomValue,
|
||||
});
|
||||
|
||||
if (onZoomChange) {
|
||||
onZoomChange(newZoomValue);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove(event) {
|
||||
const { onBackgroundMoved, initialLeft = 0, initialTop = 0 } = this.props;
|
||||
if (this.state.mouseDown) {
|
||||
let newX, newY;
|
||||
this.setState((state) => {
|
||||
newX = event.clientX - state.lastLeft;
|
||||
newY = event.clientY - state.lastTop;
|
||||
return {
|
||||
left: newX,
|
||||
top: newY,
|
||||
};
|
||||
});
|
||||
if (onBackgroundMoved) {
|
||||
onBackgroundMoved(newX + initialLeft, newY + initialTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
backgroundImage,
|
||||
imageWidth,
|
||||
initialLeft = 0,
|
||||
initialTop = 0,
|
||||
...rest
|
||||
} = this.props;
|
||||
const { left, top, zoom } = this.state;
|
||||
|
||||
const finalLeft = initialLeft + left;
|
||||
const finalTop = initialTop + top;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this.ref}
|
||||
{...computeBoxProps({
|
||||
...rest,
|
||||
style: {
|
||||
...rest.style,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
})}>
|
||||
<div
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
style={{
|
||||
'position': 'fixed',
|
||||
'height': '100%',
|
||||
'width': '100%',
|
||||
'background-image': `url("${backgroundImage}")`,
|
||||
'background-position': `${finalLeft}px ${finalTop}px`,
|
||||
'background-repeat': 'repeat',
|
||||
'background-size': `${zoom * imageWidth}px`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onMouseMove={this.handleMouseMove}
|
||||
style={{
|
||||
'position': 'fixed',
|
||||
'transform': `translate(${finalLeft}px, ${finalTop}px) scale(${zoom})`,
|
||||
'transform-origin': 'top left',
|
||||
'height': '100%',
|
||||
'width': '100%',
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<Stack position="absolute" width="100%">
|
||||
<Stack.Item>
|
||||
<Button icon="minus" onClick={this.handleZoomDecrease} />
|
||||
</Stack.Item>
|
||||
<Stack.Item grow={1}>
|
||||
<ProgressBar
|
||||
minValue={ZOOM_MIN_VAL}
|
||||
value={zoom}
|
||||
maxValue={ZOOM_MAX_VAL}>
|
||||
{zoom}x
|
||||
</ProgressBar>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Button icon="plus" onClick={this.handleZoomIncrease} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_ENTER, KEY_ESCAPE } from 'common/keycodes';
|
||||
import { classes } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
|
||||
// prettier-ignore
|
||||
export const toInputValue = value => (
|
||||
typeof value !== 'number' && typeof value !== 'string'
|
||||
? ''
|
||||
: String(value)
|
||||
);
|
||||
|
||||
export class Input extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
editing: false,
|
||||
};
|
||||
this.handleInput = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onInput } = this.props;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleFocus = (e) => {
|
||||
const { editing } = this.state;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
};
|
||||
this.handleBlur = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onChange } = this.props;
|
||||
if (editing) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.handleKeyDown = (e) => {
|
||||
const { onInput, onChange, onEnter } = this.props;
|
||||
if (e.keyCode === KEY_ENTER) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
if (onEnter) {
|
||||
onEnter(e, e.target.value);
|
||||
}
|
||||
if (this.props.selfClear) {
|
||||
e.target.value = '';
|
||||
} else {
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === KEY_ESCAPE) {
|
||||
if (this.props.onEscape) {
|
||||
this.props.onEscape(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setEditing(false);
|
||||
e.target.value = toInputValue(this.props.value);
|
||||
e.target.blur();
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const nextValue = this.props.value;
|
||||
const input = this.inputRef.current;
|
||||
if (input) {
|
||||
input.value = toInputValue(nextValue);
|
||||
}
|
||||
|
||||
if (this.props.autoFocus || this.props.autoSelect) {
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
|
||||
if (this.props.autoSelect) {
|
||||
input.select();
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { editing } = this.state;
|
||||
const prevValue = prevProps.value;
|
||||
const nextValue = this.props.value;
|
||||
const input = this.inputRef.current;
|
||||
if (input && !editing && prevValue !== nextValue) {
|
||||
input.value = toInputValue(nextValue);
|
||||
}
|
||||
}
|
||||
|
||||
setEditing(editing) {
|
||||
this.setState({ editing });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
// Input only props
|
||||
const {
|
||||
selfClear,
|
||||
onInput,
|
||||
onChange,
|
||||
onEnter,
|
||||
value,
|
||||
maxLength,
|
||||
placeholder,
|
||||
...boxProps
|
||||
} = props;
|
||||
// Box props
|
||||
const { className, fluid, monospace, ...rest } = boxProps;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'Input',
|
||||
fluid && 'Input--fluid',
|
||||
monospace && 'Input--monospace',
|
||||
className,
|
||||
])}
|
||||
{...rest}>
|
||||
<div className="Input__baseline">.</div>
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="Input__input"
|
||||
placeholder={placeholder}
|
||||
onInput={this.handleInput}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Component } from 'inferno';
|
||||
import { KeyEvent } from '../events';
|
||||
import { listenForKeyEvents } from '../hotkeys';
|
||||
|
||||
type KeyListenerProps = Partial<{
|
||||
onKey: (key: KeyEvent) => void;
|
||||
onKeyDown: (key: KeyEvent) => void;
|
||||
onKeyUp: (key: KeyEvent) => void;
|
||||
}>;
|
||||
|
||||
export class KeyListener extends Component<KeyListenerProps> {
|
||||
dispose: () => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.dispose = listenForKeyEvents((key) => {
|
||||
if (this.props.onKey) {
|
||||
this.props.onKey(key);
|
||||
}
|
||||
|
||||
if (key.isDown() && this.props.onKeyDown) {
|
||||
this.props.onKeyDown(key);
|
||||
}
|
||||
|
||||
if (key.isUp() && this.props.onKeyUp) {
|
||||
this.props.onKeyUp(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { keyOfMatchingRange, scale } from 'common/math';
|
||||
import { classes } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { DraggableControl } from './DraggableControl';
|
||||
import { NumberInput } from './NumberInput';
|
||||
|
||||
export const Knob = (props) => {
|
||||
// IE8: I don't want to support a yet another component on IE8.
|
||||
// IE8: It also can't handle SVG.
|
||||
if (Byond.IS_LTE_IE8) {
|
||||
return <NumberInput {...props} />;
|
||||
}
|
||||
const {
|
||||
// Draggable props (passthrough)
|
||||
animated,
|
||||
format,
|
||||
maxValue,
|
||||
minValue,
|
||||
unclamped,
|
||||
onChange,
|
||||
onDrag,
|
||||
step,
|
||||
stepPixelSize,
|
||||
suppressFlicker,
|
||||
unit,
|
||||
value,
|
||||
// Own props
|
||||
className,
|
||||
style,
|
||||
fillValue,
|
||||
color,
|
||||
ranges = {},
|
||||
size = 1,
|
||||
bipolar,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<DraggableControl
|
||||
dragMatrix={[0, -1]}
|
||||
{...{
|
||||
animated,
|
||||
format,
|
||||
maxValue,
|
||||
minValue,
|
||||
unclamped,
|
||||
onChange,
|
||||
onDrag,
|
||||
step,
|
||||
stepPixelSize,
|
||||
suppressFlicker,
|
||||
unit,
|
||||
value,
|
||||
}}>
|
||||
{(control) => {
|
||||
const {
|
||||
dragging,
|
||||
editing,
|
||||
value,
|
||||
displayValue,
|
||||
displayElement,
|
||||
inputElement,
|
||||
handleDragStart,
|
||||
} = control;
|
||||
const scaledFillValue = scale(
|
||||
fillValue ?? displayValue,
|
||||
minValue,
|
||||
maxValue
|
||||
);
|
||||
const scaledDisplayValue = scale(displayValue, minValue, maxValue);
|
||||
const effectiveColor =
|
||||
color || keyOfMatchingRange(fillValue ?? value, ranges) || 'default';
|
||||
const rotation = Math.min((scaledDisplayValue - 0.5) * 270, 225);
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Knob',
|
||||
'Knob--color--' + effectiveColor,
|
||||
bipolar && 'Knob--bipolar',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps({
|
||||
style: {
|
||||
'font-size': size + 'em',
|
||||
...style,
|
||||
},
|
||||
...rest,
|
||||
})}
|
||||
onMouseDown={handleDragStart}>
|
||||
<div className="Knob__circle">
|
||||
<div
|
||||
className="Knob__cursorBox"
|
||||
style={{
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
}}>
|
||||
<div className="Knob__cursor" />
|
||||
</div>
|
||||
</div>
|
||||
{dragging && (
|
||||
<div className="Knob__popupValue">{displayElement}</div>
|
||||
)}
|
||||
<svg
|
||||
className="Knob__ring Knob__ringTrackPivot"
|
||||
viewBox="0 0 100 100">
|
||||
<circle className="Knob__ringTrack" cx="50" cy="50" r="50" />
|
||||
</svg>
|
||||
<svg
|
||||
className="Knob__ring Knob__ringFillPivot"
|
||||
viewBox="0 0 100 100">
|
||||
<circle
|
||||
className="Knob__ringFill"
|
||||
style={{
|
||||
'stroke-dashoffset': Math.max(
|
||||
((bipolar ? 2.75 : 2.0) - scaledFillValue * 1.5) *
|
||||
Math.PI *
|
||||
50,
|
||||
0
|
||||
),
|
||||
}}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="50"
|
||||
/>
|
||||
</svg>
|
||||
{inputElement}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DraggableControl>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Flex } from './Flex';
|
||||
|
||||
export const LabeledControls = (props) => {
|
||||
const { children, wrap, ...rest } = props;
|
||||
return (
|
||||
<Flex
|
||||
mx={-0.5}
|
||||
wrap={wrap}
|
||||
align="stretch"
|
||||
justify="space-between"
|
||||
{...rest}>
|
||||
{children}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const LabeledControlsItem = (props) => {
|
||||
const { label, children, mx = 1, ...rest } = props;
|
||||
return (
|
||||
<Flex.Item mx={mx}>
|
||||
<Flex
|
||||
height="100%"
|
||||
direction="column"
|
||||
align="center"
|
||||
textAlign="center"
|
||||
justify="space-between"
|
||||
{...rest}>
|
||||
<Flex.Item />
|
||||
<Flex.Item>{children}</Flex.Item>
|
||||
<Flex.Item color="label">{label}</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
);
|
||||
};
|
||||
|
||||
LabeledControls.Item = LabeledControlsItem;
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { BooleanLike, classes, pureComponentHooks } from 'common/react';
|
||||
import { InfernoNode } from 'inferno';
|
||||
import { Box, unit } from './Box';
|
||||
import { Divider } from './Divider';
|
||||
|
||||
type LabeledListProps = {
|
||||
children?: any;
|
||||
};
|
||||
|
||||
export const LabeledList = (props: LabeledListProps) => {
|
||||
const { children } = props;
|
||||
return <table className="LabeledList">{children}</table>;
|
||||
};
|
||||
|
||||
LabeledList.defaultHooks = pureComponentHooks;
|
||||
|
||||
type LabeledListItemProps = {
|
||||
className?: string | BooleanLike;
|
||||
label?: string | InfernoNode | BooleanLike;
|
||||
labelColor?: string | BooleanLike;
|
||||
labelWrap?: boolean;
|
||||
color?: string | BooleanLike;
|
||||
textAlign?: string | BooleanLike;
|
||||
buttons?: InfernoNode;
|
||||
/** @deprecated */
|
||||
content?: any;
|
||||
children?: InfernoNode;
|
||||
verticalAlign?: string;
|
||||
};
|
||||
|
||||
const LabeledListItem = (props: LabeledListItemProps) => {
|
||||
const {
|
||||
className,
|
||||
label,
|
||||
labelColor = 'label',
|
||||
labelWrap,
|
||||
color,
|
||||
textAlign,
|
||||
buttons,
|
||||
content,
|
||||
children,
|
||||
verticalAlign = 'baseline',
|
||||
} = props;
|
||||
return (
|
||||
<tr className={classes(['LabeledList__row', className])}>
|
||||
<Box
|
||||
as="td"
|
||||
color={labelColor}
|
||||
className={classes([
|
||||
'LabeledList__cell',
|
||||
// Kinda flipped because we want nowrap as default. Cleaner CSS this way though.
|
||||
!labelWrap && 'LabeledList__label--nowrap',
|
||||
])}
|
||||
verticalAlign={verticalAlign}>
|
||||
{label ? (typeof label === 'string' ? label + ':' : label) : null}
|
||||
</Box>
|
||||
<Box
|
||||
as="td"
|
||||
color={color}
|
||||
textAlign={textAlign}
|
||||
className={classes(['LabeledList__cell', 'LabeledList__content'])}
|
||||
colSpan={buttons ? undefined : 2}
|
||||
verticalAlign={verticalAlign}>
|
||||
{content}
|
||||
{children}
|
||||
</Box>
|
||||
{buttons && (
|
||||
<td className="LabeledList__cell LabeledList__buttons">{buttons}</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
LabeledListItem.defaultHooks = pureComponentHooks;
|
||||
|
||||
type LabeledListDividerProps = {
|
||||
size?: number;
|
||||
};
|
||||
|
||||
const LabeledListDivider = (props: LabeledListDividerProps) => {
|
||||
const padding = props.size ? unit(Math.max(0, props.size - 1)) : 0;
|
||||
return (
|
||||
<tr className="LabeledList__row">
|
||||
<td
|
||||
colSpan={3}
|
||||
style={{
|
||||
'padding-top': padding,
|
||||
'padding-bottom': padding,
|
||||
}}>
|
||||
<Divider />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
LabeledListDivider.defaultHooks = pureComponentHooks;
|
||||
|
||||
LabeledList.Item = LabeledListItem;
|
||||
LabeledList.Divider = LabeledListDivider;
|
||||
@@ -1,231 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2022 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
import { logger } from '../logging';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
type MenuProps = {
|
||||
children: any;
|
||||
width: string;
|
||||
menuRef: RefObject<HTMLElement>;
|
||||
onOutsideClick: () => void;
|
||||
};
|
||||
|
||||
class Menu extends Component<MenuProps> {
|
||||
private readonly handleClick: (event) => void;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = (event) => {
|
||||
if (!this.props.menuRef.current) {
|
||||
logger.log(`Menu.handleClick(): No ref`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.menuRef.current.contains(event.target)) {
|
||||
logger.log(`Menu.handleClick(): Inside`);
|
||||
} else {
|
||||
logger.log(`Menu.handleClick(): Outside`);
|
||||
this.props.onOutsideClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-deprecated
|
||||
componentWillMount() {
|
||||
window.addEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('click', this.handleClick);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, children } = this.props;
|
||||
return (
|
||||
<div
|
||||
className={'MenuBar__menu'}
|
||||
style={{
|
||||
width: width,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MenuBarDropdownProps = {
|
||||
open: boolean;
|
||||
openWidth: string;
|
||||
children: any;
|
||||
disabled?: boolean;
|
||||
display: any;
|
||||
onMouseOver: () => void;
|
||||
onClick: () => void;
|
||||
onOutsideClick: () => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
class MenuBarButton extends Component<MenuBarDropdownProps> {
|
||||
private readonly menuRef: RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.menuRef = createRef();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const {
|
||||
open,
|
||||
openWidth,
|
||||
children,
|
||||
disabled,
|
||||
display,
|
||||
onMouseOver,
|
||||
onClick,
|
||||
onOutsideClick,
|
||||
...boxProps
|
||||
} = props;
|
||||
const { className, ...rest } = boxProps;
|
||||
|
||||
return (
|
||||
<div ref={this.menuRef}>
|
||||
<Box
|
||||
className={classes([
|
||||
'MenuBar__MenuBarButton',
|
||||
'MenuBar__font',
|
||||
'MenuBar__hover',
|
||||
className,
|
||||
])}
|
||||
{...rest}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
onmouseover={onMouseOver}>
|
||||
<span className="MenuBar__MenuBarButton-text">{display}</span>
|
||||
</Box>
|
||||
{open && (
|
||||
<Menu
|
||||
width={openWidth}
|
||||
menuRef={this.menuRef}
|
||||
onOutsideClick={onOutsideClick}>
|
||||
{children}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type MenuBarItemProps = {
|
||||
entry: string;
|
||||
children: any;
|
||||
openWidth: string;
|
||||
display: InfernoNode;
|
||||
setOpenMenuBar: (entry: string | null) => void;
|
||||
openMenuBar: string | null;
|
||||
setOpenOnHover: (flag: boolean) => void;
|
||||
openOnHover: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Dropdown = (props: MenuBarItemProps) => {
|
||||
const {
|
||||
entry,
|
||||
children,
|
||||
openWidth,
|
||||
display,
|
||||
setOpenMenuBar,
|
||||
openMenuBar,
|
||||
setOpenOnHover,
|
||||
openOnHover,
|
||||
disabled,
|
||||
className,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<MenuBarButton
|
||||
openWidth={openWidth}
|
||||
display={display}
|
||||
disabled={disabled}
|
||||
open={openMenuBar === entry}
|
||||
className={className}
|
||||
onClick={() => {
|
||||
const open = openMenuBar === entry ? null : entry;
|
||||
setOpenMenuBar(open);
|
||||
setOpenOnHover(!openOnHover);
|
||||
}}
|
||||
onOutsideClick={() => {
|
||||
setOpenMenuBar(null);
|
||||
setOpenOnHover(false);
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
if (openOnHover) {
|
||||
setOpenMenuBar(entry);
|
||||
}
|
||||
}}>
|
||||
{children}
|
||||
</MenuBarButton>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItemToggle = (props) => {
|
||||
const { value, displayText, onClick, checked } = props;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'MenuBar__font',
|
||||
'MenuBar__MenuItem',
|
||||
'MenuBar__MenuItemToggle',
|
||||
'MenuBar__hover',
|
||||
])}
|
||||
onClick={() => onClick(value)}>
|
||||
<div className="MenuBar__MenuItemToggle__check">
|
||||
{checked && <Icon size={1.3} name="check" />}
|
||||
</div>
|
||||
{displayText}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Dropdown.MenuItemToggle = MenuItemToggle;
|
||||
|
||||
const MenuItem = (props) => {
|
||||
const { value, displayText, onClick } = props;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'MenuBar__font',
|
||||
'MenuBar__MenuItem',
|
||||
'MenuBar__hover',
|
||||
])}
|
||||
onClick={() => onClick(value)}>
|
||||
{displayText}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Dropdown.MenuItem = MenuItem;
|
||||
|
||||
const Separator = () => {
|
||||
return <div className="MenuBar__Separator" />;
|
||||
};
|
||||
|
||||
Dropdown.Separator = Separator;
|
||||
|
||||
type MenuBarProps = {
|
||||
children: any;
|
||||
};
|
||||
|
||||
export const MenuBar = (props: MenuBarProps) => {
|
||||
const { children } = props;
|
||||
return <Box className="MenuBar">{children}</Box>;
|
||||
};
|
||||
|
||||
MenuBar.Dropdown = Dropdown;
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { Dimmer } from './Dimmer';
|
||||
|
||||
export const Modal = (props) => {
|
||||
const { className, children, onEnter, ...rest } = props;
|
||||
// VOREStation Addition start
|
||||
let handleKeyDown;
|
||||
if (onEnter) {
|
||||
handleKeyDown = (e) => {
|
||||
let key = e.which || e.keyCode;
|
||||
if (key === 13) {
|
||||
onEnter(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
// VOREStation Addition end
|
||||
return (
|
||||
<Dimmer onKeyDown={handleKeyDown} /* VOREStation edit */>
|
||||
<div
|
||||
className={classes(['Modal', className, computeBoxClassName(rest)])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{children}
|
||||
</div>
|
||||
</Dimmer>
|
||||
);
|
||||
};
|
||||
@@ -1,223 +0,0 @@
|
||||
import { Component } from 'inferno';
|
||||
import { Box, Button, Icon, Tooltip, LabeledList, Slider } from '.';
|
||||
import { useBackend } from '../backend';
|
||||
|
||||
const pauseEvent = (e) => {
|
||||
if (e.stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
e.cancelBubble = true;
|
||||
e.returnValue = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
const zoomScale = 280;
|
||||
|
||||
export class NanoMap extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Auto center based on window size
|
||||
const Xcenter = window.innerWidth / 2 - 256;
|
||||
const Ycenter = window.innerHeight / 2 - 256;
|
||||
|
||||
this.state = {
|
||||
offsetX: Xcenter,
|
||||
offsetY: Ycenter,
|
||||
transform: 'none',
|
||||
dragging: false,
|
||||
originX: null,
|
||||
originY: null,
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
// Dragging
|
||||
this.handleDragStart = (e) => {
|
||||
this.ref = e.target;
|
||||
this.setState({
|
||||
dragging: false,
|
||||
originX: e.screenX,
|
||||
originY: e.screenY,
|
||||
});
|
||||
document.addEventListener('mousemove', this.handleDragMove);
|
||||
document.addEventListener('mouseup', this.handleDragEnd);
|
||||
pauseEvent(e);
|
||||
};
|
||||
|
||||
this.handleDragMove = (e) => {
|
||||
this.setState((prevState) => {
|
||||
const state = { ...prevState };
|
||||
const newOffsetX = e.screenX - state.originX;
|
||||
const newOffsetY = e.screenY - state.originY;
|
||||
if (prevState.dragging) {
|
||||
state.offsetX += newOffsetX;
|
||||
state.offsetY += newOffsetY;
|
||||
state.originX = e.screenX;
|
||||
state.originY = e.screenY;
|
||||
} else {
|
||||
state.dragging = true;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
pauseEvent(e);
|
||||
};
|
||||
|
||||
this.handleDragEnd = (e) => {
|
||||
this.setState({
|
||||
dragging: false,
|
||||
originX: null,
|
||||
originY: null,
|
||||
});
|
||||
document.removeEventListener('mousemove', this.handleDragMove);
|
||||
document.removeEventListener('mouseup', this.handleDragEnd);
|
||||
pauseEvent(e);
|
||||
};
|
||||
|
||||
this.handleOnClick = (e) => {
|
||||
let byondX = e.offsetX / this.state.zoom / zoomScale;
|
||||
let byondY = 1 - e.offsetY / this.state.zoom / zoomScale; // Byond origin is bottom left, this is top left
|
||||
|
||||
e.byondX = byondX;
|
||||
e.byondY = byondY;
|
||||
if (typeof this.props.onClick === 'function') {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleZoom = (_e, value) => {
|
||||
this.setState((state) => {
|
||||
const newZoom = Math.min(Math.max(value, 1), 8);
|
||||
let zoomDiff = (newZoom - state.zoom) * 1.5;
|
||||
state.zoom = newZoom;
|
||||
|
||||
let newOffsetX = state.offsetX - 262 * zoomDiff;
|
||||
if (newOffsetX < -500) {
|
||||
newOffsetX = -500;
|
||||
}
|
||||
if (newOffsetX > 500) {
|
||||
newOffsetX = 500;
|
||||
}
|
||||
|
||||
let newOffsetY = state.offsetY - 256 * zoomDiff;
|
||||
if (newOffsetY < -200) {
|
||||
newOffsetY = -200;
|
||||
}
|
||||
if (newOffsetY > 200) {
|
||||
newOffsetY = 200;
|
||||
}
|
||||
|
||||
state.offsetX = newOffsetX;
|
||||
state.offsetY = newOffsetY;
|
||||
if (props.onZoom) {
|
||||
props.onZoom(state.zoom);
|
||||
}
|
||||
return state;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { config } = useBackend(this.context);
|
||||
const { dragging, offsetX, offsetY, zoom = 1 } = this.state;
|
||||
const { children } = this.props;
|
||||
|
||||
const mapUrl = config.map + '_nanomap_z' + config.mapZLevel + '.png';
|
||||
// (x * zoom), x Needs to be double the turf- map size. (for virgo, 140x140)
|
||||
const mapSize = zoomScale * zoom + 'px';
|
||||
const newStyle = {
|
||||
width: mapSize,
|
||||
height: mapSize,
|
||||
'margin-top': offsetY + 'px',
|
||||
'margin-left': offsetX + 'px',
|
||||
'overflow': 'hidden',
|
||||
'position': 'relative',
|
||||
'background-image': 'url(' + mapUrl + ')',
|
||||
'background-size': 'cover',
|
||||
'background-repeat': 'no-repeat',
|
||||
'text-align': 'center',
|
||||
'cursor': dragging ? 'move' : 'auto',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="NanoMap__container">
|
||||
<Box
|
||||
style={newStyle}
|
||||
textAlign="center"
|
||||
onMouseDown={this.handleDragStart}
|
||||
onClick={this.handleOnClick}>
|
||||
<Box>{children}</Box>
|
||||
</Box>
|
||||
<NanoMapZoomer zoom={zoom} onZoom={this.handleZoom} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const NanoMapMarker = (props, context) => {
|
||||
const { x, y, zoom = 1, icon, tooltip, color, onClick } = props;
|
||||
|
||||
const handleOnClick = (e) => {
|
||||
pauseEvent(e);
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
}
|
||||
};
|
||||
|
||||
const rx = x * 2 * zoom - zoom - 3;
|
||||
const ry = y * 2 * zoom - zoom - 3;
|
||||
return (
|
||||
<div>
|
||||
<Box
|
||||
position="absolute"
|
||||
className="NanoMap__marker"
|
||||
lineHeight="0"
|
||||
bottom={ry + 'px'}
|
||||
left={rx + 'px'}
|
||||
onMouseDown={handleOnClick}>
|
||||
<Icon name={icon} color={color} fontSize="6px" />
|
||||
<Tooltip content={tooltip} />
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NanoMap.Marker = NanoMapMarker;
|
||||
|
||||
const NanoMapZoomer = (props, context) => {
|
||||
const { act, config, data } = useBackend(context);
|
||||
return (
|
||||
<Box className="NanoMap__zoomer">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Zoom">
|
||||
<Slider
|
||||
minValue="1"
|
||||
maxValue="8"
|
||||
stepPixelSize="10"
|
||||
format={(v) => v + 'x'}
|
||||
value={props.zoom}
|
||||
onDrag={(e, v) => props.onZoom(e, v)}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Z-Level">
|
||||
{data.map_levels
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.map((level) => (
|
||||
<Button
|
||||
key={level}
|
||||
selected={~~level === ~~config.mapZLevel}
|
||||
content={level}
|
||||
onClick={() => {
|
||||
act('setZLevel', { 'mapZLevel': level });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
NanoMap.Zoomer = NanoMapZoomer;
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { Box } from './Box';
|
||||
|
||||
export const NoticeBox = (props) => {
|
||||
const { className, color, info, warning, success, danger, ...rest } = props;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'NoticeBox',
|
||||
color && 'NoticeBox--color--' + color,
|
||||
info && 'NoticeBox--type--info',
|
||||
success && 'NoticeBox--type--success',
|
||||
danger && 'NoticeBox--type--danger',
|
||||
className,
|
||||
])}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
NoticeBox.defaultHooks = pureComponentHooks;
|
||||
@@ -1,284 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp } from 'common/math';
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
import { Box } from './Box';
|
||||
|
||||
const DEFAULT_UPDATE_RATE = 400;
|
||||
|
||||
export class NumberInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { value } = props;
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
value,
|
||||
dragging: false,
|
||||
editing: false,
|
||||
internalValue: null,
|
||||
origin: null,
|
||||
suppressingFlicker: false,
|
||||
};
|
||||
|
||||
// Suppresses flickering while the value propagates through the backend
|
||||
this.flickerTimer = null;
|
||||
this.suppressFlicker = () => {
|
||||
const { suppressFlicker } = this.props;
|
||||
if (suppressFlicker > 0) {
|
||||
this.setState({
|
||||
suppressingFlicker: true,
|
||||
});
|
||||
clearTimeout(this.flickerTimer);
|
||||
this.flickerTimer = setTimeout(
|
||||
() =>
|
||||
this.setState({
|
||||
suppressingFlicker: false,
|
||||
}),
|
||||
suppressFlicker
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.handleDragStart = (e) => {
|
||||
const { value } = this.props;
|
||||
const { editing } = this.state;
|
||||
if (editing) {
|
||||
return;
|
||||
}
|
||||
document.body.style['pointer-events'] = 'none';
|
||||
this.ref = e.target;
|
||||
this.setState({
|
||||
dragging: false,
|
||||
origin: e.screenY,
|
||||
value,
|
||||
internalValue: value,
|
||||
});
|
||||
this.timer = setTimeout(() => {
|
||||
this.setState({
|
||||
dragging: true,
|
||||
});
|
||||
}, 250);
|
||||
this.dragInterval = setInterval(() => {
|
||||
const { dragging, value } = this.state;
|
||||
const { onDrag } = this.props;
|
||||
if (dragging && onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
}, this.props.updateRate || DEFAULT_UPDATE_RATE);
|
||||
document.addEventListener('mousemove', this.handleDragMove);
|
||||
document.addEventListener('mouseup', this.handleDragEnd);
|
||||
};
|
||||
|
||||
this.handleDragMove = (e) => {
|
||||
const { minValue, maxValue, step, stepPixelSize } = this.props;
|
||||
this.setState((prevState) => {
|
||||
const state = { ...prevState };
|
||||
const offset = state.origin - e.screenY;
|
||||
if (prevState.dragging) {
|
||||
const stepOffset = Number.isFinite(minValue) ? minValue % step : 0;
|
||||
// Translate mouse movement to value
|
||||
// Give it some headroom (by increasing clamp range by 1 step)
|
||||
state.internalValue = clamp(
|
||||
state.internalValue + (offset * step) / stepPixelSize,
|
||||
minValue - step,
|
||||
maxValue + step
|
||||
);
|
||||
// Clamp the final value
|
||||
state.value = clamp(
|
||||
state.internalValue - (state.internalValue % step) + stepOffset,
|
||||
minValue,
|
||||
maxValue
|
||||
);
|
||||
state.origin = e.screenY;
|
||||
} else if (Math.abs(offset) > 4) {
|
||||
state.dragging = true;
|
||||
}
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
this.handleDragEnd = (e) => {
|
||||
const { onChange, onDrag } = this.props;
|
||||
const { dragging, value, internalValue } = this.state;
|
||||
document.body.style['pointer-events'] = 'auto';
|
||||
clearTimeout(this.timer);
|
||||
clearInterval(this.dragInterval);
|
||||
this.setState({
|
||||
dragging: false,
|
||||
editing: !dragging,
|
||||
origin: null,
|
||||
});
|
||||
document.removeEventListener('mousemove', this.handleDragMove);
|
||||
document.removeEventListener('mouseup', this.handleDragEnd);
|
||||
if (dragging) {
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
} else if (this.inputRef) {
|
||||
const input = this.inputRef.current;
|
||||
input.value = internalValue;
|
||||
// IE8: Dies when trying to focus a hidden element
|
||||
// (Error: Object does not support this action)
|
||||
try {
|
||||
input.focus();
|
||||
input.select();
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dragging,
|
||||
editing,
|
||||
value: intermediateValue,
|
||||
suppressingFlicker,
|
||||
} = this.state;
|
||||
const {
|
||||
className,
|
||||
fluid,
|
||||
animated,
|
||||
value,
|
||||
unit,
|
||||
minValue,
|
||||
maxValue,
|
||||
height,
|
||||
width,
|
||||
lineHeight,
|
||||
fontSize,
|
||||
format,
|
||||
onChange,
|
||||
onDrag,
|
||||
} = this.props;
|
||||
let displayValue = value;
|
||||
if (dragging || suppressingFlicker) {
|
||||
displayValue = intermediateValue;
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const contentElement = (
|
||||
<div className="NumberInput__content" unselectable={Byond.IS_LTE_IE8}>
|
||||
{
|
||||
(animated && !dragging && !suppressingFlicker) ?
|
||||
(<AnimatedNumber value={displayValue} format={format} />) :
|
||||
(format ? format(displayValue) : displayValue)
|
||||
}
|
||||
|
||||
{unit ? ' ' + unit : ''}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'NumberInput',
|
||||
fluid && 'NumberInput--fluid',
|
||||
className,
|
||||
])}
|
||||
minWidth={width}
|
||||
minHeight={height}
|
||||
lineHeight={lineHeight}
|
||||
fontSize={fontSize}
|
||||
onMouseDown={this.handleDragStart}>
|
||||
<div className="NumberInput__barContainer">
|
||||
<div
|
||||
className="NumberInput__bar"
|
||||
style={{
|
||||
// prettier-ignore
|
||||
height: clamp(
|
||||
(displayValue - minValue) / (maxValue - minValue) * 100,
|
||||
0, 100) + '%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{contentElement}
|
||||
<input
|
||||
ref={this.inputRef}
|
||||
className="NumberInput__input"
|
||||
style={{
|
||||
display: !editing ? 'none' : undefined,
|
||||
height: height,
|
||||
'line-height': lineHeight,
|
||||
'font-size': fontSize,
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (!editing) {
|
||||
return;
|
||||
}
|
||||
const value = clamp(parseFloat(e.target.value), minValue, maxValue);
|
||||
if (Number.isNaN(value)) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
editing: false,
|
||||
value,
|
||||
});
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
// prettier-ignore
|
||||
const value = clamp(
|
||||
parseFloat(e.target.value),
|
||||
minValue,
|
||||
maxValue
|
||||
);
|
||||
if (Number.isNaN(value)) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
editing: false,
|
||||
value,
|
||||
});
|
||||
this.suppressFlicker();
|
||||
if (onChange) {
|
||||
onChange(e, value);
|
||||
}
|
||||
if (onDrag) {
|
||||
onDrag(e, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
this.setState({
|
||||
editing: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NumberInput.defaultHooks = pureComponentHooks;
|
||||
NumberInput.defaultProps = {
|
||||
minValue: -Infinity,
|
||||
maxValue: +Infinity,
|
||||
step: 1,
|
||||
stepPixelSize: 1,
|
||||
suppressFlicker: 50,
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createPopper } from '@popperjs/core';
|
||||
import { ArgumentsOf } from 'common/types';
|
||||
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
|
||||
|
||||
type PopperProps = {
|
||||
popperContent: InfernoNode;
|
||||
options?: ArgumentsOf<typeof createPopper>[2];
|
||||
additionalStyles?: CSSProperties;
|
||||
};
|
||||
|
||||
export class Popper extends Component<PopperProps> {
|
||||
static id: number = 0;
|
||||
|
||||
renderedContent: HTMLDivElement;
|
||||
popperInstance: ReturnType<typeof createPopper>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
Popper.id += 1;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { additionalStyles, options } = this.props;
|
||||
|
||||
this.renderedContent = document.createElement('div');
|
||||
|
||||
if (additionalStyles) {
|
||||
for (const [attribute, value] of Object.entries(additionalStyles)) {
|
||||
this.renderedContent.style[attribute] = value;
|
||||
}
|
||||
}
|
||||
|
||||
this.renderPopperContent(() => {
|
||||
document.body.appendChild(this.renderedContent);
|
||||
|
||||
// HACK: We don't want to create a wrapper, as it could break the layout
|
||||
// of consumers, so we do the inferno equivalent of `findDOMNode(this)`.
|
||||
// This is usually bad as refs are usually better, but refs did
|
||||
// not work in this case, as they weren't propagating correctly.
|
||||
// A previous attempt was made as a render prop that passed an ID,
|
||||
// but this made consuming use too unwieldly.
|
||||
// This code is copied from `findDOMNode` in inferno-extras.
|
||||
// Because this component is written in TypeScript, we will know
|
||||
// immediately if this internal variable is removed.
|
||||
const domNode = findDOMfromVNode(this.$LI, true);
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.popperInstance = createPopper(
|
||||
domNode,
|
||||
this.renderedContent,
|
||||
options
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.renderPopperContent(() => this.popperInstance?.update());
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.popperInstance?.destroy();
|
||||
render(null, this.renderedContent, () => {
|
||||
this.renderedContent.remove();
|
||||
});
|
||||
}
|
||||
|
||||
renderPopperContent(callback: () => void) {
|
||||
// `render` errors when given false, so we convert it to `null`,
|
||||
// which is supported.
|
||||
render(
|
||||
this.props.popperContent || null,
|
||||
this.renderedContent,
|
||||
callback,
|
||||
this.context
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp01, scale, keyOfMatchingRange, toFixed } from 'common/math';
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { CSS_COLORS } from '../constants';
|
||||
|
||||
export const ProgressBar = (props) => {
|
||||
const {
|
||||
className,
|
||||
value,
|
||||
minValue = 0,
|
||||
maxValue = 1,
|
||||
color,
|
||||
ranges = {},
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const scaledValue = scale(value, minValue, maxValue);
|
||||
const hasContent = children !== undefined;
|
||||
// prettier-ignore
|
||||
const effectiveColor = color
|
||||
|| keyOfMatchingRange(value, ranges)
|
||||
|| 'default';
|
||||
|
||||
// We permit colors to be in hex format, rgb()/rgba() format,
|
||||
// a name for a color-<name> class, or a base CSS class.
|
||||
const outerProps = computeBoxProps(rest);
|
||||
// prettier-ignore
|
||||
const outerClasses = [
|
||||
'ProgressBar',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
];
|
||||
const fillStyles = {
|
||||
'width': clamp01(scaledValue) * 100 + '%',
|
||||
};
|
||||
if (CSS_COLORS.includes(effectiveColor) || effectiveColor === 'default') {
|
||||
// If the color is a color-<name> class, just use that.
|
||||
outerClasses.push('ProgressBar--color--' + effectiveColor);
|
||||
} else {
|
||||
// Otherwise, set styles directly.
|
||||
// prettier-ignore
|
||||
outerProps.style = (outerProps.style || "")
|
||||
+ `border-color: ${effectiveColor};`;
|
||||
fillStyles['background-color'] = effectiveColor;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes(outerClasses)} {...outerProps}>
|
||||
<div
|
||||
className="ProgressBar__fill ProgressBar__fill--animated"
|
||||
style={fillStyles}
|
||||
/>
|
||||
<div className="ProgressBar__content">
|
||||
{hasContent ? children : toFixed(scaledValue * 100) + '%'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressBar.defaultHooks = pureComponentHooks;
|
||||
@@ -1,176 +0,0 @@
|
||||
import { classes } from 'common/react';
|
||||
import { clamp } from 'common/math';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
import { KEY_ESCAPE, KEY_ENTER } from 'common/keycodes';
|
||||
|
||||
const DEFAULT_MIN = 0;
|
||||
const DEFAULT_MAX = 10000;
|
||||
|
||||
/**
|
||||
* Takes a string input and parses integers or floats from it.
|
||||
* If none: Minimum is set.
|
||||
* Else: Clamps it to the given range.
|
||||
*/
|
||||
const getClampedNumber = (value, minValue, maxValue, allowFloats) => {
|
||||
const minimum = minValue || DEFAULT_MIN;
|
||||
const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX;
|
||||
if (!value || !value.length) {
|
||||
return String(minimum);
|
||||
}
|
||||
let parsedValue = allowFloats
|
||||
? parseFloat(value.replace(/[^\-\d.]/g, ''))
|
||||
: parseInt(value.replace(/[^\-\d]/g, ''), 10);
|
||||
if (isNaN(parsedValue)) {
|
||||
return String(minimum);
|
||||
} else {
|
||||
return String(clamp(parsedValue, minimum, maximum));
|
||||
}
|
||||
};
|
||||
|
||||
export class RestrictedInput extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.inputRef = createRef();
|
||||
this.state = {
|
||||
editing: false,
|
||||
};
|
||||
this.handleBlur = (e) => {
|
||||
const { editing } = this.state;
|
||||
if (editing) {
|
||||
this.setEditing(false);
|
||||
}
|
||||
};
|
||||
this.handleChange = (e) => {
|
||||
const { maxValue, minValue, onChange, allowFloats } = this.props;
|
||||
e.target.value = getClampedNumber(
|
||||
e.target.value,
|
||||
minValue,
|
||||
maxValue,
|
||||
allowFloats
|
||||
);
|
||||
if (onChange) {
|
||||
onChange(e, +e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleFocus = (e) => {
|
||||
const { editing } = this.state;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
};
|
||||
this.handleInput = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onInput } = this.props;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, +e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleKeyDown = (e) => {
|
||||
const { maxValue, minValue, onChange, onEnter, allowFloats } = this.props;
|
||||
if (e.keyCode === KEY_ENTER) {
|
||||
const safeNum = getClampedNumber(
|
||||
e.target.value,
|
||||
minValue,
|
||||
maxValue,
|
||||
allowFloats
|
||||
);
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, +safeNum);
|
||||
}
|
||||
if (onEnter) {
|
||||
onEnter(e, +safeNum);
|
||||
}
|
||||
e.target.blur();
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === KEY_ESCAPE) {
|
||||
if (this.props.onEscape) {
|
||||
this.props.onEscape(e);
|
||||
return;
|
||||
}
|
||||
this.setEditing(false);
|
||||
e.target.value = this.props.value;
|
||||
e.target.blur();
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { maxValue, minValue, allowFloats } = this.props;
|
||||
const nextValue = this.props.value?.toString();
|
||||
const input = this.inputRef.current;
|
||||
if (input) {
|
||||
input.value = getClampedNumber(
|
||||
nextValue,
|
||||
minValue,
|
||||
maxValue,
|
||||
allowFloats
|
||||
);
|
||||
}
|
||||
if (this.props.autoFocus || this.props.autoSelect) {
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
|
||||
if (this.props.autoSelect) {
|
||||
input.select();
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, _) {
|
||||
const { maxValue, minValue, allowFloats } = this.props;
|
||||
const { editing } = this.state;
|
||||
const prevValue = prevProps.value?.toString();
|
||||
const nextValue = this.props.value?.toString();
|
||||
const input = this.inputRef.current;
|
||||
if (input && !editing) {
|
||||
if (nextValue !== prevValue && nextValue !== input.value) {
|
||||
input.value = getClampedNumber(
|
||||
nextValue,
|
||||
minValue,
|
||||
maxValue,
|
||||
allowFloats
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEditing(editing) {
|
||||
this.setState({ editing });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { onChange, onEnter, onInput, value, ...boxProps } = props;
|
||||
const { className, fluid, monospace, ...rest } = boxProps;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'Input',
|
||||
fluid && 'Input--fluid',
|
||||
monospace && 'Input--monospace',
|
||||
className,
|
||||
])}
|
||||
{...rest}>
|
||||
<div className="Input__baseline">.</div>
|
||||
<input
|
||||
className="Input__input"
|
||||
onChange={this.handleChange}
|
||||
onInput={this.handleInput}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.inputRef}
|
||||
type="number"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
|
||||
import { classes } from 'common/react';
|
||||
import { AnimatedNumber } from './AnimatedNumber';
|
||||
import { Box, computeBoxClassName, computeBoxProps } from './Box';
|
||||
|
||||
export const RoundGauge = (props) => {
|
||||
// Support for IE8 is for losers sorry B)
|
||||
if (Byond.IS_LTE_IE8) {
|
||||
return <AnimatedNumber {...props} />;
|
||||
}
|
||||
|
||||
const {
|
||||
value,
|
||||
minValue = 1,
|
||||
maxValue = 1,
|
||||
ranges,
|
||||
alertAfter,
|
||||
alertBefore,
|
||||
format,
|
||||
size = 1,
|
||||
className,
|
||||
style,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const scaledValue = scale(value, minValue, maxValue);
|
||||
const clampedValue = clamp01(scaledValue);
|
||||
const scaledRanges = ranges ? {} : { 'primary': [0, 1] };
|
||||
if (ranges) {
|
||||
Object.keys(ranges).forEach((x) => {
|
||||
const range = ranges[x];
|
||||
scaledRanges[x] = [
|
||||
scale(range[0], minValue, maxValue),
|
||||
scale(range[1], minValue, maxValue),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
const shouldShowAlert = () => {
|
||||
// If both after and before alert props are set, attempt to interpret both
|
||||
// in a helpful way.
|
||||
if (alertAfter && alertBefore && alertAfter < alertBefore) {
|
||||
// If alertAfter is before alertBefore, only display an alert if
|
||||
// we're between them.
|
||||
if (alertAfter < value && alertBefore > value) {
|
||||
return true;
|
||||
}
|
||||
} else if (alertAfter < value || alertBefore > value) {
|
||||
// Otherwise, we have distint ranges, or only one or neither are set.
|
||||
// Either way, being on the active side of either is sufficient.
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
const alertColor = shouldShowAlert()
|
||||
&& keyOfMatchingRange(clampedValue, scaledRanges);
|
||||
|
||||
return (
|
||||
<Box inline>
|
||||
<div
|
||||
className={classes([
|
||||
'RoundGauge',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps({
|
||||
style: {
|
||||
'font-size': size + 'em',
|
||||
...style,
|
||||
},
|
||||
...rest,
|
||||
})}>
|
||||
<svg viewBox="0 0 100 50">
|
||||
{(alertAfter || alertBefore) && (
|
||||
<g
|
||||
className={classes([
|
||||
'RoundGauge__alert',
|
||||
alertColor ? `active RoundGauge__alert--${alertColor}` : '',
|
||||
])}>
|
||||
<path d="M48.211,14.578C48.55,13.9 49.242,13.472 50,13.472C50.758,13.472 51.45,13.9 51.789,14.578C54.793,20.587 60.795,32.589 63.553,38.106C63.863,38.726 63.83,39.462 63.465,40.051C63.101,40.641 62.457,41 61.764,41C55.996,41 44.004,41 38.236,41C37.543,41 36.899,40.641 36.535,40.051C36.17,39.462 36.137,38.726 36.447,38.106C39.205,32.589 45.207,20.587 48.211,14.578ZM50,34.417C51.426,34.417 52.583,35.574 52.583,37C52.583,38.426 51.426,39.583 50,39.583C48.574,39.583 47.417,38.426 47.417,37C47.417,35.574 48.574,34.417 50,34.417ZM50,32.75C50,32.75 53,31.805 53,22.25C53,20.594 51.656,19.25 50,19.25C48.344,19.25 47,20.594 47,22.25C47,31.805 50,32.75 50,32.75Z" />
|
||||
</g>
|
||||
)}
|
||||
<g>
|
||||
<circle className="RoundGauge__ringTrack" cx="50" cy="50" r="45" />
|
||||
</g>
|
||||
<g>
|
||||
{Object.keys(scaledRanges).map((x, i) => {
|
||||
const col_ranges = scaledRanges[x];
|
||||
return (
|
||||
<circle
|
||||
className={`RoundGauge__ringFill RoundGauge--color--${x}`}
|
||||
key={i}
|
||||
style={{
|
||||
'stroke-dashoffset': Math.max(
|
||||
(2.0 - (col_ranges[1] - col_ranges[0])) * Math.PI * 50,
|
||||
0
|
||||
),
|
||||
}}
|
||||
transform={`rotate(${180 + 180 * col_ranges[0]} 50 50)`}
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
<g
|
||||
className="RoundGauge__needle"
|
||||
transform={`rotate(${clampedValue * 180 - 90} 50 50)`}>
|
||||
<polygon
|
||||
className="RoundGauge__needleLine"
|
||||
points="46,50 50,0 54,50"
|
||||
/>
|
||||
<circle
|
||||
className="RoundGauge__needleMiddle"
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="8"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<AnimatedNumber value={value} format={format} size={size} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { canRender, classes } from 'common/react';
|
||||
import { Component, createRef, InfernoNode, RefObject } from 'inferno';
|
||||
import { addScrollableNode, removeScrollableNode } from '../events';
|
||||
import { BoxProps, computeBoxClassName, computeBoxProps } from './Box';
|
||||
|
||||
interface SectionProps extends BoxProps {
|
||||
className?: string;
|
||||
title?: string | InfernoElement<string>;
|
||||
buttons?: InfernoNode;
|
||||
fill?: boolean;
|
||||
fitted?: boolean;
|
||||
scrollable?: boolean;
|
||||
scrollableHorizontal?: boolean;
|
||||
flexGrow?: boolean; // VOREStation Addition
|
||||
noTopPadding?: boolean; // VOREStation Addition
|
||||
stretchContents?: boolean; // VOREStation Addition
|
||||
/** @deprecated This property no longer works, please remove it. */
|
||||
level?: never;
|
||||
/** @deprecated Please use `scrollable` property */
|
||||
overflowY?: never;
|
||||
/** @member Allows external control of scrolling. */
|
||||
scrollableRef?: RefObject<HTMLDivElement>;
|
||||
/** @member Callback function for the `scroll` event */
|
||||
onScroll?: (this: GlobalEventHandlers, ev: Event) => any;
|
||||
}
|
||||
|
||||
export class Section extends Component<SectionProps> {
|
||||
scrollableRef: RefObject<HTMLDivElement>;
|
||||
scrollable: boolean;
|
||||
onScroll?: (this: GlobalEventHandlers, ev: Event) => any;
|
||||
scrollableHorizontal: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.scrollableRef = props.scrollableRef || createRef();
|
||||
this.scrollable = props.scrollable;
|
||||
this.onScroll = props.onScroll;
|
||||
this.scrollableHorizontal = props.scrollableHorizontal;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.scrollable || this.scrollableHorizontal) {
|
||||
addScrollableNode(this.scrollableRef.current as HTMLElement);
|
||||
if (this.onScroll && this.scrollableRef.current) {
|
||||
this.scrollableRef.current.onscroll = this.onScroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.scrollable || this.scrollableHorizontal) {
|
||||
removeScrollableNode(this.scrollableRef.current as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
buttons,
|
||||
fill,
|
||||
fitted,
|
||||
scrollable,
|
||||
scrollableHorizontal,
|
||||
flexGrow, // VOREStation Addition
|
||||
noTopPadding, // VOREStation Addition
|
||||
stretchContents, // VOREStation Addition
|
||||
children,
|
||||
onScroll,
|
||||
...rest
|
||||
} = this.props;
|
||||
const hasTitle = canRender(title) || canRender(buttons);
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Section',
|
||||
Byond.IS_LTE_IE8 && 'Section--iefix',
|
||||
fill && 'Section--fill',
|
||||
fitted && 'Section--fitted',
|
||||
scrollable && 'Section--scrollable',
|
||||
scrollableHorizontal && 'Section--scrollableHorizontal',
|
||||
flexGrow && 'Section--flex', // VOREStation Addition
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{hasTitle && (
|
||||
<div className="Section__title">
|
||||
<span className="Section__titleText">{title}</span>
|
||||
<div className="Section__buttons">{buttons}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="Section__rest">
|
||||
{/* Vorestation Edit Start */}
|
||||
<div
|
||||
ref={this.scrollableRef}
|
||||
onScroll={onScroll}
|
||||
className={classes([
|
||||
'Section__content',
|
||||
!!stretchContents && 'Section__content--stretchContents',
|
||||
!!noTopPadding && 'Section__content--noTopPadding',
|
||||
])}>
|
||||
{children}
|
||||
</div>
|
||||
{/* Vorestation Edit End */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { clamp01, keyOfMatchingRange, scale } from 'common/math';
|
||||
import { classes } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { DraggableControl } from './DraggableControl';
|
||||
import { NumberInput } from './NumberInput';
|
||||
|
||||
export const Slider = (props) => {
|
||||
// IE8: I don't want to support a yet another component on IE8.
|
||||
if (Byond.IS_LTE_IE8) {
|
||||
return <NumberInput {...props} />;
|
||||
}
|
||||
const {
|
||||
// Draggable props (passthrough)
|
||||
animated,
|
||||
format,
|
||||
maxValue,
|
||||
minValue,
|
||||
onChange,
|
||||
onDrag,
|
||||
step,
|
||||
stepPixelSize,
|
||||
suppressFlicker,
|
||||
unit,
|
||||
value,
|
||||
// Own props
|
||||
className,
|
||||
fillValue,
|
||||
color,
|
||||
ranges = {},
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
const hasContent = children !== undefined;
|
||||
return (
|
||||
<DraggableControl
|
||||
dragMatrix={[1, 0]}
|
||||
{...{
|
||||
animated,
|
||||
format,
|
||||
maxValue,
|
||||
minValue,
|
||||
onChange,
|
||||
onDrag,
|
||||
step,
|
||||
stepPixelSize,
|
||||
suppressFlicker,
|
||||
unit,
|
||||
value,
|
||||
}}>
|
||||
{(control) => {
|
||||
const {
|
||||
dragging,
|
||||
editing,
|
||||
value,
|
||||
displayValue,
|
||||
displayElement,
|
||||
inputElement,
|
||||
handleDragStart,
|
||||
} = control;
|
||||
const hasFillValue = fillValue !== undefined && fillValue !== null;
|
||||
const scaledValue = scale(value, minValue, maxValue);
|
||||
const scaledFillValue = scale(
|
||||
fillValue ?? displayValue,
|
||||
minValue,
|
||||
maxValue
|
||||
);
|
||||
const scaledDisplayValue = scale(displayValue, minValue, maxValue);
|
||||
// prettier-ignore
|
||||
const effectiveColor = color
|
||||
|| keyOfMatchingRange(fillValue ?? value, ranges) || 'default';
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Slider',
|
||||
'ProgressBar',
|
||||
'ProgressBar--color--' + effectiveColor,
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps(rest)}
|
||||
onMouseDown={handleDragStart}>
|
||||
<div
|
||||
className={classes([
|
||||
'ProgressBar__fill',
|
||||
hasFillValue && 'ProgressBar__fill--animated',
|
||||
])}
|
||||
style={{
|
||||
width: clamp01(scaledFillValue) * 100 + '%',
|
||||
opacity: 0.4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="ProgressBar__fill"
|
||||
style={{
|
||||
// prettier-ignore
|
||||
width: clamp01(Math.min(scaledFillValue, scaledDisplayValue))
|
||||
* 100 + '%',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="Slider__cursorOffset"
|
||||
style={{
|
||||
width: clamp01(scaledDisplayValue) * 100 + '%',
|
||||
}}>
|
||||
<div className="Slider__cursor" />
|
||||
<div className="Slider__pointer" />
|
||||
{dragging && (
|
||||
<div className="Slider__popupValue">{displayElement}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ProgressBar__content">
|
||||
{hasContent ? children : displayElement}
|
||||
</div>
|
||||
{inputElement}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DraggableControl>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2021 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { RefObject } from 'inferno';
|
||||
import { computeFlexClassName, computeFlexItemClassName, computeFlexItemProps, computeFlexProps, FlexItemProps, FlexProps } from './Flex';
|
||||
|
||||
type StackProps = FlexProps & {
|
||||
vertical?: boolean;
|
||||
fill?: boolean;
|
||||
};
|
||||
|
||||
export const Stack = (props: StackProps) => {
|
||||
const { className, vertical, fill, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Stack',
|
||||
fill && 'Stack--fill',
|
||||
vertical ? 'Stack--vertical' : 'Stack--horizontal',
|
||||
className,
|
||||
computeFlexClassName(props),
|
||||
])}
|
||||
{...computeFlexProps({
|
||||
direction: vertical ? 'column' : 'row',
|
||||
...rest,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type StackItemProps = FlexProps & {
|
||||
innerRef?: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
const StackItem = (props: StackItemProps) => {
|
||||
const { className, innerRef, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Stack__item',
|
||||
className,
|
||||
computeFlexItemClassName(rest),
|
||||
])}
|
||||
ref={innerRef}
|
||||
{...computeFlexItemProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Stack.Item = StackItem;
|
||||
|
||||
type StackDividerProps = FlexItemProps & {
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
const StackDivider = (props: StackDividerProps) => {
|
||||
const { className, hidden, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Stack__item',
|
||||
'Stack__divider',
|
||||
hidden && 'Stack__divider--hidden',
|
||||
className,
|
||||
computeFlexItemClassName(rest),
|
||||
])}
|
||||
{...computeFlexItemProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Stack.Divider = StackDivider;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { SFC } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
|
||||
// The cost of flexibility and prettiness.
|
||||
export const StyleableSection: SFC<{
|
||||
style?;
|
||||
titleStyle?;
|
||||
textStyle?;
|
||||
title?;
|
||||
titleSubtext?;
|
||||
}> = (props) => {
|
||||
return (
|
||||
<Box style={props.style}>
|
||||
{/* Yes, this box (line above) is missing the "Section" class. This is very intentional, as the layout looks *ugly* with it.*/}
|
||||
<Box class="Section__title" style={props.titleStyle}>
|
||||
<Box class="Section__titleText" style={props.textStyle}>
|
||||
{props.title}
|
||||
</Box>
|
||||
<div className="Section__buttons">{props.titleSubtext}</div>
|
||||
</Box>
|
||||
<Box class="Section__rest">
|
||||
<Box class="Section__content">{props.children}</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes, pureComponentHooks } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
|
||||
export const Table = (props) => {
|
||||
const { className, collapsing, children, ...rest } = props;
|
||||
return (
|
||||
<table
|
||||
className={classes([
|
||||
'Table',
|
||||
collapsing && 'Table--collapsing',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps(rest)}>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
Table.defaultHooks = pureComponentHooks;
|
||||
|
||||
export const TableRow = (props) => {
|
||||
const { className, header, ...rest } = props;
|
||||
return (
|
||||
<tr
|
||||
className={classes([
|
||||
'Table__row',
|
||||
header && 'Table__row--header',
|
||||
className,
|
||||
computeBoxClassName(props),
|
||||
])}
|
||||
{...computeBoxProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TableRow.defaultHooks = pureComponentHooks;
|
||||
|
||||
export const TableCell = (props) => {
|
||||
const { className, collapsing, header, ...rest } = props;
|
||||
return (
|
||||
<td
|
||||
className={classes([
|
||||
'Table__cell',
|
||||
collapsing && 'Table__cell--collapsing',
|
||||
header && 'Table__cell--header',
|
||||
className,
|
||||
computeBoxClassName(props),
|
||||
])}
|
||||
{...computeBoxProps(rest)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
TableCell.defaultHooks = pureComponentHooks;
|
||||
|
||||
Table.Row = TableRow;
|
||||
Table.Cell = TableCell;
|
||||
@@ -1,63 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { canRender, classes } from 'common/react';
|
||||
import { computeBoxClassName, computeBoxProps } from './Box';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export const Tabs = (props) => {
|
||||
const { className, vertical, fill, fluid, children, ...rest } = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Tabs',
|
||||
vertical ? 'Tabs--vertical' : 'Tabs--horizontal',
|
||||
fill && 'Tabs--fill',
|
||||
fluid && 'Tabs--fluid',
|
||||
className,
|
||||
computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Tab = (props) => {
|
||||
const {
|
||||
className,
|
||||
selected,
|
||||
color,
|
||||
icon,
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
className={classes([
|
||||
'Tab',
|
||||
'Tabs__Tab',
|
||||
'Tab--color--' + color,
|
||||
selected && 'Tab--selected',
|
||||
className,
|
||||
...computeBoxClassName(rest),
|
||||
])}
|
||||
{...computeBoxProps(rest)}>
|
||||
{(canRender(leftSlot) && <div className="Tab__left">{leftSlot}</div>) ||
|
||||
(!!icon && (
|
||||
<div className="Tab__left">
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
))}
|
||||
<div className="Tab__text">{children}</div>
|
||||
{canRender(rightSlot) && <div className="Tab__right">{rightSlot}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.Tab = Tab;
|
||||
@@ -1,238 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @author Warlockd
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { classes } from 'common/react';
|
||||
import { Component, createRef } from 'inferno';
|
||||
import { Box } from './Box';
|
||||
import { toInputValue } from './Input';
|
||||
import { KEY_ENTER, KEY_ESCAPE, KEY_TAB } from 'common/keycodes';
|
||||
|
||||
export class TextArea extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.textareaRef = props.innerRef || createRef();
|
||||
this.state = {
|
||||
editing: false,
|
||||
scrolledAmount: 0,
|
||||
};
|
||||
const { dontUseTabForIndent = false } = props;
|
||||
this.handleOnInput = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onInput } = this.props;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleOnChange = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onChange } = this.props;
|
||||
if (editing) {
|
||||
this.setEditing(false);
|
||||
}
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleKeyPress = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onKeyPress } = this.props;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
if (onKeyPress) {
|
||||
onKeyPress(e, e.target.value);
|
||||
}
|
||||
};
|
||||
this.handleKeyDown = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onChange, onInput, onEnter, onKey } = this.props;
|
||||
if (e.keyCode === KEY_ENTER) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
if (onEnter) {
|
||||
onEnter(e, e.target.value);
|
||||
}
|
||||
if (this.props.selfClear) {
|
||||
e.target.value = '';
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.keyCode === KEY_ESCAPE) {
|
||||
if (this.props.onEscape) {
|
||||
this.props.onEscape(e);
|
||||
}
|
||||
this.setEditing(false);
|
||||
if (this.props.selfClear) {
|
||||
e.target.value = '';
|
||||
} else {
|
||||
e.target.value = toInputValue(this.props.value);
|
||||
e.target.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
// Custom key handler
|
||||
if (onKey) {
|
||||
onKey(e, e.target.value);
|
||||
}
|
||||
if (!dontUseTabForIndent) {
|
||||
const keyCode = e.keyCode || e.which;
|
||||
if (keyCode === KEY_TAB) {
|
||||
e.preventDefault();
|
||||
const { value, selectionStart, selectionEnd } = e.target;
|
||||
e.target.value =
|
||||
value.substring(0, selectionStart) +
|
||||
'\t' +
|
||||
value.substring(selectionEnd);
|
||||
e.target.selectionEnd = selectionStart + 1;
|
||||
if (onInput) {
|
||||
onInput(e, e.target.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
this.handleFocus = (e) => {
|
||||
const { editing } = this.state;
|
||||
if (!editing) {
|
||||
this.setEditing(true);
|
||||
}
|
||||
};
|
||||
this.handleBlur = (e) => {
|
||||
const { editing } = this.state;
|
||||
const { onChange } = this.props;
|
||||
if (editing) {
|
||||
this.setEditing(false);
|
||||
if (onChange) {
|
||||
onChange(e, e.target.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.handleScroll = (e) => {
|
||||
const { displayedValue } = this.props;
|
||||
const input = this.textareaRef.current;
|
||||
if (displayedValue && input) {
|
||||
this.setState({
|
||||
scrolledAmount: input.scrollTop,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const nextValue = this.props.value;
|
||||
const input = this.textareaRef.current;
|
||||
if (input) {
|
||||
input.value = toInputValue(nextValue);
|
||||
}
|
||||
if (this.props.autoFocus || this.props.autoSelect) {
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
|
||||
if (this.props.autoSelect) {
|
||||
input.select();
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const prevValue = prevProps.value;
|
||||
const nextValue = this.props.value;
|
||||
const input = this.textareaRef.current;
|
||||
if (input && typeof nextValue === 'string' && prevValue !== nextValue) {
|
||||
input.value = toInputValue(nextValue);
|
||||
}
|
||||
}
|
||||
|
||||
setEditing(editing) {
|
||||
this.setState({ editing });
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.textareaRef.current && this.textareaRef.current.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Input only props
|
||||
const {
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onKeyPress,
|
||||
onInput,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onEnter,
|
||||
value,
|
||||
maxLength,
|
||||
placeholder,
|
||||
scrollbar,
|
||||
noborder,
|
||||
displayedValue,
|
||||
...boxProps
|
||||
} = this.props;
|
||||
// Box props
|
||||
const { className, fluid, nowrap, ...rest } = boxProps;
|
||||
const { scrolledAmount } = this.state;
|
||||
return (
|
||||
<Box
|
||||
className={classes([
|
||||
'TextArea',
|
||||
fluid && 'TextArea--fluid',
|
||||
noborder && 'TextArea--noborder',
|
||||
className,
|
||||
])}
|
||||
{...rest}>
|
||||
{!!displayedValue && (
|
||||
<Box position="absolute" width="100%" height="100%" overflow="hidden">
|
||||
<div
|
||||
className={classes([
|
||||
'TextArea__textarea',
|
||||
'TextArea__textarea_custom',
|
||||
])}
|
||||
style={{
|
||||
'transform': `translateY(-${scrolledAmount}px)`,
|
||||
}}>
|
||||
{displayedValue}
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
<textarea
|
||||
ref={this.textareaRef}
|
||||
className={classes([
|
||||
'TextArea__textarea',
|
||||
scrollbar && 'TextArea__textarea--scrollable',
|
||||
nowrap && 'TextArea__nowrap',
|
||||
])}
|
||||
placeholder={placeholder}
|
||||
onChange={this.handleOnChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onInput={this.handleOnInput}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
onScroll={this.handleScroll}
|
||||
maxLength={maxLength}
|
||||
style={{
|
||||
'color': displayedValue ? 'rgba(0, 0, 0, 0)' : 'inherit',
|
||||
}}
|
||||
/>
|
||||
{/* CHOMPedit End */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { formatTime } from '../format';
|
||||
import { Component } from 'inferno';
|
||||
|
||||
// AnimatedNumber Copypaste
|
||||
const isSafeNumber = (value) => {
|
||||
return (
|
||||
typeof value === 'number' && Number.isFinite(value) && !Number.isNaN(value)
|
||||
);
|
||||
};
|
||||
|
||||
export class TimeDisplay extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.timer = null;
|
||||
this.last_seen_value = undefined;
|
||||
this.state = {
|
||||
value: 0,
|
||||
};
|
||||
// Set initial state with value provided in props
|
||||
if (isSafeNumber(props.value)) {
|
||||
this.state.value = Number(props.value);
|
||||
this.last_seen_value = Number(props.value);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.auto !== undefined) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
let current = Number(this.state.value);
|
||||
if (this.props.value !== this.last_seen_value) {
|
||||
this.last_seen_value = this.props.value;
|
||||
current = this.props.value;
|
||||
}
|
||||
const mod = this.props.auto === 'up' ? 10 : -10; // Time down by default.
|
||||
const value = Math.max(0, current + mod); // one sec tick
|
||||
this.setState({ value });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.auto !== undefined) {
|
||||
this.timer = setInterval(() => this.tick(), 1000); // every 1 s
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render() {
|
||||
const val = this.state.value;
|
||||
// Directly display weird stuff
|
||||
if (!isSafeNumber(val)) {
|
||||
return this.state.value || null;
|
||||
}
|
||||
|
||||
return formatTime(val);
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { createPopper, Placement, VirtualElement } from '@popperjs/core';
|
||||
import { Component, findDOMfromVNode, InfernoNode, render } from 'inferno';
|
||||
|
||||
type TooltipProps = {
|
||||
children?: InfernoNode;
|
||||
content: InfernoNode;
|
||||
position?: Placement;
|
||||
};
|
||||
|
||||
type TooltipState = {
|
||||
hovered: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'eventListeners',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const NULL_RECT: DOMRect = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => null,
|
||||
};
|
||||
|
||||
export class Tooltip extends Component<TooltipProps, TooltipState> {
|
||||
// Mounting poppers is really laggy because popper.js is very slow.
|
||||
// Thus, instead of using the Popper component, Tooltip creates ONE popper
|
||||
// and stores every tooltip inside that.
|
||||
// This means you can never have two tooltips at once, for instance.
|
||||
static renderedTooltip: HTMLDivElement | undefined;
|
||||
static singletonPopper: ReturnType<typeof createPopper> | undefined;
|
||||
static currentHoveredElement: Element | undefined;
|
||||
static virtualElement: VirtualElement = {
|
||||
// prettier-ignore
|
||||
getBoundingClientRect: () => (
|
||||
Tooltip.currentHoveredElement?.getBoundingClientRect()
|
||||
?? NULL_RECT
|
||||
),
|
||||
};
|
||||
|
||||
getDOMNode() {
|
||||
// HACK: We don't want to create a wrapper, as it could break the layout
|
||||
// of consumers, so we do the inferno equivalent of `findDOMNode(this)`.
|
||||
// My attempt to avoid this was a render prop that passed in
|
||||
// callbacks to onmouseenter and onmouseleave, but this was unwiedly
|
||||
// to consumers, specifically buttons.
|
||||
// This code is copied from `findDOMNode` in inferno-extras.
|
||||
// Because this component is written in TypeScript, we will know
|
||||
// immediately if this internal variable is removed.
|
||||
return findDOMfromVNode(this.$LI, true);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const domNode = this.getDOMNode();
|
||||
|
||||
if (!domNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
domNode.addEventListener('mouseenter', () => {
|
||||
let renderedTooltip = Tooltip.renderedTooltip;
|
||||
if (renderedTooltip === undefined) {
|
||||
renderedTooltip = document.createElement('div');
|
||||
renderedTooltip.className = 'Tooltip';
|
||||
document.body.appendChild(renderedTooltip);
|
||||
Tooltip.renderedTooltip = renderedTooltip;
|
||||
}
|
||||
|
||||
Tooltip.currentHoveredElement = domNode;
|
||||
|
||||
renderedTooltip.style.opacity = '1';
|
||||
|
||||
this.renderPopperContent();
|
||||
});
|
||||
|
||||
domNode.addEventListener('mouseleave', () => {
|
||||
this.fadeOut();
|
||||
});
|
||||
}
|
||||
|
||||
fadeOut() {
|
||||
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Tooltip.currentHoveredElement = undefined;
|
||||
Tooltip.renderedTooltip!.style.opacity = '0';
|
||||
}
|
||||
|
||||
renderPopperContent() {
|
||||
const renderedTooltip = Tooltip.renderedTooltip;
|
||||
if (!renderedTooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
render(
|
||||
<span>{this.props.content}</span>,
|
||||
renderedTooltip,
|
||||
() => {
|
||||
let singletonPopper = Tooltip.singletonPopper;
|
||||
if (singletonPopper === undefined) {
|
||||
singletonPopper = createPopper(
|
||||
Tooltip.virtualElement,
|
||||
renderedTooltip!,
|
||||
{
|
||||
...DEFAULT_OPTIONS,
|
||||
placement: this.props.position || 'auto',
|
||||
}
|
||||
);
|
||||
|
||||
Tooltip.singletonPopper = singletonPopper;
|
||||
} else {
|
||||
singletonPopper.setOptions({
|
||||
...DEFAULT_OPTIONS,
|
||||
placement: this.props.position || 'auto',
|
||||
});
|
||||
|
||||
singletonPopper.update();
|
||||
}
|
||||
},
|
||||
this.context
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (Tooltip.currentHoveredElement !== this.getDOMNode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderPopperContent();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.fadeOut();
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, createRef } from 'inferno';
|
||||
|
||||
type Props = {
|
||||
onOutsideClick: () => void;
|
||||
};
|
||||
|
||||
export class TrackOutsideClicks extends Component<Props> {
|
||||
ref = createRef<HTMLDivElement>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.handleOutsideClick = this.handleOutsideClick.bind(this);
|
||||
|
||||
document.addEventListener('click', this.handleOutsideClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.handleOutsideClick);
|
||||
}
|
||||
|
||||
handleOutsideClick(event: MouseEvent) {
|
||||
if (!(event.target instanceof Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.ref.current && !this.ref.current.contains(event.target)) {
|
||||
this.props.onOutsideClick();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={this.ref}>{this.props.children}</div>;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export { AnimatedNumber } from './AnimatedNumber';
|
||||
export { Autofocus } from './Autofocus';
|
||||
export { Blink } from './Blink';
|
||||
export { BlockQuote } from './BlockQuote';
|
||||
export { Box } from './Box';
|
||||
export { Button } from './Button';
|
||||
export { ByondUi } from './ByondUi';
|
||||
export { Chart } from './Chart';
|
||||
export { Collapsible } from './Collapsible';
|
||||
export { ColorBox } from './ColorBox';
|
||||
export { Dimmer } from './Dimmer';
|
||||
export { Divider } from './Divider';
|
||||
export { DraggableControl } from './DraggableControl';
|
||||
export { Dropdown } from './Dropdown';
|
||||
export { Flex } from './Flex';
|
||||
export { FitText } from './FitText';
|
||||
export { Grid } from './Grid';
|
||||
export { Icon } from './Icon';
|
||||
export { InfinitePlane } from './InfinitePlane';
|
||||
export { Input } from './Input';
|
||||
export { KeyListener } from './KeyListener';
|
||||
export { Knob } from './Knob';
|
||||
export { LabeledControls } from './LabeledControls';
|
||||
export { LabeledList } from './LabeledList';
|
||||
export { MenuBar } from './MenuBar';
|
||||
export { Modal } from './Modal';
|
||||
export { NoticeBox } from './NoticeBox';
|
||||
export { NumberInput } from './NumberInput';
|
||||
export { ProgressBar } from './ProgressBar';
|
||||
export { Popper } from './Popper';
|
||||
export { RestrictedInput } from './RestrictedInput';
|
||||
export { RoundGauge } from './RoundGauge';
|
||||
export { Section } from './Section';
|
||||
export { Slider } from './Slider';
|
||||
export { StyleableSection } from './StyleableSection';
|
||||
export { Stack } from './Stack';
|
||||
export { Table } from './Table';
|
||||
export { Tabs } from './Tabs';
|
||||
export { TextArea } from './TextArea';
|
||||
export { TimeDisplay } from './TimeDisplay';
|
||||
export { TrackOutsideClicks } from './TrackOutsideClicks';
|
||||
export { Tooltip } from './Tooltip';
|
||||
export { Dialog } from './Dialog';
|
||||
@@ -1,88 +0,0 @@
|
||||
// import { getGasColor, getGasFromId, getGasFromPath, getGasLabel } from './constants';
|
||||
import { getGasColor, getGasFromId, getGasLabel } from './constants';
|
||||
|
||||
describe('gas helper functions', () => {
|
||||
it('should get the proper gas label', () => {
|
||||
// Testing for alphabetic gas id
|
||||
const gasId = 'oxygen';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
expect(gasLabel).toBe('O₂');
|
||||
});
|
||||
|
||||
it('should get the proper gas label', () => {
|
||||
// Testing for underscore gas id
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
expect(gasLabel).toBe('N₂O');
|
||||
});
|
||||
|
||||
it('should get the proper gas label', () => {
|
||||
// Testing for wrong capitalization of two word gas
|
||||
const gasId = 'nitrous oxide';
|
||||
const gasLabel = getGasLabel(gasId); // This should set to Nitrous Oxide before checking
|
||||
expect(gasLabel).toBe('N₂O');
|
||||
});
|
||||
|
||||
it('should get the proper gas label with a fallback', () => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasLabel = getGasLabel(gasId, 'fallback');
|
||||
|
||||
expect(gasLabel).toBe('fallback');
|
||||
});
|
||||
|
||||
it('should return none if no gas and no fallback is found', () => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasLabel = getGasLabel(gasId);
|
||||
|
||||
expect(gasLabel).toBe('None');
|
||||
});
|
||||
|
||||
it('should get the proper gas color', () => {
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gasColor = getGasColor(gasId);
|
||||
|
||||
expect(gasColor).toBe('red');
|
||||
});
|
||||
|
||||
it('should return a string if no gas is found', () => {
|
||||
const gasId = 'nonexistent';
|
||||
const gasColor = getGasColor(gasId);
|
||||
|
||||
expect(gasColor).toBe('black');
|
||||
});
|
||||
|
||||
it('should return the gas object if found', () => {
|
||||
const gasId = 'nitrous_oxide';
|
||||
const gas = getGasFromId(gasId);
|
||||
|
||||
expect(gas).toEqual({
|
||||
id: 'nitrous_oxide',
|
||||
// path: '/datum/gas/antinoblium',
|
||||
name: 'Nitrous Oxide',
|
||||
label: 'N₂O',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined if no gas is found', () => {
|
||||
const gasId = 'nonexistent';
|
||||
const gas = getGasFromId(gasId);
|
||||
|
||||
expect(gas).toBeUndefined();
|
||||
});
|
||||
|
||||
/*
|
||||
it('should return the gas using a path', () => {
|
||||
const gasPath = '/datum/gas/antinoblium';
|
||||
const gas = getGasFromPath(gasPath);
|
||||
|
||||
expect(gas).toEqual({
|
||||
id: 'antinoblium',
|
||||
path: '/datum/gas/antinoblium',
|
||||
name: 'Antinoblium',
|
||||
label: 'Anti-Noblium',
|
||||
color: 'maroon',
|
||||
});
|
||||
});
|
||||
*/
|
||||
});
|
||||
@@ -1,316 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
type Gas = {
|
||||
id: string;
|
||||
// path: string;
|
||||
name: string;
|
||||
label: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
// VOREStation Addition start
|
||||
/** 0.0 Degrees Celsius in Kelvin */
|
||||
export const T0C = 273.15;
|
||||
// VOREStation Addition end
|
||||
|
||||
// UI states, which are mirrored from the BYOND code.
|
||||
export const UI_INTERACTIVE = 2;
|
||||
export const UI_UPDATE = 1;
|
||||
export const UI_DISABLED = 0;
|
||||
export const UI_CLOSE = -1;
|
||||
|
||||
// All game related colors are stored here
|
||||
export const COLORS = {
|
||||
// Department colors
|
||||
department: {
|
||||
captain: '#c06616',
|
||||
security: '#e74c3c',
|
||||
medbay: '#3498db',
|
||||
science: '#9b59b6',
|
||||
engineering: '#f1c40f',
|
||||
cargo: '#f39c12',
|
||||
centcom: '#00c100',
|
||||
other: '#c38312',
|
||||
},
|
||||
// VOREStation Addition begin
|
||||
manifest: {
|
||||
command: '#3333FF',
|
||||
security: '#8e0000',
|
||||
medical: '#006600',
|
||||
engineering: '#b27300',
|
||||
science: '#a65ba6',
|
||||
cargo: '#bb9040',
|
||||
planetside: '#555555',
|
||||
civilian: '#a32800',
|
||||
miscellaneous: '#666666',
|
||||
silicon: '#222222',
|
||||
},
|
||||
// VOREStation Addition end
|
||||
// Damage type colors
|
||||
damageType: {
|
||||
oxy: '#3498db',
|
||||
toxin: '#2ecc71',
|
||||
burn: '#e67e22',
|
||||
brute: '#e74c3c',
|
||||
},
|
||||
// reagent / chemistry related colours
|
||||
reagent: {
|
||||
acidicbuffer: '#fbc314',
|
||||
basicbuffer: '#3853a4',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Colors defined in CSS
|
||||
export const CSS_COLORS = [
|
||||
'black',
|
||||
'white',
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'olive',
|
||||
'green',
|
||||
'teal',
|
||||
'blue',
|
||||
'violet',
|
||||
'purple',
|
||||
'pink',
|
||||
'brown',
|
||||
'grey',
|
||||
'good',
|
||||
'average',
|
||||
'bad',
|
||||
'label',
|
||||
];
|
||||
|
||||
// VOREStation Edit Start
|
||||
// If you ever add a new radio channel, you can either manually update this, or
|
||||
// go use /client/verb/generate_tgui_radio_constants() in communications.dm.
|
||||
export const RADIO_CHANNELS = [
|
||||
{
|
||||
'name': 'Mercenary',
|
||||
'freq': 1213,
|
||||
'color': '#6D3F40',
|
||||
},
|
||||
{
|
||||
'name': 'Raider',
|
||||
'freq': 1277,
|
||||
'color': '#6D3F40',
|
||||
},
|
||||
{
|
||||
'name': 'Special Ops',
|
||||
'freq': 1341,
|
||||
'color': '#5C5C8A',
|
||||
},
|
||||
{
|
||||
'name': 'AI Private',
|
||||
'freq': 1343,
|
||||
'color': '#FF00FF',
|
||||
},
|
||||
{
|
||||
'name': 'Response Team',
|
||||
'freq': 1345,
|
||||
'color': '#5C5C8A',
|
||||
},
|
||||
{
|
||||
'name': 'Supply',
|
||||
'freq': 1347,
|
||||
'color': '#5F4519',
|
||||
},
|
||||
{
|
||||
'name': 'Service',
|
||||
'freq': 1349,
|
||||
'color': '#6eaa2c',
|
||||
},
|
||||
{
|
||||
'name': 'Science',
|
||||
'freq': 1351,
|
||||
'color': '#993399',
|
||||
},
|
||||
{
|
||||
'name': 'Command',
|
||||
'freq': 1353,
|
||||
'color': '#193A7A',
|
||||
},
|
||||
{
|
||||
'name': 'Medical',
|
||||
'freq': 1355,
|
||||
'color': '#008160',
|
||||
},
|
||||
{
|
||||
'name': 'Engineering',
|
||||
'freq': 1357,
|
||||
'color': '#A66300',
|
||||
},
|
||||
{
|
||||
'name': 'Security',
|
||||
'freq': 1359,
|
||||
'color': '#A30000',
|
||||
},
|
||||
{
|
||||
'name': 'Explorer',
|
||||
'freq': 1361,
|
||||
'color': '#555555',
|
||||
},
|
||||
{
|
||||
'name': 'Talon',
|
||||
'freq': 1363,
|
||||
'color': '#555555',
|
||||
},
|
||||
{
|
||||
'name': 'Common',
|
||||
'freq': 1459,
|
||||
'color': '#008000',
|
||||
},
|
||||
{
|
||||
'name': 'Entertainment',
|
||||
'freq': 1461,
|
||||
'color': '#339966',
|
||||
},
|
||||
{
|
||||
'name': 'Security(I)',
|
||||
'freq': 1475,
|
||||
'color': '#008000',
|
||||
},
|
||||
{
|
||||
'name': 'Medical(I)',
|
||||
'freq': 1485,
|
||||
'color': '#008000',
|
||||
},
|
||||
] as const;
|
||||
|
||||
/*
|
||||
Entries must match /code/defines/gases.dm entries.
|
||||
*/
|
||||
const GASES = [
|
||||
{
|
||||
'id': 'oxygen',
|
||||
'name': 'Oxygen',
|
||||
'label': 'O₂',
|
||||
'color': 'blue',
|
||||
},
|
||||
{
|
||||
'id': 'nitrogen',
|
||||
'name': 'Nitrogen',
|
||||
'label': 'N₂',
|
||||
'color': 'green',
|
||||
},
|
||||
{
|
||||
'id': 'carbon_dioxide',
|
||||
'name': 'Carbon Dioxide',
|
||||
'label': 'CO₂',
|
||||
'color': 'grey',
|
||||
},
|
||||
{
|
||||
'id': 'phoron',
|
||||
'name': 'Phoron',
|
||||
'label': 'Phoron',
|
||||
'color': 'pink',
|
||||
},
|
||||
{
|
||||
'id': 'volatile_fuel',
|
||||
'name': 'Volatile Fuel',
|
||||
'label': 'EXP',
|
||||
'color': 'teal',
|
||||
},
|
||||
{
|
||||
'id': 'nitrous_oxide',
|
||||
'name': 'Nitrous Oxide',
|
||||
'label': 'N₂O',
|
||||
'color': 'red',
|
||||
},
|
||||
{
|
||||
'id': 'other',
|
||||
'name': 'Other',
|
||||
'label': 'Other',
|
||||
'color': 'white',
|
||||
},
|
||||
{
|
||||
'id': 'pressure',
|
||||
'name': 'Pressure',
|
||||
'label': 'Pressure',
|
||||
'color': 'average',
|
||||
},
|
||||
{
|
||||
'id': 'temperature',
|
||||
'name': 'Temperature',
|
||||
'label': 'Temperature',
|
||||
'color': 'yellow',
|
||||
},
|
||||
] as const;
|
||||
|
||||
// VOREStation Edit End
|
||||
|
||||
// Returns gas label based on gasId
|
||||
// Checks GASES for both id (all chars lowercase)
|
||||
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
|
||||
export const getGasLabel = (gasId: string, fallbackValue?: string) => {
|
||||
if (!gasId) return fallbackValue || 'None';
|
||||
|
||||
const gasSearchId = gasId.toLowerCase();
|
||||
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
|
||||
letter.toUpperCase()
|
||||
);
|
||||
|
||||
for (let idx = 0; idx < GASES.length; idx++) {
|
||||
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
|
||||
return GASES[idx].label;
|
||||
}
|
||||
}
|
||||
|
||||
return fallbackValue || 'None';
|
||||
};
|
||||
|
||||
// Returns gas color based on gasId
|
||||
// Checks GASES for both id (all chars lowercase)
|
||||
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
|
||||
export const getGasColor = (gasId: string) => {
|
||||
if (!gasId) return 'black';
|
||||
|
||||
const gasSearchId = gasId.toLowerCase();
|
||||
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
|
||||
letter.toUpperCase()
|
||||
);
|
||||
|
||||
for (let idx = 0; idx < GASES.length; idx++) {
|
||||
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
|
||||
return GASES[idx].color;
|
||||
}
|
||||
}
|
||||
|
||||
return 'black';
|
||||
};
|
||||
|
||||
// Returns gas object based on gasId
|
||||
// Checks GASES for both id (all chars lowercase)
|
||||
// and name (each word start capitalized, to match standards in code\defines\gases.dm)
|
||||
export const getGasFromId = (gasId: string): Gas | undefined => {
|
||||
if (!gasId) return;
|
||||
|
||||
const gasSearchId = gasId.toLowerCase();
|
||||
const gasSearchName = gasId.replace(/(^\w{1})|(\s+\w{1})/g, (letter) =>
|
||||
letter.toUpperCase()
|
||||
);
|
||||
|
||||
for (let idx = 0; idx < GASES.length; idx++) {
|
||||
if (GASES[idx].id === gasSearchId || GASES[idx].name === gasSearchName) {
|
||||
return GASES[idx];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
// Returns gas object based on gasPath
|
||||
export const getGasFromPath = (gasPath: string): Gas | undefined => {
|
||||
if (!gasPath) return;
|
||||
|
||||
for (let idx = 0; idx < GASES.length; idx++) {
|
||||
if (GASES[idx].path === gasPath) {
|
||||
return GASES[idx];
|
||||
}
|
||||
}
|
||||
};
|
||||
*/
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { useLocalState } from '../backend';
|
||||
import { Flex, Section, Tabs } from '../components';
|
||||
import { Pane, Window } from '../layouts';
|
||||
|
||||
const r = require.context('../stories', false, /\.stories\.js$/);
|
||||
|
||||
/**
|
||||
* @returns {{
|
||||
* meta: {
|
||||
* title: string,
|
||||
* render: () => any,
|
||||
* },
|
||||
* }[]}
|
||||
*/
|
||||
const getStories = () => r.keys().map((path) => r(path));
|
||||
|
||||
export const KitchenSink = (props, context) => {
|
||||
const { panel } = props;
|
||||
const [theme] = useLocalState(context, 'kitchenSinkTheme');
|
||||
const [pageIndex, setPageIndex] = useLocalState(context, 'pageIndex', 0);
|
||||
const stories = getStories();
|
||||
const story = stories[pageIndex];
|
||||
const Layout = panel ? Pane : Window;
|
||||
return (
|
||||
<Layout title="Kitchen Sink" width={600} height={500} theme={theme}>
|
||||
<Flex height="100%">
|
||||
<Flex.Item m={1} mr={0}>
|
||||
<Section fill fitted>
|
||||
<Tabs vertical>
|
||||
{stories.map((story, i) => (
|
||||
<Tabs.Tab
|
||||
key={i}
|
||||
color="transparent"
|
||||
selected={i === pageIndex}
|
||||
onClick={() => setPageIndex(i)}>
|
||||
{story.meta.title}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
<Flex.Item position="relative" grow={1}>
|
||||
<Layout.Content scrollable>{story.meta.render()}</Layout.Content>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createAction } from 'common/redux';
|
||||
|
||||
export const toggleKitchenSink = createAction('debug/toggleKitchenSink');
|
||||
export const toggleDebugLayout = createAction('debug/toggleDebugLayout');
|
||||
export const openExternalBrowser = createAction('debug/openExternalBrowser');
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { useSelector } from 'common/redux';
|
||||
import { selectDebug } from './selectors';
|
||||
|
||||
export const useDebug = (context) => useSelector(context, selectDebug);
|
||||
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export { useDebug } from './hooks';
|
||||
export { KitchenSink } from './KitchenSink';
|
||||
export { debugMiddleware, relayMiddleware } from './middleware';
|
||||
export { debugReducer } from './reducer';
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_BACKSPACE, KEY_F10, KEY_F11, KEY_F12 } from 'common/keycodes';
|
||||
import { globalEvents } from '../events';
|
||||
import { acquireHotKey } from '../hotkeys';
|
||||
import { openExternalBrowser, toggleDebugLayout, toggleKitchenSink } from './actions';
|
||||
|
||||
// prettier-ignore
|
||||
const relayedTypes = [
|
||||
'backend/update',
|
||||
'chat/message',
|
||||
];
|
||||
|
||||
export const debugMiddleware = (store) => {
|
||||
acquireHotKey(KEY_F11);
|
||||
acquireHotKey(KEY_F12);
|
||||
globalEvents.on('keydown', (key) => {
|
||||
if (key.code === KEY_F11) {
|
||||
store.dispatch(toggleDebugLayout());
|
||||
}
|
||||
if (key.code === KEY_F12) {
|
||||
store.dispatch(toggleKitchenSink());
|
||||
}
|
||||
if (key.ctrl && key.alt && key.code === KEY_BACKSPACE) {
|
||||
// NOTE: We need to call this in a timeout, because we need a clean
|
||||
// stack in order for this to be a fatal error.
|
||||
setTimeout(() => {
|
||||
// prettier-ignore
|
||||
throw new Error(
|
||||
'OOPSIE WOOPSIE!! UwU We made a fucky wucky!! A wittle'
|
||||
+ ' fucko boingo! The code monkeys at our headquarters are'
|
||||
+ ' working VEWY HAWD to fix this!');
|
||||
});
|
||||
}
|
||||
});
|
||||
return (next) => (action) => next(action);
|
||||
};
|
||||
|
||||
export const relayMiddleware = (store) => {
|
||||
const devServer = require('tgui-dev-server/link/client.cjs');
|
||||
const externalBrowser = location.search === '?external';
|
||||
if (externalBrowser) {
|
||||
devServer.subscribe((msg) => {
|
||||
const { type, payload } = msg;
|
||||
if (type === 'relay' && payload.windowId === Byond.windowId) {
|
||||
store.dispatch({
|
||||
...payload.action,
|
||||
relayed: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
acquireHotKey(KEY_F10);
|
||||
globalEvents.on('keydown', (key) => {
|
||||
if (key === KEY_F10) {
|
||||
store.dispatch(openExternalBrowser());
|
||||
}
|
||||
});
|
||||
}
|
||||
return (next) => (action) => {
|
||||
const { type, payload, relayed } = action;
|
||||
if (type === openExternalBrowser.type) {
|
||||
window.open(location.href + '?external', '_blank');
|
||||
return;
|
||||
}
|
||||
if (relayedTypes.includes(type) && !relayed && !externalBrowser) {
|
||||
devServer.sendMessage({
|
||||
type: 'relay',
|
||||
payload: {
|
||||
windowId: Byond.windowId,
|
||||
action,
|
||||
},
|
||||
});
|
||||
}
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const debugReducer = (state = {}, action) => {
|
||||
const { type, payload } = action;
|
||||
if (type === 'debug/toggleKitchenSink') {
|
||||
return {
|
||||
...state,
|
||||
kitchenSink: !state.kitchenSink,
|
||||
};
|
||||
}
|
||||
if (type === 'debug/toggleDebugLayout') {
|
||||
return {
|
||||
...state,
|
||||
debugLayout: !state.debugLayout,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const selectDebug = (state) => state.debug;
|
||||
@@ -1,286 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { vecAdd, vecMultiply, vecScale, vecSubtract } from 'common/vector';
|
||||
|
||||
import { createLogger } from './logging';
|
||||
import { storage } from 'common/storage';
|
||||
|
||||
const logger = createLogger('drag');
|
||||
const pixelRatio = window.devicePixelRatio ?? 1;
|
||||
let windowKey = Byond.windowId;
|
||||
let dragging = false;
|
||||
let resizing = false;
|
||||
let screenOffset: [number, number] = [0, 0];
|
||||
let screenOffsetPromise: Promise<[number, number]>;
|
||||
let dragPointOffset: [number, number];
|
||||
let resizeMatrix: [number, number];
|
||||
let initialSize: [number, number];
|
||||
let size: [number, number];
|
||||
|
||||
// Set the window key
|
||||
export const setWindowKey = (key: string): void => {
|
||||
windowKey = key;
|
||||
};
|
||||
|
||||
// Get window position
|
||||
export const getWindowPosition = (): [number, number] => [
|
||||
window.screenLeft * pixelRatio,
|
||||
window.screenTop * pixelRatio,
|
||||
];
|
||||
|
||||
// Get window size
|
||||
export const getWindowSize = (): [number, number] => [
|
||||
window.innerWidth * pixelRatio,
|
||||
window.innerHeight * pixelRatio,
|
||||
];
|
||||
|
||||
// Set window position
|
||||
const setWindowPosition = (vec: [number, number]) => {
|
||||
const byondPos = vecAdd(vec, screenOffset);
|
||||
return Byond.winset(Byond.windowId, {
|
||||
pos: byondPos[0] + ',' + byondPos[1],
|
||||
});
|
||||
};
|
||||
|
||||
// Set window size
|
||||
const setWindowSize = (vec: [number, number]) => {
|
||||
return Byond.winset(Byond.windowId, {
|
||||
size: vec[0] + 'x' + vec[1],
|
||||
});
|
||||
};
|
||||
|
||||
// Get screen position
|
||||
const getScreenPosition = (): [number, number] => [
|
||||
0 - screenOffset[0],
|
||||
0 - screenOffset[1],
|
||||
];
|
||||
|
||||
// Get screen size
|
||||
const getScreenSize = (): [number, number] => [
|
||||
window.screen.availWidth * pixelRatio,
|
||||
window.screen.availHeight * pixelRatio,
|
||||
];
|
||||
|
||||
/**
|
||||
* Moves an item to the top of the recents array, and keeps its length
|
||||
* limited to the number in `limit` argument.
|
||||
*
|
||||
* Uses a strict equality check for comparisons.
|
||||
*
|
||||
* Returns new recents and an item which was trimmed.
|
||||
*/
|
||||
export const touchRecents = (
|
||||
recents: string[],
|
||||
touchedItem: string,
|
||||
limit = 50
|
||||
): [string[], string | undefined] => {
|
||||
const nextRecents: string[] = [touchedItem];
|
||||
let trimmedItem: string | undefined;
|
||||
for (let i = 0; i < recents.length; i++) {
|
||||
const item = recents[i];
|
||||
if (item === touchedItem) {
|
||||
continue;
|
||||
}
|
||||
if (nextRecents.length < limit) {
|
||||
nextRecents.push(item);
|
||||
} else {
|
||||
trimmedItem = item;
|
||||
}
|
||||
}
|
||||
return [nextRecents, trimmedItem];
|
||||
};
|
||||
|
||||
// Store window geometry in local storage
|
||||
const storeWindowGeometry = async () => {
|
||||
logger.log('storing geometry');
|
||||
const geometry = {
|
||||
pos: getWindowPosition(),
|
||||
size: getWindowSize(),
|
||||
};
|
||||
storage.set(windowKey, geometry);
|
||||
// Update the list of stored geometries
|
||||
const [geometries, trimmedKey] = touchRecents(
|
||||
(await storage.get('geometries')) || [],
|
||||
windowKey
|
||||
);
|
||||
if (trimmedKey) {
|
||||
storage.remove(trimmedKey);
|
||||
}
|
||||
storage.set('geometries', geometries);
|
||||
};
|
||||
|
||||
// Recall window geometry from local storage and apply it
|
||||
export const recallWindowGeometry = async (
|
||||
options: {
|
||||
fancy?: boolean;
|
||||
pos?: [number, number];
|
||||
size?: [number, number];
|
||||
locked?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const geometry = options.fancy && (await storage.get(windowKey));
|
||||
if (geometry) {
|
||||
logger.log('recalled geometry:', geometry);
|
||||
}
|
||||
// options.pos is assumed to already be in display-pixels
|
||||
let pos = geometry?.pos || options.pos;
|
||||
let size = options.size;
|
||||
// Convert size from css-pixels to display-pixels
|
||||
if (size) {
|
||||
size = [size[0] * pixelRatio, size[1] * pixelRatio];
|
||||
}
|
||||
// Wait until screen offset gets resolved
|
||||
await screenOffsetPromise;
|
||||
const areaAvailable = getScreenSize();
|
||||
// Set window size
|
||||
if (size) {
|
||||
// Constraint size to not exceed available screen area
|
||||
size = [
|
||||
Math.min(areaAvailable[0], size[0]),
|
||||
Math.min(areaAvailable[1], size[1]),
|
||||
];
|
||||
setWindowSize(size);
|
||||
}
|
||||
// Set window position
|
||||
if (pos) {
|
||||
// Constraint window position if monitor lock was set in preferences.
|
||||
if (size && options.locked) {
|
||||
pos = constraintPosition(pos, size)[1];
|
||||
}
|
||||
setWindowPosition(pos);
|
||||
// Set window position at the center of the screen.
|
||||
} else if (size) {
|
||||
pos = vecAdd(
|
||||
vecScale(areaAvailable, 0.5),
|
||||
vecScale(size, -0.5),
|
||||
vecScale(screenOffset, -1.0)
|
||||
);
|
||||
setWindowPosition(pos);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup draggable window
|
||||
export const setupDrag = async () => {
|
||||
// Calculate screen offset caused by the windows taskbar
|
||||
let windowPosition = getWindowPosition();
|
||||
|
||||
screenOffsetPromise = Byond.winget(Byond.windowId, 'pos').then((pos) => [
|
||||
pos.x - windowPosition[0],
|
||||
pos.y - windowPosition[1],
|
||||
]);
|
||||
screenOffset = await screenOffsetPromise;
|
||||
logger.debug('screen offset', screenOffset);
|
||||
};
|
||||
|
||||
/**
|
||||
* Constraints window position to safe screen area, accounting for safe
|
||||
* margins which could be a system taskbar.
|
||||
*/
|
||||
const constraintPosition = (
|
||||
pos: [number, number],
|
||||
size: [number, number]
|
||||
): [boolean, [number, number]] => {
|
||||
const screenPos = getScreenPosition();
|
||||
const screenSize = getScreenSize();
|
||||
const nextPos: [number, number] = [pos[0], pos[1]];
|
||||
let relocated = false;
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const leftBoundary = screenPos[i];
|
||||
const rightBoundary = screenPos[i] + screenSize[i];
|
||||
if (pos[i] < leftBoundary) {
|
||||
nextPos[i] = leftBoundary;
|
||||
relocated = true;
|
||||
} else if (pos[i] + size[i] > rightBoundary) {
|
||||
nextPos[i] = rightBoundary - size[i];
|
||||
relocated = true;
|
||||
}
|
||||
}
|
||||
return [relocated, nextPos];
|
||||
};
|
||||
|
||||
// Start dragging the window
|
||||
export const dragStartHandler = (event: MouseEvent) => {
|
||||
logger.log('drag start');
|
||||
dragging = true;
|
||||
dragPointOffset = vecSubtract(
|
||||
[event.screenX, event.screenY],
|
||||
getWindowPosition()
|
||||
);
|
||||
// Focus click target
|
||||
(event.target as HTMLElement)?.focus();
|
||||
document.addEventListener('mousemove', dragMoveHandler);
|
||||
document.addEventListener('mouseup', dragEndHandler);
|
||||
dragMoveHandler(event);
|
||||
};
|
||||
|
||||
// End dragging the window
|
||||
const dragEndHandler = (event: MouseEvent) => {
|
||||
logger.log('drag end');
|
||||
dragMoveHandler(event);
|
||||
document.removeEventListener('mousemove', dragMoveHandler);
|
||||
document.removeEventListener('mouseup', dragEndHandler);
|
||||
dragging = false;
|
||||
storeWindowGeometry();
|
||||
};
|
||||
|
||||
// Move the window while dragging
|
||||
const dragMoveHandler = (event: MouseEvent) => {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
setWindowPosition(
|
||||
vecSubtract([event.screenX, event.screenY], dragPointOffset)
|
||||
);
|
||||
};
|
||||
|
||||
// Start resizing the window
|
||||
export const resizeStartHandler =
|
||||
(x: number, y: number) => (event: MouseEvent) => {
|
||||
resizeMatrix = [x, y];
|
||||
logger.log('resize start', resizeMatrix);
|
||||
resizing = true;
|
||||
dragPointOffset = vecSubtract(
|
||||
[event.screenX, event.screenY],
|
||||
getWindowPosition()
|
||||
);
|
||||
initialSize = getWindowSize();
|
||||
// Focus click target
|
||||
(event.target as HTMLElement)?.focus();
|
||||
document.addEventListener('mousemove', resizeMoveHandler);
|
||||
document.addEventListener('mouseup', resizeEndHandler);
|
||||
resizeMoveHandler(event);
|
||||
};
|
||||
|
||||
// End resizing the window
|
||||
const resizeEndHandler = (event: MouseEvent) => {
|
||||
logger.log('resize end', size);
|
||||
resizeMoveHandler(event);
|
||||
document.removeEventListener('mousemove', resizeMoveHandler);
|
||||
document.removeEventListener('mouseup', resizeEndHandler);
|
||||
resizing = false;
|
||||
storeWindowGeometry();
|
||||
};
|
||||
|
||||
// Move the window while resizing
|
||||
const resizeMoveHandler = (event: MouseEvent) => {
|
||||
if (!resizing) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const currentOffset = vecSubtract(
|
||||
[event.screenX, event.screenY],
|
||||
getWindowPosition()
|
||||
);
|
||||
const delta = vecSubtract(currentOffset, dragPointOffset);
|
||||
// Extra 1x1 area is added to ensure the browser can see the cursor
|
||||
size = vecAdd(initialSize, vecMultiply(resizeMatrix, delta), [1, 1]);
|
||||
// Sane window size values
|
||||
size[0] = Math.max(size[0], 150 * pixelRatio);
|
||||
size[1] = Math.max(size[1], 50 * pixelRatio);
|
||||
setWindowSize(size);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { KeyEvent, addScrollableNode, canStealFocus, removeScrollableNode, setupGlobalEvents } from './events';
|
||||
|
||||
describe('focusEvents', () => {
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('setupGlobalEvents sets the ignoreWindowFocus flag correctly', () => {
|
||||
setupGlobalEvents({ ignoreWindowFocus: true });
|
||||
// Test other functionality that depends on the ignoreWindowFocus flag
|
||||
});
|
||||
|
||||
it('canStealFocus returns true for input and textarea elements', () => {
|
||||
const inputElement = document.createElement('input');
|
||||
const textareaElement = document.createElement('textarea');
|
||||
const divElement = document.createElement('div');
|
||||
|
||||
expect(canStealFocus(inputElement)).toBe(true);
|
||||
expect(canStealFocus(textareaElement)).toBe(true);
|
||||
expect(canStealFocus(divElement)).toBe(false);
|
||||
});
|
||||
|
||||
it('addScrollableNode and removeScrollableNode manage the list of scrollable nodes correctly', () => {
|
||||
const divElement1 = document.createElement('div');
|
||||
const divElement2 = document.createElement('div');
|
||||
|
||||
addScrollableNode(divElement1);
|
||||
addScrollableNode(divElement2);
|
||||
// Test other functionality that depends on the list of scrollable nodes
|
||||
|
||||
removeScrollableNode(divElement1);
|
||||
removeScrollableNode(divElement2);
|
||||
// Test other functionality that depends on the list of scrollable nodes
|
||||
});
|
||||
|
||||
it('KeyEvent class works correctly', () => {
|
||||
const keyboardEvent = new KeyboardEvent('keydown', {
|
||||
key: 'a',
|
||||
keyCode: 65,
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
|
||||
const keyEvent = new KeyEvent(keyboardEvent, 'keydown', false);
|
||||
|
||||
expect(keyEvent.event).toBe(keyboardEvent);
|
||||
expect(keyEvent.type).toBe('keydown');
|
||||
expect(keyEvent.code).toBe(65);
|
||||
expect(keyEvent.ctrl).toBe(true);
|
||||
expect(keyEvent.alt).toBe(true);
|
||||
expect(keyEvent.shift).toBe(true);
|
||||
expect(keyEvent.repeat).toBe(false);
|
||||
expect(keyEvent.hasModifierKeys()).toBe(true);
|
||||
expect(keyEvent.isModifierKey()).toBe(false);
|
||||
expect(keyEvent.isDown()).toBe(true);
|
||||
expect(keyEvent.isUp()).toBe(false);
|
||||
expect(keyEvent.toString()).toBe('Ctrl+Alt+Shift+A');
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
/**
|
||||
* Normalized browser focus events and BYOND-specific focus helpers.
|
||||
*
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_ALT, KEY_CTRL, KEY_F1, KEY_F12, KEY_SHIFT } from 'common/keycodes';
|
||||
|
||||
import { EventEmitter } from 'common/events';
|
||||
|
||||
export const globalEvents = new EventEmitter();
|
||||
let ignoreWindowFocus = false;
|
||||
|
||||
export const setupGlobalEvents = (
|
||||
options: { ignoreWindowFocus?: boolean } = {}
|
||||
): void => {
|
||||
ignoreWindowFocus = !!options.ignoreWindowFocus;
|
||||
};
|
||||
|
||||
// Window focus
|
||||
// --------------------------------------------------------
|
||||
|
||||
let windowFocusTimeout: ReturnType<typeof setTimeout> | null;
|
||||
let windowFocused = true;
|
||||
|
||||
// Pretend to always be in focus.
|
||||
const setWindowFocus = (value: boolean, delayed?: boolean) => {
|
||||
if (ignoreWindowFocus) {
|
||||
windowFocused = true;
|
||||
return;
|
||||
}
|
||||
if (windowFocusTimeout) {
|
||||
clearTimeout(windowFocusTimeout);
|
||||
windowFocusTimeout = null;
|
||||
}
|
||||
if (delayed) {
|
||||
windowFocusTimeout = setTimeout(() => setWindowFocus(value));
|
||||
return;
|
||||
}
|
||||
if (windowFocused !== value) {
|
||||
windowFocused = value;
|
||||
globalEvents.emit(value ? 'window-focus' : 'window-blur');
|
||||
globalEvents.emit('window-focus-change', value);
|
||||
}
|
||||
};
|
||||
|
||||
// Focus stealing
|
||||
// --------------------------------------------------------
|
||||
|
||||
let focusStolenBy: HTMLElement | null = null;
|
||||
|
||||
export const canStealFocus = (node: HTMLElement) => {
|
||||
const tag = String(node.tagName).toLowerCase();
|
||||
return tag === 'input' || tag === 'textarea';
|
||||
};
|
||||
|
||||
const stealFocus = (node: HTMLElement) => {
|
||||
releaseStolenFocus();
|
||||
focusStolenBy = node;
|
||||
focusStolenBy.addEventListener('blur', releaseStolenFocus);
|
||||
};
|
||||
|
||||
const releaseStolenFocus = () => {
|
||||
if (focusStolenBy) {
|
||||
focusStolenBy.removeEventListener('blur', releaseStolenFocus);
|
||||
focusStolenBy = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Focus follows the mouse
|
||||
// --------------------------------------------------------
|
||||
|
||||
let focusedNode: HTMLElement | null = null;
|
||||
let lastVisitedNode: HTMLElement | null = null;
|
||||
const trackedNodes: HTMLElement[] = [];
|
||||
|
||||
export const addScrollableNode = (node: HTMLElement) => {
|
||||
trackedNodes.push(node);
|
||||
};
|
||||
|
||||
export const removeScrollableNode = (node: HTMLElement) => {
|
||||
const index = trackedNodes.indexOf(node);
|
||||
if (index >= 0) {
|
||||
trackedNodes.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const focusNearestTrackedParent = (node: HTMLElement | null) => {
|
||||
if (focusStolenBy || !windowFocused) {
|
||||
return;
|
||||
}
|
||||
const body = document.body;
|
||||
while (node && node !== body) {
|
||||
if (trackedNodes.includes(node)) {
|
||||
// NOTE: Contains is a DOM4 method
|
||||
if (node.contains(focusedNode)) {
|
||||
return;
|
||||
}
|
||||
focusedNode = node;
|
||||
node.focus();
|
||||
return;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const node = e.target as HTMLElement;
|
||||
if (node !== lastVisitedNode) {
|
||||
lastVisitedNode = node;
|
||||
focusNearestTrackedParent(node);
|
||||
}
|
||||
});
|
||||
|
||||
// Focus event hooks
|
||||
// --------------------------------------------------------
|
||||
|
||||
window.addEventListener('focusin', (e) => {
|
||||
lastVisitedNode = null;
|
||||
focusedNode = e.target as HTMLElement;
|
||||
setWindowFocus(true);
|
||||
if (canStealFocus(e.target as HTMLElement)) {
|
||||
stealFocus(e.target as HTMLElement);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('focusout', (e) => {
|
||||
lastVisitedNode = null;
|
||||
setWindowFocus(false, true);
|
||||
});
|
||||
|
||||
window.addEventListener('blur', (e) => {
|
||||
lastVisitedNode = null;
|
||||
setWindowFocus(false, true);
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', (e) => {
|
||||
setWindowFocus(false);
|
||||
});
|
||||
|
||||
// Key events
|
||||
// --------------------------------------------------------
|
||||
|
||||
const keyHeldByCode: Record<number, boolean> = {};
|
||||
|
||||
export class KeyEvent {
|
||||
event: KeyboardEvent;
|
||||
type: 'keydown' | 'keyup';
|
||||
code: number;
|
||||
ctrl: boolean;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
repeat: boolean;
|
||||
_str?: string;
|
||||
|
||||
constructor(e: KeyboardEvent, type: 'keydown' | 'keyup', repeat?: boolean) {
|
||||
this.event = e;
|
||||
this.type = type;
|
||||
this.code = e.keyCode;
|
||||
this.ctrl = e.ctrlKey;
|
||||
this.shift = e.shiftKey;
|
||||
this.alt = e.altKey;
|
||||
this.repeat = !!repeat;
|
||||
}
|
||||
|
||||
hasModifierKeys() {
|
||||
return this.ctrl || this.alt || this.shift;
|
||||
}
|
||||
|
||||
isModifierKey() {
|
||||
return (
|
||||
this.code === KEY_CTRL || this.code === KEY_SHIFT || this.code === KEY_ALT
|
||||
);
|
||||
}
|
||||
|
||||
isDown() {
|
||||
return this.type === 'keydown';
|
||||
}
|
||||
|
||||
isUp() {
|
||||
return this.type === 'keyup';
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this._str) {
|
||||
return this._str;
|
||||
}
|
||||
this._str = '';
|
||||
if (this.ctrl) {
|
||||
this._str += 'Ctrl+';
|
||||
}
|
||||
if (this.alt) {
|
||||
this._str += 'Alt+';
|
||||
}
|
||||
if (this.shift) {
|
||||
this._str += 'Shift+';
|
||||
}
|
||||
if (this.code >= 48 && this.code <= 90) {
|
||||
this._str += String.fromCharCode(this.code);
|
||||
} else if (this.code >= KEY_F1 && this.code <= KEY_F12) {
|
||||
this._str += 'F' + (this.code - 111);
|
||||
} else {
|
||||
this._str += '[' + this.code + ']';
|
||||
}
|
||||
return this._str;
|
||||
}
|
||||
}
|
||||
|
||||
// IE8: Keydown event is only available on document.
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (canStealFocus(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const code = e.keyCode;
|
||||
const key = new KeyEvent(e, 'keydown', keyHeldByCode[code]);
|
||||
globalEvents.emit('keydown', key);
|
||||
globalEvents.emit('key', key);
|
||||
keyHeldByCode[code] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (canStealFocus(e.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const code = e.keyCode;
|
||||
const key = new KeyEvent(e, 'keyup');
|
||||
globalEvents.emit('keyup', key);
|
||||
globalEvents.emit('key', key);
|
||||
keyHeldByCode[code] = false;
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Various focus helpers.
|
||||
*
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Moves focus to the BYOND map window.
|
||||
*/
|
||||
export const focusMap = () => {
|
||||
Byond.winset('mapwindow.map', {
|
||||
focus: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves focus to the browser window.
|
||||
*/
|
||||
export const focusWindow = () => {
|
||||
Byond.winset(Byond.windowId, {
|
||||
focus: true,
|
||||
});
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import { formatDb, formatMoney, formatSiBaseTenUnit, formatSiUnit, formatTime } from './format';
|
||||
|
||||
describe('formatSiUnit', () => {
|
||||
it('formats base values correctly', () => {
|
||||
const value = 100;
|
||||
const result = formatSiUnit(value);
|
||||
expect(result).toBe('100');
|
||||
});
|
||||
|
||||
it('formats kilo values correctly', () => {
|
||||
const value = 1500;
|
||||
const result = formatSiUnit(value);
|
||||
expect(result).toBe('1.50 k');
|
||||
});
|
||||
|
||||
it('formats micro values correctly', () => {
|
||||
const value = 0.0001;
|
||||
const result = formatSiUnit(value);
|
||||
expect(result).toBe('100 μ');
|
||||
});
|
||||
|
||||
it('formats values with custom units correctly', () => {
|
||||
const value = 0.5;
|
||||
const result = formatSiUnit(value, 0, 'Hz');
|
||||
expect(result).toBe('0.50 Hz');
|
||||
});
|
||||
|
||||
it('handles non-finite values correctly', () => {
|
||||
const value = Infinity;
|
||||
const result = formatSiUnit(value);
|
||||
expect(result).toBe('Infinity');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatMoney', () => {
|
||||
it('formats integer values with default precision', () => {
|
||||
const value = 1234567;
|
||||
const result = formatMoney(value);
|
||||
expect(result).toBe('1\u2009234\u2009567');
|
||||
});
|
||||
|
||||
it('formats float values with specified precision', () => {
|
||||
const value = 1234567.89;
|
||||
const result = formatMoney(value, 2);
|
||||
expect(result).toBe('1\u2009234\u2009567.89');
|
||||
});
|
||||
|
||||
it('formats negative values correctly', () => {
|
||||
const value = -1234567.89;
|
||||
const result = formatMoney(value, 2);
|
||||
expect(result).toBe('-1\u2009234\u2009567.89');
|
||||
});
|
||||
|
||||
it('returns non-finite values as is', () => {
|
||||
const value = Infinity;
|
||||
const result = formatMoney(value);
|
||||
expect(result).toBe('Infinity');
|
||||
});
|
||||
|
||||
it('formats zero correctly', () => {
|
||||
const value = 0;
|
||||
const result = formatMoney(value);
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDb', () => {
|
||||
it('formats positive values correctly', () => {
|
||||
const value = 1;
|
||||
const result = formatDb(value);
|
||||
expect(result).toBe('+0.00 dB');
|
||||
});
|
||||
|
||||
it('formats negative values correctly', () => {
|
||||
const value = 0.5;
|
||||
const result = formatDb(value);
|
||||
expect(result).toBe('-6.02 dB');
|
||||
});
|
||||
|
||||
it('formats Infinity correctly', () => {
|
||||
const value = 0;
|
||||
const result = formatDb(value);
|
||||
expect(result).toBe('-Inf dB');
|
||||
});
|
||||
|
||||
it('formats very large values correctly', () => {
|
||||
const value = 1e6;
|
||||
const result = formatDb(value);
|
||||
expect(result).toBe('+120.00 dB');
|
||||
});
|
||||
|
||||
it('formats very small values correctly', () => {
|
||||
const value = 1e-6;
|
||||
const result = formatDb(value);
|
||||
expect(result).toBe('-120.00 dB');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatSiBaseTenUnit', () => {
|
||||
it('formats SI base 10 units', () => {
|
||||
expect(formatSiBaseTenUnit(1e9)).toBe('1.00 · 10⁹');
|
||||
expect(formatSiBaseTenUnit(1234567890, 0, 'm')).toBe('1.23 · 10⁹ m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatTime', () => {
|
||||
it('formats time values', () => {
|
||||
expect(formatTime(36000)).toBe('01:00:00');
|
||||
expect(formatTime(36610)).toBe('01:01:01');
|
||||
expect(formatTime(36610, 'short')).toBe('1h1m1s');
|
||||
});
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const SI_SYMBOLS = [
|
||||
'f', // femto
|
||||
'p', // pico
|
||||
'n', // nano
|
||||
'μ', // micro
|
||||
'm', // milli
|
||||
// NOTE: This is a space for a reason. When we right align si numbers,
|
||||
// in monospace mode, we want to units and numbers stay in their respective
|
||||
// columns. If rendering in HTML mode, this space will collapse into
|
||||
// a single space anyway.
|
||||
' ', // base
|
||||
'k', // kilo
|
||||
'M', // mega
|
||||
'G', // giga
|
||||
'T', // tera
|
||||
'P', // peta
|
||||
'E', // exa
|
||||
'Z', // zetta
|
||||
'Y', // yotta
|
||||
'R', // ronna
|
||||
'Q', // quecca
|
||||
'F',
|
||||
'N',
|
||||
'H',
|
||||
] as const;
|
||||
|
||||
const SI_BASE_INDEX = SI_SYMBOLS.indexOf(' ');
|
||||
|
||||
// Formats a number to a human readable form, with a custom unit
|
||||
export const formatSiUnit = (
|
||||
value: number,
|
||||
minBase1000 = -SI_BASE_INDEX,
|
||||
unit = ''
|
||||
): string => {
|
||||
if (!isFinite(value)) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
const realBase10 = Math.floor(Math.log10(Math.abs(value)));
|
||||
const base10 = Math.max(minBase1000 * 3, realBase10);
|
||||
const base1000 = Math.floor(base10 / 3);
|
||||
const symbol =
|
||||
SI_SYMBOLS[Math.min(base1000 + SI_BASE_INDEX, SI_SYMBOLS.length - 1)];
|
||||
|
||||
const scaledValue = value / Math.pow(1000, base1000);
|
||||
|
||||
let formattedValue = scaledValue.toFixed(2);
|
||||
if (formattedValue.endsWith('.00')) {
|
||||
formattedValue = formattedValue.slice(0, -3);
|
||||
} else if (formattedValue.endsWith('.0')) {
|
||||
formattedValue = formattedValue.slice(0, -2);
|
||||
}
|
||||
|
||||
return `${formattedValue} ${symbol.trim()}${unit}`.trim();
|
||||
};
|
||||
|
||||
// Formats a number to a human readable form, with power (W) as the unit
|
||||
export const formatPower = (value: number, minBase1000 = 0) => {
|
||||
return formatSiUnit(value, minBase1000, 'W');
|
||||
};
|
||||
|
||||
// Formats a number as a currency string
|
||||
export const formatMoney = (value: number, precision = 0) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// Round the number and make it fixed precision
|
||||
const roundedValue = Number(value.toFixed(precision));
|
||||
|
||||
// Handle the negative sign
|
||||
const isNegative = roundedValue < 0;
|
||||
const absoluteValue = Math.abs(roundedValue);
|
||||
|
||||
// Convert to string and place thousand separators
|
||||
const parts = absoluteValue.toString().split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, '\u2009'); // Thin space
|
||||
|
||||
const formattedValue = parts.join('.');
|
||||
|
||||
return isNegative ? `-${formattedValue}` : formattedValue;
|
||||
};
|
||||
|
||||
// Formats a floating point number as a number on the decibel scale
|
||||
export const formatDb = (value: number) => {
|
||||
const db = 20 * Math.log10(value);
|
||||
const sign = db >= 0 ? '+' : '-';
|
||||
let formatted: string | number = Math.abs(db);
|
||||
|
||||
if (formatted === Infinity) {
|
||||
formatted = 'Inf';
|
||||
} else {
|
||||
formatted = formatted.toFixed(2);
|
||||
}
|
||||
|
||||
return `${sign}${formatted} dB`;
|
||||
};
|
||||
|
||||
const SI_BASE_TEN_UNITS = [
|
||||
'',
|
||||
'· 10³', // kilo
|
||||
'· 10⁶', // mega
|
||||
'· 10⁹', // giga
|
||||
'· 10¹²', // tera
|
||||
'· 10¹⁵', // peta
|
||||
'· 10¹⁸', // exa
|
||||
'· 10²¹', // zetta
|
||||
'· 10²⁴', // yotta
|
||||
'· 10²⁷', // ronna
|
||||
'· 10³⁰', // quecca
|
||||
'· 10³³',
|
||||
'· 10³⁶',
|
||||
'· 10³⁹',
|
||||
] as const;
|
||||
|
||||
// Converts a number to a string with SI base 10 units
|
||||
export const formatSiBaseTenUnit = (
|
||||
value: number,
|
||||
minBase1000 = 0,
|
||||
unit = ''
|
||||
): string => {
|
||||
if (!isFinite(value)) {
|
||||
return 'NaN';
|
||||
}
|
||||
|
||||
const realBase10 = Math.floor(Math.log10(value));
|
||||
const base10 = Math.max(minBase1000 * 3, realBase10);
|
||||
const base1000 = Math.floor(base10 / 3);
|
||||
const symbol = SI_BASE_TEN_UNITS[base1000];
|
||||
|
||||
const scaledValue = value / Math.pow(1000, base1000);
|
||||
const precision = Math.max(0, 2 - (base10 % 3));
|
||||
const formattedValue = scaledValue.toFixed(precision);
|
||||
|
||||
return `${formattedValue} ${symbol} ${unit}`.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats decisecond count into HH:MM:SS display by default
|
||||
* "short" format does not pad and adds hms suffixes
|
||||
*/
|
||||
export const formatTime = (
|
||||
val: number,
|
||||
formatType: 'short' | 'default' = 'default'
|
||||
): string => {
|
||||
const totalSeconds = Math.floor(val / 10);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
|
||||
if (formatType === 'short') {
|
||||
const hoursFormatted = hours > 0 ? `${hours}h` : '';
|
||||
const minutesFormatted = minutes > 0 ? `${minutes}m` : '';
|
||||
const secondsFormatted = seconds > 0 ? `${seconds}s` : '';
|
||||
return `${hoursFormatted}${minutesFormatted}${secondsFormatted}`;
|
||||
}
|
||||
|
||||
const hoursPadded = String(hours).padStart(2, '0');
|
||||
const minutesPadded = String(minutes).padStart(2, '0');
|
||||
const secondsPadded = String(seconds).padStart(2, '0');
|
||||
|
||||
return `${hoursPadded}:${minutesPadded}:${secondsPadded}`;
|
||||
};
|
||||
|
||||
/* VOREStation Addition Start */
|
||||
export const formatCommaNumber = (value) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
// From http://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript
|
||||
let parts = value.toString().split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return parts.join('.');
|
||||
};
|
||||
/* VOREStation Addition End */
|
||||
@@ -1,220 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as keycodes from 'common/keycodes';
|
||||
import { globalEvents, KeyEvent } from './events';
|
||||
import { createLogger } from './logging';
|
||||
|
||||
const logger = createLogger('hotkeys');
|
||||
|
||||
// BYOND macros, in `key: command` format.
|
||||
const byondMacros: Record<string, string> = {};
|
||||
|
||||
// Default set of acquired keys, which will not be sent to BYOND.
|
||||
const hotKeysAcquired = [
|
||||
keycodes.KEY_ESCAPE,
|
||||
keycodes.KEY_ENTER,
|
||||
keycodes.KEY_SPACE,
|
||||
keycodes.KEY_TAB,
|
||||
keycodes.KEY_CTRL,
|
||||
keycodes.KEY_SHIFT,
|
||||
keycodes.KEY_UP,
|
||||
keycodes.KEY_DOWN,
|
||||
keycodes.KEY_LEFT,
|
||||
keycodes.KEY_RIGHT,
|
||||
keycodes.KEY_F5,
|
||||
];
|
||||
|
||||
// State of passed-through keys.
|
||||
const keyState: Record<string, boolean> = {};
|
||||
|
||||
// Custom listeners for key events
|
||||
const keyListeners: ((key: KeyEvent) => void)[] = [];
|
||||
|
||||
/**
|
||||
* Converts a browser keycode to BYOND keycode.
|
||||
*/
|
||||
const keyCodeToByond = (keyCode: number) => {
|
||||
if (keyCode === 16) return 'Shift';
|
||||
if (keyCode === 17) return 'Ctrl';
|
||||
if (keyCode === 18) return 'Alt';
|
||||
if (keyCode === 33) return 'Northeast';
|
||||
if (keyCode === 34) return 'Southeast';
|
||||
if (keyCode === 35) return 'Southwest';
|
||||
if (keyCode === 36) return 'Northwest';
|
||||
if (keyCode === 37) return 'West';
|
||||
if (keyCode === 38) return 'North';
|
||||
if (keyCode === 39) return 'East';
|
||||
if (keyCode === 40) return 'South';
|
||||
if (keyCode === 45) return 'Insert';
|
||||
if (keyCode === 46) return 'Delete';
|
||||
// prettier-ignore
|
||||
if (keyCode >= 48 && keyCode <= 57 || keyCode >= 65 && keyCode <= 90) {
|
||||
return String.fromCharCode(keyCode);
|
||||
}
|
||||
if (keyCode >= 96 && keyCode <= 105) {
|
||||
return 'Numpad' + (keyCode - 96);
|
||||
}
|
||||
if (keyCode >= 112 && keyCode <= 123) {
|
||||
return 'F' + (keyCode - 111);
|
||||
}
|
||||
if (keyCode === 188) return ',';
|
||||
if (keyCode === 189) return '-';
|
||||
if (keyCode === 190) return '.';
|
||||
};
|
||||
|
||||
/**
|
||||
* Keyboard passthrough logic. This allows you to keep doing things
|
||||
* in game while the browser window is focused.
|
||||
*/
|
||||
const handlePassthrough = (key: KeyEvent) => {
|
||||
const keyString = String(key);
|
||||
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
|
||||
if (keyString === 'Ctrl+F5' || keyString === 'Ctrl+R') {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
// Prevent passthrough on Ctrl+F
|
||||
if (keyString === 'Ctrl+F') {
|
||||
return;
|
||||
}
|
||||
// NOTE: Alt modifier is pretty bad and sticky in IE11.
|
||||
// prettier-ignore
|
||||
if (
|
||||
key.event.defaultPrevented
|
||||
|| key.isModifierKey()
|
||||
|| hotKeysAcquired.includes(key.code)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const byondKeyCode = keyCodeToByond(key.code);
|
||||
if (!byondKeyCode) {
|
||||
return;
|
||||
}
|
||||
// Macro
|
||||
const macro = byondMacros[byondKeyCode];
|
||||
if (macro) {
|
||||
logger.debug('macro', macro);
|
||||
return Byond.command(macro);
|
||||
}
|
||||
// KeyDown
|
||||
if (key.isDown() && !keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = true;
|
||||
const command = `KeyDown "${byondKeyCode}"`;
|
||||
logger.debug(command);
|
||||
return Byond.command(command);
|
||||
}
|
||||
// KeyUp
|
||||
if (key.isUp() && keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
const command = `KeyUp "${byondKeyCode}"`;
|
||||
logger.debug(command);
|
||||
return Byond.command(command);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Acquires a lock on the hotkey, which prevents it from being
|
||||
* passed through to BYOND.
|
||||
*/
|
||||
export const acquireHotKey = (keyCode: number) => {
|
||||
hotKeysAcquired.push(keyCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the hotkey available to BYOND again.
|
||||
*/
|
||||
export const releaseHotKey = (keyCode: number) => {
|
||||
const index = hotKeysAcquired.indexOf(keyCode);
|
||||
if (index >= 0) {
|
||||
hotKeysAcquired.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const releaseHeldKeys = () => {
|
||||
for (let byondKeyCode of Object.keys(keyState)) {
|
||||
if (keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
logger.log(`releasing key "${byondKeyCode}"`);
|
||||
Byond.command(`KeyUp "${byondKeyCode}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type ByondSkinMacro = {
|
||||
command: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const setupHotKeys = () => {
|
||||
// Read macros
|
||||
Byond.winget('default.*').then((data: Record<string, string>) => {
|
||||
// Group each macro by ref
|
||||
const groupedByRef: Record<string, ByondSkinMacro> = {};
|
||||
for (let key of Object.keys(data)) {
|
||||
const keyPath = key.split('.');
|
||||
const ref = keyPath[1];
|
||||
const prop = keyPath[2];
|
||||
if (ref && prop) {
|
||||
// This piece of code imperatively adds each property to a
|
||||
// ByondSkinMacro object in the order we meet it, which is hard
|
||||
// to express safely in typescript.
|
||||
if (!groupedByRef[ref]) {
|
||||
groupedByRef[ref] = {} as any;
|
||||
}
|
||||
groupedByRef[ref][prop] = data[key];
|
||||
}
|
||||
}
|
||||
// Insert macros
|
||||
const escapedQuotRegex = /\\"/g;
|
||||
// prettier-ignore
|
||||
const unescape = (str: string) => str
|
||||
.substring(1, str.length - 1)
|
||||
.replace(escapedQuotRegex, '"');
|
||||
for (let ref of Object.keys(groupedByRef)) {
|
||||
const macro = groupedByRef[ref];
|
||||
const byondKeyName = unescape(macro.name);
|
||||
byondMacros[byondKeyName] = unescape(macro.command);
|
||||
}
|
||||
logger.debug('loaded macros', byondMacros);
|
||||
});
|
||||
// Setup event handlers
|
||||
globalEvents.on('window-blur', () => {
|
||||
releaseHeldKeys();
|
||||
});
|
||||
globalEvents.on('key', (key: KeyEvent) => {
|
||||
for (const keyListener of keyListeners) {
|
||||
keyListener(key);
|
||||
}
|
||||
handlePassthrough(key);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers for any key events, such as key down or key up.
|
||||
* This should be preferred over directly connecting to keydown/keyup
|
||||
* as it lets tgui prevent the key from reaching BYOND.
|
||||
*
|
||||
* If using in a component, prefer KeyListener, which automatically handles
|
||||
* stopping listening when unmounting.
|
||||
*
|
||||
* @param callback The function to call whenever a key event occurs
|
||||
* @returns A callback to stop listening
|
||||
*/
|
||||
export const listenForKeyEvents = (callback: (key: KeyEvent) => void) => {
|
||||
keyListeners.push(callback);
|
||||
|
||||
let removed = false;
|
||||
|
||||
return () => {
|
||||
if (removed) {
|
||||
return;
|
||||
}
|
||||
|
||||
removed = true;
|
||||
keyListeners.splice(keyListeners.indexOf(callback), 1);
|
||||
};
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* An equivalent to `fetch`, except will automatically retry.
|
||||
*/
|
||||
export const fetchRetry = (
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
retryTimer: number = 1000
|
||||
): Promise<Response> => {
|
||||
return fetch(url, options).catch(() => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
fetchRetry(url, options, retryTimer).then(resolve);
|
||||
}, retryTimer);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Themes
|
||||
import './styles/main.scss';
|
||||
import './styles/themes/abductor.scss';
|
||||
import './styles/themes/cardtable.scss';
|
||||
import './styles/themes/hackerman.scss';
|
||||
import './styles/themes/malfunction.scss';
|
||||
import './styles/themes/neutral.scss';
|
||||
import './styles/themes/ntos.scss';
|
||||
import './styles/themes/paper.scss';
|
||||
import './styles/themes/pda-retro.scss';
|
||||
import './styles/themes/retro.scss';
|
||||
import './styles/themes/syndicate.scss';
|
||||
import './styles/themes/wizard.scss';
|
||||
import './styles/themes/abstract.scss';
|
||||
|
||||
import { StoreProvider, configureStore } from './store';
|
||||
|
||||
import { captureExternalLinks } from './links';
|
||||
import { createRenderer } from './renderer';
|
||||
import { perf } from 'common/perf';
|
||||
import { setupGlobalEvents } from './events';
|
||||
import { setupHotKeys } from './hotkeys';
|
||||
import { setupHotReloading } from 'tgui-dev-server/link/client.cjs';
|
||||
|
||||
perf.mark('inception', window.performance?.timing?.navigationStart);
|
||||
perf.mark('init');
|
||||
|
||||
const store = configureStore();
|
||||
|
||||
const renderApp = createRenderer(() => {
|
||||
const { getRoutedComponent } = require('./routes');
|
||||
const Component = getRoutedComponent(store);
|
||||
return (
|
||||
<StoreProvider store={store}>
|
||||
<Component />
|
||||
</StoreProvider>
|
||||
);
|
||||
});
|
||||
|
||||
const setupApp = () => {
|
||||
// Delay setup
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', setupApp);
|
||||
return;
|
||||
}
|
||||
|
||||
setupGlobalEvents();
|
||||
setupHotKeys();
|
||||
captureExternalLinks();
|
||||
|
||||
// Re-render UI on store updates
|
||||
store.subscribe(renderApp);
|
||||
|
||||
// Dispatch incoming messages as store actions
|
||||
Byond.subscribe((type, payload) => store.dispatch({ type, payload }));
|
||||
|
||||
// Enable hot module reloading
|
||||
if (module.hot) {
|
||||
setupHotReloading();
|
||||
// prettier-ignore
|
||||
module.hot.accept([
|
||||
'./components',
|
||||
'./debug',
|
||||
'./layouts',
|
||||
'./routes',
|
||||
], () => {
|
||||
renderApp();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setupApp();
|
||||
@@ -1,126 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, ProgressBar, LabeledList, Box, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AICard = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
has_ai,
|
||||
integrity,
|
||||
backup_capacitor,
|
||||
flushing,
|
||||
has_laws,
|
||||
laws,
|
||||
wireless,
|
||||
radio,
|
||||
} = data;
|
||||
|
||||
if (has_ai === 0) {
|
||||
return (
|
||||
<Window width={600} height={470} resizable>
|
||||
<Window.Content>
|
||||
<Section title="Stored AI">
|
||||
<Box>
|
||||
<h3>No AI detected.</h3>
|
||||
</Box>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
} else {
|
||||
let integrityColor = null; // Handles changing color of the integrity bar
|
||||
if (integrity >= 75) {
|
||||
integrityColor = 'green';
|
||||
} else if (integrity >= 25) {
|
||||
integrityColor = 'yellow';
|
||||
} else {
|
||||
integrityColor = 'red';
|
||||
}
|
||||
|
||||
let powerColor = null;
|
||||
if (backup_capacitor >= 75) {
|
||||
powerColor = 'green';
|
||||
}
|
||||
if (backup_capacitor >= 25) {
|
||||
powerColor = 'yellow';
|
||||
} else {
|
||||
powerColor = 'red';
|
||||
}
|
||||
|
||||
return (
|
||||
<Window width={600} height={470} resizable>
|
||||
<Window.Content scrollable>
|
||||
<Section title="Stored AI">
|
||||
<Box bold display="inline-block">
|
||||
<h3>{name}</h3>
|
||||
</Box>
|
||||
<Box>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Integrity">
|
||||
<ProgressBar color={integrityColor} value={integrity / 100} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Power">
|
||||
<ProgressBar
|
||||
color={powerColor}
|
||||
value={backup_capacitor / 100}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Box>
|
||||
<Box color="red">
|
||||
<h2>{flushing === 1 ? 'Wipe of AI in progress...' : ''}</h2>
|
||||
</Box>
|
||||
</Section>
|
||||
|
||||
<Section title="Laws">
|
||||
{(!!has_laws && (
|
||||
<Box>
|
||||
{laws.map((value, key) => (
|
||||
<Box key={key} display="inline-block">
|
||||
{value}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)) || ( // Else, no laws.
|
||||
<Box color="red">
|
||||
<h3>No laws detected.</h3>
|
||||
</Box>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<Section title="Actions">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Wireless Activity">
|
||||
<Button
|
||||
icon={wireless ? 'check' : 'times'}
|
||||
content={wireless ? 'Enabled' : 'Disabled'}
|
||||
color={wireless ? 'green' : 'red'}
|
||||
onClick={() => act('wireless')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Subspace Transceiver">
|
||||
<Button
|
||||
icon={radio ? 'check' : 'times'}
|
||||
content={radio ? 'Enabled' : 'Disabled'}
|
||||
color={radio ? 'green' : 'red'}
|
||||
onClick={() => act('radio')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="AI Power">
|
||||
<Button.Confirm
|
||||
icon="radiation"
|
||||
confirmIcon="radiation"
|
||||
disabled={flushing || integrity === 0}
|
||||
confirmColor="red"
|
||||
content="Shutdown"
|
||||
onClick={() => act('wipe')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,311 +0,0 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, Dimmer, Icon, LabeledList, ProgressBar, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
|
||||
import { FullscreenNotice } from './common/FullscreenNotice';
|
||||
|
||||
export const APC = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
let body = <ApcContent />;
|
||||
|
||||
if (data.gridCheck) {
|
||||
body = <GridCheck />;
|
||||
} else if (data.failTime) {
|
||||
body = <ApcFailure />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Window width={450} height={475} resizable>
|
||||
<Window.Content scrollable>{body}</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const powerStatusMap = {
|
||||
2: {
|
||||
color: 'good',
|
||||
externalPowerText: 'External Power',
|
||||
chargingText: 'Fully Charged',
|
||||
},
|
||||
1: {
|
||||
color: 'average',
|
||||
externalPowerText: 'Low External Power',
|
||||
chargingText: 'Charging',
|
||||
},
|
||||
0: {
|
||||
color: 'bad',
|
||||
externalPowerText: 'No External Power',
|
||||
chargingText: 'Not Charging',
|
||||
},
|
||||
};
|
||||
|
||||
const malfMap = {
|
||||
1: {
|
||||
icon: 'terminal',
|
||||
content: 'Override Programming',
|
||||
action: 'hack',
|
||||
},
|
||||
// 2: {
|
||||
// icon: 'caret-square-down',
|
||||
// content: 'Shunt Core Process',
|
||||
// action: 'occupy',
|
||||
// },
|
||||
// 3: {
|
||||
// icon: 'caret-square-left',
|
||||
// content: 'Return to Main Core',
|
||||
// action: 'deoccupy',
|
||||
// },
|
||||
// 4: {
|
||||
// icon: 'caret-square-down',
|
||||
// content: 'Shunt Core Process',
|
||||
// action: 'occupy',
|
||||
// },
|
||||
};
|
||||
|
||||
const ApcContent = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const locked = data.locked && !data.siliconUser;
|
||||
const normallyLocked = data.normallyLocked;
|
||||
const externalPowerStatus =
|
||||
powerStatusMap[data.externalPower] || powerStatusMap[0];
|
||||
const chargingStatus =
|
||||
powerStatusMap[data.chargingStatus] || powerStatusMap[0];
|
||||
const channelArray = data.powerChannels || [];
|
||||
// const malfStatus = malfMap[data.malfStatus] || null;
|
||||
const adjustedCellChange = data.powerCellStatus / 100;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InterfaceLockNoticeBox
|
||||
deny={data.emagged}
|
||||
denialMessage={
|
||||
<Fragment>
|
||||
<Box color="bad" fontSize="1.5rem">
|
||||
Fault in ID authenticator.
|
||||
</Box>
|
||||
<Box color="bad">Please contact maintenance for service.</Box>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
<Section title="Power Status">
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Main Breaker"
|
||||
color={externalPowerStatus.color}
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.isOperating ? 'power-off' : 'times'}
|
||||
content={data.isOperating ? 'On' : 'Off'}
|
||||
selected={data.isOperating && !locked}
|
||||
color={data.isOperating ? '' : 'bad'}
|
||||
disabled={locked}
|
||||
onClick={() => act('breaker')}
|
||||
/>
|
||||
}>
|
||||
[ {externalPowerStatus.externalPowerText} ]
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Power Cell">
|
||||
<ProgressBar color="good" value={adjustedCellChange} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Charge Mode"
|
||||
color={chargingStatus.color}
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.chargeMode ? 'sync' : 'times'}
|
||||
content={data.chargeMode ? 'Auto' : 'Off'}
|
||||
selected={data.chargeMode}
|
||||
disabled={locked}
|
||||
onClick={() => act('charge')}
|
||||
/>
|
||||
}>
|
||||
[ {chargingStatus.chargingText} ]
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section title="Power Channels">
|
||||
<LabeledList>
|
||||
{channelArray.map((channel) => {
|
||||
const { topicParams } = channel;
|
||||
return (
|
||||
<LabeledList.Item
|
||||
key={channel.title}
|
||||
label={channel.title}
|
||||
buttons={
|
||||
<Fragment>
|
||||
<Box
|
||||
inline
|
||||
mx={2}
|
||||
color={channel.status >= 2 ? 'good' : 'bad'}>
|
||||
{channel.status >= 2 ? 'On' : 'Off'}
|
||||
</Box>
|
||||
<Button
|
||||
icon="sync"
|
||||
content="Auto"
|
||||
selected={
|
||||
!locked &&
|
||||
(channel.status === 1 || channel.status === 3)
|
||||
}
|
||||
disabled={locked}
|
||||
onClick={() => act('channel', topicParams.auto)}
|
||||
/>
|
||||
<Button
|
||||
icon="power-off"
|
||||
content="On"
|
||||
selected={!locked && channel.status === 2}
|
||||
disabled={locked}
|
||||
onClick={() => act('channel', topicParams.on)}
|
||||
/>
|
||||
<Button
|
||||
icon="times"
|
||||
content="Off"
|
||||
selected={!locked && channel.status === 0}
|
||||
disabled={locked}
|
||||
onClick={() => act('channel', topicParams.off)}
|
||||
/>
|
||||
</Fragment>
|
||||
}>
|
||||
{channel.powerLoad} W
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})}
|
||||
<LabeledList.Item label="Total Load">
|
||||
{data.totalCharging ? (
|
||||
<b>
|
||||
{data.totalLoad} W (+ {data.totalCharging} W charging)
|
||||
</b>
|
||||
) : (
|
||||
<b>{data.totalLoad} W</b>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section
|
||||
title="Misc"
|
||||
buttons={
|
||||
!!data.siliconUser && (
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
content="Overload"
|
||||
onClick={() => act('overload')}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Cover Lock"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.coverLocked ? 'lock' : 'unlock'}
|
||||
content={data.coverLocked ? 'Engaged' : 'Disengaged'}
|
||||
selected={data.coverLocked}
|
||||
disabled={locked}
|
||||
onClick={() => act('cover')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<LabeledList.Item
|
||||
label="Night Shift Lighting"
|
||||
buttons={
|
||||
<Fragment>
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
content="Disabled"
|
||||
selected={data.nightshiftSetting === 2}
|
||||
onClick={() =>
|
||||
act('nightshift', {
|
||||
nightshift: 2,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
content="Automatic"
|
||||
selected={data.nightshiftSetting === 1}
|
||||
onClick={() =>
|
||||
act('nightshift', {
|
||||
nightshift: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
content="Enabled"
|
||||
selected={data.nightshiftSetting === 3}
|
||||
onClick={() =>
|
||||
act('nightshift', {
|
||||
nightshift: 3,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
<LabeledList.Item
|
||||
label="Emergency Lighting"
|
||||
buttons={
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
content={data.emergencyLights ? 'Enabled' : 'Disabled'}
|
||||
selected={data.emergencyLights}
|
||||
onClick={() => act('emergency_lighting')}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const GridCheck = (props, context) => {
|
||||
return (
|
||||
<FullscreenNotice title="System Failure">
|
||||
<Box fontSize="1.5rem" bold>
|
||||
<Icon
|
||||
name="exclamation-triangle"
|
||||
verticalAlign="middle"
|
||||
size={3}
|
||||
mr="1rem"
|
||||
/>
|
||||
</Box>
|
||||
<Box fontSize="1.5rem" bold>
|
||||
Power surge detected, grid check in effect...
|
||||
</Box>
|
||||
</FullscreenNotice>
|
||||
);
|
||||
};
|
||||
|
||||
const ApcFailure = (props, context) => {
|
||||
const { data, act } = useBackend(context);
|
||||
|
||||
let rebootOptions = (
|
||||
<Button
|
||||
icon="repeat"
|
||||
content="Restart Now"
|
||||
color="good"
|
||||
onClick={() => act('reboot')}
|
||||
/>
|
||||
);
|
||||
|
||||
if (data.locked && !data.siliconUser) {
|
||||
rebootOptions = <Box color="bad">Swipe an ID card for manual reboot.</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dimmer textAlign="center">
|
||||
<Box color="bad">
|
||||
<h1>SYSTEM FAILURE</h1>
|
||||
</Box>
|
||||
<Box color="average">
|
||||
<h2>
|
||||
I/O regulators malfunction detected! Waiting for system reboot...
|
||||
</h2>
|
||||
</Box>
|
||||
<Box color="good">Automatic reboot in {data.failTime} seconds...</Box>
|
||||
<Box mt={4}>{rebootOptions}</Box>
|
||||
</Dimmer>
|
||||
);
|
||||
};
|
||||
@@ -1,220 +0,0 @@
|
||||
import { useBackend, useSharedState } from '../backend';
|
||||
import { Box, Button, LabeledList, Input, Section, Table, Tabs } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AccountsTerminal = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { id_inserted, id_card, access_level, machine_id } = data;
|
||||
|
||||
return (
|
||||
<Window width={400} height={640}>
|
||||
<Window.Content scrollable>
|
||||
<Section>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Machine" color="average">
|
||||
{machine_id}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="ID">
|
||||
<Button
|
||||
icon={id_inserted ? 'eject' : 'sign-in-alt'}
|
||||
fluid
|
||||
content={id_card}
|
||||
onClick={() => act('insert_card')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
{access_level > 0 && <AccountTerminalContent />}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AccountTerminalContent = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { creating_new_account, detailed_account_view } = data;
|
||||
|
||||
return (
|
||||
<Section title="Menu">
|
||||
<Tabs>
|
||||
<Tabs.Tab
|
||||
selected={!creating_new_account && !detailed_account_view}
|
||||
icon="home"
|
||||
onClick={() => act('view_accounts_list')}>
|
||||
Home
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={creating_new_account}
|
||||
icon="cog"
|
||||
onClick={() => act('create_account')}>
|
||||
New Account
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
disabled={creating_new_account}
|
||||
icon="print"
|
||||
onClick={() => act('print')}>
|
||||
Print
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
{(creating_new_account && <NewAccountView />) ||
|
||||
(detailed_account_view && <DetailedView />) || <ListView />}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const NewAccountView = (props, context) => {
|
||||
const { act } = useBackend(context);
|
||||
|
||||
const [holder, setHolder] = useSharedState(context, 'holder', '');
|
||||
const [newMoney, setMoney] = useSharedState(context, 'money', '');
|
||||
|
||||
return (
|
||||
<Section title="Create Account" level={2}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Account Holder">
|
||||
<Input value={holder} fluid onInput={(e, val) => setHolder(val)} />
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Initial Deposit">
|
||||
<Input value={newMoney} fluid onInput={(e, val) => setMoney(val)} />
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Button
|
||||
disabled={!holder || !newMoney}
|
||||
mt={1}
|
||||
fluid
|
||||
icon="plus"
|
||||
onClick={() =>
|
||||
act('finalise_create_account', {
|
||||
holder_name: holder,
|
||||
starting_funds: newMoney,
|
||||
})
|
||||
}
|
||||
content="Create"
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const DetailedView = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
access_level,
|
||||
station_account_number,
|
||||
account_number,
|
||||
owner_name,
|
||||
money,
|
||||
suspended,
|
||||
transactions,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Section
|
||||
title="Account Details"
|
||||
level={2}
|
||||
buttons={
|
||||
<Button
|
||||
icon="ban"
|
||||
selected={suspended}
|
||||
content="Suspend"
|
||||
onClick={() => act('toggle_suspension')}
|
||||
/>
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Account Number">
|
||||
#{account_number}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Holder">{owner_name}</LabeledList.Item>
|
||||
<LabeledList.Item label="Balance">{money}₮</LabeledList.Item>
|
||||
<LabeledList.Item label="Status" color={suspended ? 'bad' : 'good'}>
|
||||
{suspended ? 'SUSPENDED' : 'Active'}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
<Section title="CentCom Administrator" level={2} mt={1}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Payroll">
|
||||
<Button.Confirm
|
||||
color="bad"
|
||||
fluid
|
||||
icon="ban"
|
||||
confirmIcon="ban"
|
||||
content="Revoke"
|
||||
confirmContent="This cannot be undone."
|
||||
disabled={account_number === station_account_number}
|
||||
onClick={() => act('revoke_payroll')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
{access_level >= 2 && (
|
||||
<Section title="Silent Funds Transfer" level={2}>
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={() => act('add_funds')}
|
||||
content="Add Funds"
|
||||
/>
|
||||
<Button
|
||||
icon="plus"
|
||||
onClick={() => act('remove_funds')}
|
||||
content="Remove Funds"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<Section title="Transactions" level={2} mt={1}>
|
||||
<Table>
|
||||
<Table.Row header>
|
||||
<Table.Cell>Timestamp</Table.Cell>
|
||||
<Table.Cell>Target</Table.Cell>
|
||||
<Table.Cell>Reason</Table.Cell>
|
||||
<Table.Cell>Value</Table.Cell>
|
||||
<Table.Cell>Terminal</Table.Cell>
|
||||
</Table.Row>
|
||||
{transactions.map((trans, i) => (
|
||||
<Table.Row key={i}>
|
||||
<Table.Cell>
|
||||
{trans.date} {trans.time}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{trans.target_name}</Table.Cell>
|
||||
<Table.Cell>{trans.purpose}</Table.Cell>
|
||||
<Table.Cell>{trans.amount}₮</Table.Cell>
|
||||
<Table.Cell>{trans.source_terminal}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const ListView = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { accounts } = data;
|
||||
|
||||
return (
|
||||
<Section title="NanoTrasen Accounts" level={2}>
|
||||
{(accounts.length && (
|
||||
<LabeledList>
|
||||
{accounts.map((acc) => (
|
||||
<LabeledList.Item
|
||||
label={acc.owner_name + acc.suspended}
|
||||
color={acc.suspended ? 'bad' : null}
|
||||
key={acc.account_index}>
|
||||
<Button
|
||||
fluid
|
||||
content={'#' + acc.account_number}
|
||||
onClick={() =>
|
||||
act('view_account_detail', {
|
||||
'account_index': acc.account_index,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
))}
|
||||
</LabeledList>
|
||||
)) || <Box color="bad">There are no accounts available.</Box>}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
import { sortBy } from 'common/collections';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
shuttles: Shuttle[];
|
||||
overmap_ships: OvermapShip[];
|
||||
};
|
||||
|
||||
type Shuttle = {
|
||||
ref: string;
|
||||
name: string;
|
||||
current_location;
|
||||
status;
|
||||
};
|
||||
|
||||
type OvermapShip = {
|
||||
ref: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const AdminShuttleController = () => {
|
||||
return (
|
||||
<Window width={600} height={600} resizable>
|
||||
<Window.Content scrollable>
|
||||
<ShuttleList />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShuttleList = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
|
||||
const { shuttles, overmap_ships } = data;
|
||||
|
||||
return (
|
||||
<Section noTopPadding>
|
||||
<Section title="Classic Shuttles">
|
||||
<Table>
|
||||
{sortBy((f: Shuttle) => f.name)(shuttles).map((shuttle) => (
|
||||
<Table.Row key={shuttle.ref}>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
m={0}
|
||||
content="JMP"
|
||||
onClick={() => act('adminobserve', { ref: shuttle.ref })}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
m={0}
|
||||
content="Fly"
|
||||
onClick={() => act('classicmove', { ref: shuttle.ref })}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{shuttle.name}</Table.Cell>
|
||||
<Table.Cell>{shuttle.current_location}</Table.Cell>
|
||||
<Table.Cell>{shuttleStatusToWords(shuttle.status)}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
<Section title="Overmap Ships">
|
||||
<Table>
|
||||
{sortBy((f: OvermapShip) => f.name?.toLowerCase() || f.name || f.ref)(
|
||||
overmap_ships
|
||||
).map((ship) => (
|
||||
<Table.Row key={ship.ref}>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
content="JMP"
|
||||
onClick={() => act('adminobserve', { ref: ship.ref })}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
content="Control"
|
||||
onClick={() => act('overmap_control', { ref: ship.ref })}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{ship.name}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
/* Helpers */
|
||||
const shuttleStatusToWords = (status) => {
|
||||
switch (status) {
|
||||
case 0:
|
||||
return 'Idle';
|
||||
case 1:
|
||||
return 'Warmup';
|
||||
case 2:
|
||||
return 'Transit';
|
||||
default:
|
||||
return 'UNK';
|
||||
}
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { BooleanLike } from 'common/react';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
entries: { name: string; value: string }[];
|
||||
electronic_warfare: BooleanLike;
|
||||
};
|
||||
|
||||
export const AgentCard = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
|
||||
const { entries, electronic_warfare } = data;
|
||||
|
||||
return (
|
||||
<Window width={550} height={400} theme="syndicate">
|
||||
<Window.Content>
|
||||
<Section title="Info">
|
||||
<Table>
|
||||
{entries.map((a) => (
|
||||
<Table.Row key={a.name}>
|
||||
<Table.Cell>
|
||||
<Button
|
||||
onClick={() => act(a.name.toLowerCase().replace(/ /g, ''))}
|
||||
icon="cog"
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{a.name}</Table.Cell>
|
||||
<Table.Cell>{a.value}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
<Section title="Electronic Warfare">
|
||||
<Button.Checkbox
|
||||
checked={electronic_warfare}
|
||||
content={
|
||||
electronic_warfare
|
||||
? 'Electronic warfare is enabled. This will prevent you from being tracked by the AI.'
|
||||
: 'Electronic warfare disabled.'
|
||||
}
|
||||
onClick={() => act('electronic_warfare')}
|
||||
/>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,196 +0,0 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const dangerMap = {
|
||||
2: {
|
||||
color: 'good',
|
||||
localStatusText: 'Optimal',
|
||||
},
|
||||
1: {
|
||||
color: 'average',
|
||||
localStatusText: 'Caution',
|
||||
},
|
||||
0: {
|
||||
color: 'bad',
|
||||
localStatusText: 'Offline',
|
||||
},
|
||||
};
|
||||
|
||||
export const AiAirlock = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const statusMain = dangerMap[data.power.main] || dangerMap[0];
|
||||
const statusBackup = dangerMap[data.power.backup] || dangerMap[0];
|
||||
const statusElectrify = dangerMap[data.shock] || dangerMap[0];
|
||||
return (
|
||||
<Window width={500} height={390}>
|
||||
<Window.Content>
|
||||
<Section title="Power Status">
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Main"
|
||||
color={statusMain.color}
|
||||
buttons={
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
disabled={!data.power.main}
|
||||
content="Disrupt"
|
||||
onClick={() => act('disrupt-main')}
|
||||
/>
|
||||
}>
|
||||
{data.power.main ? 'Online' : 'Offline'}{' '}
|
||||
{((!data.wires.main_1 || !data.wires.main_2) &&
|
||||
'[Wires have been cut!]') ||
|
||||
(data.power.main_timeleft > 0 &&
|
||||
`[${data.power.main_timeleft}s]`)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Backup"
|
||||
color={statusBackup.color}
|
||||
buttons={
|
||||
<Button
|
||||
icon="lightbulb-o"
|
||||
disabled={!data.power.backup}
|
||||
content="Disrupt"
|
||||
onClick={() => act('disrupt-backup')}
|
||||
/>
|
||||
}>
|
||||
{data.power.backup ? 'Online' : 'Offline'}{' '}
|
||||
{((!data.wires.backup_1 || !data.wires.backup_2) &&
|
||||
'[Wires have been cut!]') ||
|
||||
(data.power.backup_timeleft > 0 &&
|
||||
`[${data.power.backup_timeleft}s]`)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Electrify"
|
||||
color={statusElectrify.color}
|
||||
buttons={
|
||||
<Fragment>
|
||||
<Button
|
||||
icon="wrench"
|
||||
disabled={!(data.wires.shock && data.shock === 0)}
|
||||
content="Restore"
|
||||
onClick={() => act('shock-restore')}
|
||||
/>
|
||||
<Button
|
||||
icon="bolt"
|
||||
disabled={!data.wires.shock}
|
||||
content="Temporary"
|
||||
onClick={() => act('shock-temp')}
|
||||
/>
|
||||
<Button
|
||||
icon="bolt"
|
||||
disabled={!data.wires.shock}
|
||||
content="Permanent"
|
||||
onClick={() => act('shock-perm')}
|
||||
/>
|
||||
</Fragment>
|
||||
}>
|
||||
{data.shock === 2 ? 'Safe' : 'Electrified'}{' '}
|
||||
{(!data.wires.shock && '[Wires have been cut!]') ||
|
||||
(data.shock_timeleft > 0 && `[${data.shock_timeleft}s]`) ||
|
||||
(data.shock_timeleft === -1 && '[Permanent]')}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section title="Access and Door Control">
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="ID Scan"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.id_scanner ? 'power-off' : 'times'}
|
||||
content={data.id_scanner ? 'Enabled' : 'Disabled'}
|
||||
selected={data.id_scanner}
|
||||
disabled={!data.wires.id_scanner}
|
||||
onClick={() => act('idscan-toggle')}
|
||||
/>
|
||||
}>
|
||||
{!data.wires.id_scanner && '[Wires have been cut!]'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Divider />
|
||||
<LabeledList.Item
|
||||
label="Door Bolts"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.locked ? 'lock' : 'unlock'}
|
||||
content={data.locked ? 'Lowered' : 'Raised'}
|
||||
selected={data.locked}
|
||||
disabled={!data.wires.bolts}
|
||||
onClick={() => act('bolt-toggle')}
|
||||
/>
|
||||
}>
|
||||
{!data.wires.bolts && '[Wires have been cut!]'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Door Bolt Lights"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.lights ? 'power-off' : 'times'}
|
||||
content={data.lights ? 'Enabled' : 'Disabled'}
|
||||
selected={data.lights}
|
||||
disabled={!data.wires.lights}
|
||||
onClick={() => act('light-toggle')}
|
||||
/>
|
||||
}>
|
||||
{!data.wires.lights && '[Wires have been cut!]'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Door Force Sensors"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.safe ? 'power-off' : 'times'}
|
||||
content={data.safe ? 'Enabled' : 'Disabled'}
|
||||
selected={data.safe}
|
||||
disabled={!data.wires.safe}
|
||||
onClick={() => act('safe-toggle')}
|
||||
/>
|
||||
}>
|
||||
{!data.wires.safe && '[Wires have been cut!]'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Door Timing Safety"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.speed ? 'power-off' : 'times'}
|
||||
content={data.speed ? 'Enabled' : 'Disabled'}
|
||||
selected={data.speed}
|
||||
disabled={!data.wires.timing}
|
||||
onClick={() => act('speed-toggle')}
|
||||
/>
|
||||
}>
|
||||
{!data.wires.timing && '[Wires have been cut!]'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Divider />
|
||||
<LabeledList.Item
|
||||
label="Door Control"
|
||||
color="bad"
|
||||
buttons={
|
||||
<Button
|
||||
icon={data.opened ? 'sign-out-alt' : 'sign-in-alt'}
|
||||
content={data.opened ? 'Open' : 'Closed'}
|
||||
selected={data.opened}
|
||||
disabled={data.locked || data.welded}
|
||||
onClick={() => act('open-close')}
|
||||
/>
|
||||
}>
|
||||
{!!(data.locked || data.welded) && (
|
||||
<span>
|
||||
[Door is {data.locked ? 'bolted' : ''}
|
||||
{data.locked && data.welded ? ' and ' : ''}
|
||||
{data.welded ? 'welded' : ''}!]
|
||||
</span>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, LabeledList, NoticeBox, ProgressBar, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AiRestorer = () => {
|
||||
return (
|
||||
<Window width={370} height={360} resizable>
|
||||
<Window.Content scrollable>
|
||||
<AiRestorerContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const AiRestorerContent = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
AI_present,
|
||||
error,
|
||||
name,
|
||||
laws,
|
||||
isDead,
|
||||
restoring,
|
||||
health,
|
||||
ejectable,
|
||||
} = data;
|
||||
return (
|
||||
<Fragment>
|
||||
{error && <NoticeBox textAlign="center">{error}</NoticeBox>}
|
||||
{!!ejectable && (
|
||||
<Button
|
||||
fluid
|
||||
icon="eject"
|
||||
content={AI_present ? name : '----------'}
|
||||
disabled={!AI_present}
|
||||
onClick={() => act('PRG_eject')}
|
||||
/>
|
||||
)}
|
||||
{!!AI_present && (
|
||||
<Section
|
||||
title={ejectable ? 'System Status' : name}
|
||||
buttons={
|
||||
<Box inline bold color={isDead ? 'bad' : 'good'}>
|
||||
{isDead ? 'Nonfunctional' : 'Functional'}
|
||||
</Box>
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Integrity">
|
||||
<ProgressBar
|
||||
value={health}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
ranges={{
|
||||
good: [70, Infinity],
|
||||
average: [50, 70],
|
||||
bad: [-Infinity, 50],
|
||||
}}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
{!!restoring && (
|
||||
<Box bold textAlign="center" fontSize="20px" color="good" mt={1}>
|
||||
RECONSTRUCTION IN PROGRESS
|
||||
</Box>
|
||||
)}
|
||||
<Button
|
||||
fluid
|
||||
icon="plus"
|
||||
content="Begin Reconstruction"
|
||||
disabled={restoring}
|
||||
mt={1}
|
||||
onClick={() => act('PRG_beginReconstruction')}
|
||||
/>
|
||||
<Section title="Laws" level={2}>
|
||||
{laws.map((law) => (
|
||||
<Box key={law} className="candystripe">
|
||||
{law}
|
||||
</Box>
|
||||
))}
|
||||
</Section>
|
||||
</Section>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Icon, LabeledList, ProgressBar, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { FullscreenNotice } from './common/FullscreenNotice';
|
||||
|
||||
export const AiSupermatter = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
|
||||
const { integrity_percentage, ambient_temp, ambient_pressure, detonating } =
|
||||
data;
|
||||
|
||||
let body = <AiSupermatterContent />;
|
||||
if (detonating) {
|
||||
body = <AiSupermatterDetonation />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Window width={500} height={300}>
|
||||
<Window.Content>{body}</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AiSupermatterDetonation = (props, context) => (
|
||||
<FullscreenNotice title="DETONATION IMMINENT">
|
||||
<Box fontSize="1.5rem" bold color="bad">
|
||||
<Icon
|
||||
color="bad"
|
||||
name="exclamation-triangle"
|
||||
verticalAlign="middle"
|
||||
size={3}
|
||||
mr="1rem"
|
||||
/>
|
||||
<Box color="bad">CRYSTAL DELAMINATING</Box>
|
||||
<Box color="bad">Evacuate area immediately</Box>
|
||||
</Box>
|
||||
</FullscreenNotice>
|
||||
);
|
||||
|
||||
const AiSupermatterContent = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
|
||||
const { integrity_percentage, ambient_temp, ambient_pressure } = data;
|
||||
|
||||
return (
|
||||
<Section title="Status">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Crystal Integrity">
|
||||
<ProgressBar
|
||||
value={integrity_percentage}
|
||||
maxValue={100}
|
||||
ranges={{
|
||||
good: [90, Infinity],
|
||||
average: [25, 90],
|
||||
bad: [-Infinity, 25],
|
||||
}}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Environment Temperature">
|
||||
<ProgressBar
|
||||
value={ambient_temp}
|
||||
maxValue={10000}
|
||||
ranges={{
|
||||
bad: [5000, Infinity],
|
||||
average: [4000, 5000],
|
||||
good: [-Infinity, 4000],
|
||||
}}>
|
||||
{ambient_temp} K
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Environment Pressure">
|
||||
{ambient_pressure} kPa
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -1,312 +0,0 @@
|
||||
import { toFixed } from 'common/math';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, LabeledList, Section } from '../components';
|
||||
import { getGasLabel, getGasColor } from '../constants';
|
||||
import { Window } from '../layouts';
|
||||
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
|
||||
import { Vent, Scrubber } from './common/AtmosControls';
|
||||
|
||||
export const AirAlarm = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const locked = data.locked && !data.siliconUser && !data.remoteUser;
|
||||
return (
|
||||
<Window width={440} height={650} resizable>
|
||||
<Window.Content scrollable>
|
||||
<InterfaceLockNoticeBox />
|
||||
<AirAlarmStatus />
|
||||
<AirAlarmUnlockedControl />
|
||||
{!locked && <AirAlarmControl />}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AirAlarmStatus = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
const entries = (data.environment_data || []).filter(
|
||||
(entry) => entry.value >= 0.01
|
||||
);
|
||||
const dangerMap = {
|
||||
0: {
|
||||
color: 'good',
|
||||
localStatusText: 'Optimal',
|
||||
},
|
||||
1: {
|
||||
color: 'average',
|
||||
localStatusText: 'Caution',
|
||||
},
|
||||
2: {
|
||||
color: 'bad',
|
||||
localStatusText: 'Danger (Internals Required)',
|
||||
},
|
||||
};
|
||||
const localStatus = dangerMap[data.danger_level] || dangerMap[0];
|
||||
return (
|
||||
<Section title="Air Status">
|
||||
<LabeledList>
|
||||
{(entries.length > 0 && (
|
||||
<Fragment>
|
||||
{entries.map((entry) => {
|
||||
const status = dangerMap[entry.danger_level] || dangerMap[0];
|
||||
return (
|
||||
<LabeledList.Item
|
||||
key={entry.name}
|
||||
label={getGasLabel(entry.name)}
|
||||
color={status.color}>
|
||||
{toFixed(entry.value, 2)}
|
||||
{entry.unit}
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})}
|
||||
<LabeledList.Item label="Local status" color={localStatus.color}>
|
||||
{localStatus.localStatusText}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Area status"
|
||||
color={data.atmos_alarm || data.fire_alarm ? 'bad' : 'good'}>
|
||||
{(data.atmos_alarm && 'Atmosphere Alarm') ||
|
||||
(data.fire_alarm && 'Fire Alarm') ||
|
||||
'Nominal'}
|
||||
</LabeledList.Item>
|
||||
</Fragment>
|
||||
)) || (
|
||||
<LabeledList.Item label="Warning" color="bad">
|
||||
Cannot obtain air sample for analysis.
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
{!!data.emagged && (
|
||||
<LabeledList.Item label="Warning" color="bad">
|
||||
Safety measures offline. Device may exhibit abnormal behavior.
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AirAlarmUnlockedControl = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { target_temperature, rcon } = data;
|
||||
return (
|
||||
<Section title="Comfort Settings">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Remote Control">
|
||||
<Button
|
||||
selected={rcon === 1}
|
||||
content="Off"
|
||||
onClick={() => act('rcon', { 'rcon': 1 })}
|
||||
/>
|
||||
<Button
|
||||
selected={rcon === 2}
|
||||
content="Auto"
|
||||
onClick={() => act('rcon', { 'rcon': 2 })}
|
||||
/>
|
||||
<Button
|
||||
selected={rcon === 3}
|
||||
content="On"
|
||||
onClick={() => act('rcon', { 'rcon': 3 })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Thermostat">
|
||||
<Button
|
||||
content={target_temperature}
|
||||
onClick={() => act('temperature')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AIR_ALARM_ROUTES = {
|
||||
home: {
|
||||
title: 'Air Controls',
|
||||
component: () => AirAlarmControlHome,
|
||||
},
|
||||
vents: {
|
||||
title: 'Vent Controls',
|
||||
component: () => AirAlarmControlVents,
|
||||
},
|
||||
scrubbers: {
|
||||
title: 'Scrubber Controls',
|
||||
component: () => AirAlarmControlScrubbers,
|
||||
},
|
||||
modes: {
|
||||
title: 'Operating Mode',
|
||||
component: () => AirAlarmControlModes,
|
||||
},
|
||||
thresholds: {
|
||||
title: 'Alarm Thresholds',
|
||||
component: () => AirAlarmControlThresholds,
|
||||
},
|
||||
};
|
||||
|
||||
const AirAlarmControl = (props, context) => {
|
||||
const [screen, setScreen] = useLocalState(context, 'screen');
|
||||
const route = AIR_ALARM_ROUTES[screen] || AIR_ALARM_ROUTES.home;
|
||||
const Component = route.component();
|
||||
return (
|
||||
<Section
|
||||
title={route.title}
|
||||
buttons={
|
||||
screen && (
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => setScreen()}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
<Component />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
// Home screen
|
||||
// --------------------------------------------------------
|
||||
|
||||
const AirAlarmControlHome = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [screen, setScreen] = useLocalState(context, 'screen');
|
||||
const { mode, atmos_alarm } = data;
|
||||
return (
|
||||
<Fragment>
|
||||
<Button
|
||||
icon={atmos_alarm ? 'exclamation-triangle' : 'exclamation'}
|
||||
color={atmos_alarm && 'caution'}
|
||||
content="Area Atmosphere Alarm"
|
||||
onClick={() => act(atmos_alarm ? 'reset' : 'alarm')}
|
||||
/>
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon={mode === 3 ? 'exclamation-triangle' : 'exclamation'}
|
||||
color={mode === 3 && 'danger'}
|
||||
content="Panic Siphon"
|
||||
onClick={() =>
|
||||
act('mode', {
|
||||
mode: mode === 3 ? 1 : 3,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box mt={2} />
|
||||
<Button
|
||||
icon="sign-out-alt"
|
||||
content="Vent Controls"
|
||||
onClick={() => setScreen('vents')}
|
||||
/>
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="filter"
|
||||
content="Scrubber Controls"
|
||||
onClick={() => setScreen('scrubbers')}
|
||||
/>
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="cog"
|
||||
content="Operating Mode"
|
||||
onClick={() => setScreen('modes')}
|
||||
/>
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="chart-bar"
|
||||
content="Alarm Thresholds"
|
||||
onClick={() => setScreen('thresholds')}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
// Vents
|
||||
// --------------------------------------------------------
|
||||
|
||||
const AirAlarmControlVents = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
const { vents } = data;
|
||||
if (!vents || vents.length === 0) {
|
||||
return 'Nothing to show';
|
||||
}
|
||||
return vents.map((vent) => <Vent key={vent.id_tag} vent={vent} />);
|
||||
};
|
||||
|
||||
// Scrubbers
|
||||
// --------------------------------------------------------
|
||||
|
||||
const AirAlarmControlScrubbers = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
const { scrubbers } = data;
|
||||
if (!scrubbers || scrubbers.length === 0) {
|
||||
return 'Nothing to show';
|
||||
}
|
||||
return scrubbers.map((scrubber) => (
|
||||
<Scrubber key={scrubber.id_tag} scrubber={scrubber} />
|
||||
));
|
||||
};
|
||||
|
||||
// Modes
|
||||
// --------------------------------------------------------
|
||||
|
||||
const AirAlarmControlModes = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { modes } = data;
|
||||
if (!modes || modes.length === 0) {
|
||||
return 'Nothing to show';
|
||||
}
|
||||
return modes.map((mode) => (
|
||||
<Fragment key={mode.mode}>
|
||||
<Button
|
||||
icon={mode.selected ? 'check-square-o' : 'square-o'}
|
||||
selected={mode.selected}
|
||||
color={mode.selected && mode.danger && 'danger'}
|
||||
content={mode.name}
|
||||
onClick={() => act('mode', { mode: mode.mode })}
|
||||
/>
|
||||
<Box mt={1} />
|
||||
</Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
// Thresholds
|
||||
// --------------------------------------------------------
|
||||
|
||||
const AirAlarmControlThresholds = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { thresholds } = data;
|
||||
return (
|
||||
<table className="LabeledList" style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td />
|
||||
<td className="color-bad">min2</td>
|
||||
<td className="color-average">min1</td>
|
||||
<td className="color-average">max1</td>
|
||||
<td className="color-bad">max2</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{thresholds.map((threshold) => (
|
||||
<tr key={threshold.name}>
|
||||
<td className="LabeledList__label">
|
||||
<span className={'color-' + getGasColor(threshold.name)}>
|
||||
{getGasLabel(threshold.name)}
|
||||
</span>
|
||||
</td>
|
||||
{threshold.settings.map((setting) => (
|
||||
<td key={setting.val}>
|
||||
<Button
|
||||
content={toFixed(setting.selected, 2)}
|
||||
onClick={() =>
|
||||
act('threshold', {
|
||||
env: setting.env,
|
||||
var: setting.val,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
import { Loader } from './common/Loader';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { KEY_ENTER, KEY_ESCAPE, KEY_LEFT, KEY_RIGHT, KEY_SPACE, KEY_TAB } from '../../common/keycodes';
|
||||
import { Autofocus, Box, Button, Flex, Section, Stack } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type AlertModalData = {
|
||||
autofocus: boolean;
|
||||
buttons: string[];
|
||||
large_buttons: boolean;
|
||||
message: string;
|
||||
swapped_buttons: boolean;
|
||||
timeout: number;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const KEY_DECREMENT = -1;
|
||||
const KEY_INCREMENT = 1;
|
||||
|
||||
export const AlertModal = (props, context) => {
|
||||
const { act, data } = useBackend<AlertModalData>(context);
|
||||
const {
|
||||
autofocus,
|
||||
buttons = [],
|
||||
large_buttons,
|
||||
message = '',
|
||||
timeout,
|
||||
title,
|
||||
} = data;
|
||||
const [selected, setSelected] = useLocalState<number>(context, 'selected', 0);
|
||||
// Dynamically sets window dimensions
|
||||
const windowHeight =
|
||||
115 +
|
||||
(message.length > 30 ? Math.ceil(message.length / 4) : 0) +
|
||||
(message.length && large_buttons ? 5 : 0);
|
||||
const windowWidth = 325 + (buttons.length > 2 ? 55 : 0);
|
||||
const onKey = (direction: number) => {
|
||||
if (selected === 0 && direction === KEY_DECREMENT) {
|
||||
setSelected(buttons.length - 1);
|
||||
} else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) {
|
||||
setSelected(0);
|
||||
} else {
|
||||
setSelected(selected + direction);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Window height={windowHeight} title={title} width={windowWidth}>
|
||||
{!!timeout && <Loader value={timeout} />}
|
||||
<Window.Content
|
||||
onKeyDown={(e) => {
|
||||
const keyCode = window.event ? e.which : e.keyCode;
|
||||
/**
|
||||
* Simulate a click when pressing space or enter,
|
||||
* allow keyboard navigation, override tab behavior
|
||||
*/
|
||||
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
|
||||
act('choose', { choice: buttons[selected] });
|
||||
} else if (keyCode === KEY_ESCAPE) {
|
||||
act('cancel');
|
||||
} else if (keyCode === KEY_LEFT) {
|
||||
e.preventDefault();
|
||||
onKey(KEY_DECREMENT);
|
||||
} else if (keyCode === KEY_TAB || keyCode === KEY_RIGHT) {
|
||||
e.preventDefault();
|
||||
onKey(KEY_INCREMENT);
|
||||
}
|
||||
}}>
|
||||
<Section fill>
|
||||
<Stack fill vertical>
|
||||
<Stack.Item grow m={1}>
|
||||
<Box color="label" overflow="hidden">
|
||||
{message}
|
||||
</Box>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{!!autofocus && <Autofocus />}
|
||||
<ButtonDisplay selected={selected} />
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a list of buttons ordered by user prefs.
|
||||
* Technically this handles more than 2 buttons, but you
|
||||
* should just be using a list input in that case.
|
||||
*/
|
||||
const ButtonDisplay = (props, context) => {
|
||||
const { data } = useBackend<AlertModalData>(context);
|
||||
const { buttons = [], large_buttons, swapped_buttons } = data;
|
||||
const { selected } = props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
align="center"
|
||||
direction={!swapped_buttons ? 'row-reverse' : 'row'}
|
||||
fill
|
||||
justify="space-around"
|
||||
wrap>
|
||||
{buttons?.map((button, index) =>
|
||||
!!large_buttons && buttons.length < 3 ? (
|
||||
<Flex.Item grow key={index}>
|
||||
<AlertButton
|
||||
button={button}
|
||||
id={index.toString()}
|
||||
selected={selected === index}
|
||||
/>
|
||||
</Flex.Item>
|
||||
) : (
|
||||
<Flex.Item key={index}>
|
||||
<AlertButton
|
||||
button={button}
|
||||
id={index.toString()}
|
||||
selected={selected === index}
|
||||
/>
|
||||
</Flex.Item>
|
||||
)
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays a button with variable sizing.
|
||||
*/
|
||||
const AlertButton = (props, context) => {
|
||||
const { act, data } = useBackend<AlertModalData>(context);
|
||||
const { large_buttons } = data;
|
||||
const { button, selected } = props;
|
||||
const buttonWidth = button.length > 7 ? button.length : 7;
|
||||
|
||||
return (
|
||||
<Button
|
||||
fluid={!!large_buttons}
|
||||
height={!!large_buttons && 2}
|
||||
onClick={() => act('choose', { choice: button })}
|
||||
m={0.5}
|
||||
pl={2}
|
||||
pr={2}
|
||||
pt={large_buttons ? 0.33 : 0}
|
||||
selected={selected}
|
||||
textAlign="center"
|
||||
width={!large_buttons && buttonWidth}>
|
||||
{!large_buttons ? button : button.toUpperCase()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, NoticeBox, LabeledList, ProgressBar, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { capitalize } from 'common/string';
|
||||
|
||||
export const AlgaeFarm = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const {
|
||||
usePower,
|
||||
materials,
|
||||
last_flow_rate,
|
||||
last_power_draw,
|
||||
inputDir,
|
||||
outputDir,
|
||||
input,
|
||||
output,
|
||||
errorText,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Window width={500} height={300} resizable>
|
||||
<Window.Content>
|
||||
{errorText && (
|
||||
<NoticeBox warning>
|
||||
<Box display="inline-block" verticalAlign="middle">
|
||||
{errorText}
|
||||
</Box>
|
||||
</NoticeBox>
|
||||
)}
|
||||
<Section
|
||||
title="Status"
|
||||
buttons={
|
||||
<Button
|
||||
icon="power-off"
|
||||
content="Processing"
|
||||
selected={usePower === 2}
|
||||
onClick={() => act('toggle')}
|
||||
/>
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Flow Rate">
|
||||
{last_flow_rate} L/s
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Power Draw">
|
||||
{last_power_draw} W
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Divider size={1} />
|
||||
{materials.map((material) => (
|
||||
<LabeledList.Item
|
||||
key={material.name}
|
||||
label={capitalize(material.display)}>
|
||||
<ProgressBar
|
||||
width="80%"
|
||||
value={material.qty}
|
||||
maxValue={material.max}>
|
||||
{material.qty}/{material.max}
|
||||
</ProgressBar>
|
||||
<Button
|
||||
ml={1}
|
||||
content="Eject"
|
||||
onClick={() =>
|
||||
act('ejectMaterial', {
|
||||
mat: material.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
))}
|
||||
</LabeledList>
|
||||
<Table mt={1}>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<Section title={'Gas Input (' + inputDir + ')'}>
|
||||
{input ? (
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Total Pressure">
|
||||
{input.pressure} kPa
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={input.name}>
|
||||
{input.percent}% ({input.moles} moles)
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
) : (
|
||||
<Box color="bad">No connection detected.</Box>
|
||||
)}
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Section title={'Gas Output (' + outputDir + ')'}>
|
||||
{output ? (
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Total Pressure">
|
||||
{output.pressure} kPa
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={output.name}>
|
||||
{output.percent}% ({output.moles} moles)
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
) : (
|
||||
<Box color="bad">No connection detected.</Box>
|
||||
)}
|
||||
</Section>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Table>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,509 +0,0 @@
|
||||
import { sortBy } from 'common/collections';
|
||||
import { capitalize, decodeHtmlEntities } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, ByondUi, Flex, LabeledList, Section, Tabs, ColorBox } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AppearanceChanger = (props, context) => {
|
||||
const { act, config, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
name,
|
||||
specimen,
|
||||
gender,
|
||||
gender_id,
|
||||
hair_style,
|
||||
facial_hair_style,
|
||||
ear_style,
|
||||
tail_style,
|
||||
wing_style,
|
||||
markings,
|
||||
change_race,
|
||||
change_gender,
|
||||
change_eye_color,
|
||||
change_skin_tone,
|
||||
change_skin_color,
|
||||
change_hair_color,
|
||||
change_facial_hair_color,
|
||||
change_hair,
|
||||
change_facial_hair,
|
||||
mapRef,
|
||||
} = data;
|
||||
|
||||
const { title } = config;
|
||||
|
||||
const change_color =
|
||||
change_eye_color ||
|
||||
change_skin_tone ||
|
||||
change_skin_color ||
|
||||
change_hair_color ||
|
||||
change_facial_hair_color;
|
||||
|
||||
let firstAccesibleTab = -1;
|
||||
if (change_race) {
|
||||
firstAccesibleTab = 0;
|
||||
} else if (change_gender) {
|
||||
firstAccesibleTab = 1;
|
||||
} else if (change_color) {
|
||||
firstAccesibleTab = 2;
|
||||
} else if (change_hair) {
|
||||
firstAccesibleTab = 4;
|
||||
} else if (change_facial_hair) {
|
||||
firstAccesibleTab = 5;
|
||||
}
|
||||
|
||||
const [tabIndex, setTabIndex] = useLocalState(
|
||||
context,
|
||||
'tabIndex',
|
||||
firstAccesibleTab
|
||||
);
|
||||
|
||||
return (
|
||||
<Window width={700} height={650} title={decodeHtmlEntities(title)}>
|
||||
<Window.Content>
|
||||
<Section title="Reflection">
|
||||
<Flex>
|
||||
<Flex.Item grow={1}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Name">{name}</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Species"
|
||||
color={!change_race ? 'grey' : null}>
|
||||
{specimen}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Biological Sex"
|
||||
color={!change_gender ? 'grey' : null}>
|
||||
{gender ? capitalize(gender) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Gender Identity"
|
||||
color={!change_color ? 'grey' : null}>
|
||||
{gender_id ? capitalize(gender_id) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Hair Style"
|
||||
color={!change_hair ? 'grey' : null}>
|
||||
{hair_style ? capitalize(hair_style) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Facial Hair Style"
|
||||
color={!change_facial_hair ? 'grey' : null}>
|
||||
{facial_hair_style
|
||||
? capitalize(facial_hair_style)
|
||||
: 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Ear Style"
|
||||
color={!change_hair ? 'grey' : null}>
|
||||
{ear_style ? capitalize(ear_style) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Tail Style"
|
||||
color={!change_hair ? 'grey' : null}>
|
||||
{tail_style ? capitalize(tail_style) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item
|
||||
label="Wing Style"
|
||||
color={!change_hair ? 'grey' : null}>
|
||||
{wing_style ? capitalize(wing_style) : 'Not Set'}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<ByondUi
|
||||
style={{
|
||||
width: '256px',
|
||||
height: '256px',
|
||||
}}
|
||||
params={{
|
||||
id: mapRef,
|
||||
type: 'map',
|
||||
}}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
<Tabs>
|
||||
{change_race ? (
|
||||
<Tabs.Tab selected={tabIndex === 0} onClick={() => setTabIndex(0)}>
|
||||
Race
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{change_gender ? (
|
||||
<Tabs.Tab selected={tabIndex === 1} onClick={() => setTabIndex(1)}>
|
||||
Gender & Sex
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{change_color ? (
|
||||
<Tabs.Tab selected={tabIndex === 2} onClick={() => setTabIndex(2)}>
|
||||
Colors
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{change_hair ? (
|
||||
<Fragment>
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 3}
|
||||
onClick={() => setTabIndex(3)}>
|
||||
Hair
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 5}
|
||||
onClick={() => setTabIndex(5)}>
|
||||
Ear
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 6}
|
||||
onClick={() => setTabIndex(6)}>
|
||||
Tail
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 7}
|
||||
onClick={() => setTabIndex(7)}>
|
||||
Wing
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
selected={tabIndex === 8}
|
||||
onClick={() => setTabIndex(8)}>
|
||||
Markings
|
||||
</Tabs.Tab>
|
||||
</Fragment>
|
||||
) : null}
|
||||
{change_facial_hair ? (
|
||||
<Tabs.Tab selected={tabIndex === 4} onClick={() => setTabIndex(4)}>
|
||||
Facial Hair
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
</Tabs>
|
||||
<Box height="43%">
|
||||
{change_race && tabIndex === 0 ? <AppearanceChangerSpecies /> : null}
|
||||
{change_gender && tabIndex === 1 ? <AppearanceChangerGender /> : null}
|
||||
{change_color && tabIndex === 2 ? <AppearanceChangerColors /> : null}
|
||||
{change_hair && tabIndex === 3 ? <AppearanceChangerHair /> : null}
|
||||
{change_facial_hair && tabIndex === 4 ? (
|
||||
<AppearanceChangerFacialHair />
|
||||
) : null}
|
||||
{change_hair && tabIndex === 5 ? <AppearanceChangerEars /> : null}
|
||||
{change_hair && tabIndex === 6 ? <AppearanceChangerTails /> : null}
|
||||
{change_hair && tabIndex === 7 ? <AppearanceChangerWings /> : null}
|
||||
{change_hair && tabIndex === 8 ? <AppearanceChangerMarkings /> : null}
|
||||
</Box>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerSpecies = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { species, specimen } = data;
|
||||
|
||||
const sortedSpecies = sortBy((val) => val.specimen)(species || []);
|
||||
|
||||
return (
|
||||
<Section title="Species" fill scrollable>
|
||||
{sortedSpecies.map((spec) => (
|
||||
<Button
|
||||
key={spec.specimen}
|
||||
content={spec.specimen}
|
||||
selected={specimen === spec.specimen}
|
||||
onClick={() => act('race', { race: spec.specimen })}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerGender = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { gender, gender_id, genders, id_genders } = data;
|
||||
|
||||
return (
|
||||
<Section title="Gender & Sex" fill scrollable>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Biological Sex">
|
||||
{genders.map((g) => (
|
||||
<Button
|
||||
key={g.gender_key}
|
||||
selected={g.gender_key === gender}
|
||||
content={g.gender_name}
|
||||
onClick={() => act('gender', { 'gender': g.gender_key })}
|
||||
/>
|
||||
))}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Gender Identity">
|
||||
{id_genders.map((g) => (
|
||||
<Button
|
||||
key={g.gender_key}
|
||||
selected={g.gender_key === gender_id}
|
||||
content={g.gender_name}
|
||||
onClick={() => act('gender_id', { 'gender_id': g.gender_key })}
|
||||
/>
|
||||
))}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerColors = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
change_eye_color,
|
||||
change_skin_tone,
|
||||
change_skin_color,
|
||||
change_hair_color,
|
||||
change_facial_hair_color,
|
||||
eye_color,
|
||||
skin_color,
|
||||
hair_color,
|
||||
facial_hair_color,
|
||||
ears_color,
|
||||
ears2_color,
|
||||
tail_color,
|
||||
tail2_color,
|
||||
wing_color,
|
||||
wing2_color,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Section title="Colors" fill scrollable>
|
||||
{change_eye_color ? (
|
||||
<Box>
|
||||
<ColorBox color={eye_color} mr={1} />
|
||||
<Button content="Change Eye Color" onClick={() => act('eye_color')} />
|
||||
</Box>
|
||||
) : null}
|
||||
{change_skin_tone ? (
|
||||
<Box>
|
||||
<Button content="Change Skin Tone" onClick={() => act('skin_tone')} />
|
||||
</Box>
|
||||
) : null}
|
||||
{change_skin_color ? (
|
||||
<Box>
|
||||
<ColorBox color={skin_color} mr={1} />
|
||||
<Button
|
||||
content="Change Skin Color"
|
||||
onClick={() => act('skin_color')}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{change_hair_color ? (
|
||||
<Fragment>
|
||||
<Box>
|
||||
<ColorBox color={hair_color} mr={1} />
|
||||
<Button
|
||||
content="Change Hair Color"
|
||||
onClick={() => act('hair_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={ears_color} mr={1} />
|
||||
<Button
|
||||
content="Change Ears Color"
|
||||
onClick={() => act('ears_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={ears2_color} mr={1} />
|
||||
<Button
|
||||
content="Change Secondary Ears Color"
|
||||
onClick={() => act('ears2_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={tail_color} mr={1} />
|
||||
<Button
|
||||
content="Change Tail Color"
|
||||
onClick={() => act('tail_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={tail2_color} mr={1} />
|
||||
<Button
|
||||
content="Change Secondary Tail Color"
|
||||
onClick={() => act('tail2_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={wing_color} mr={1} />
|
||||
<Button
|
||||
content="Change Wing Color"
|
||||
onClick={() => act('wing_color')}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<ColorBox color={wing2_color} mr={1} />
|
||||
<Button
|
||||
content="Change Secondary Wing Color"
|
||||
onClick={() => act('wing2_color')}
|
||||
/>
|
||||
</Box>
|
||||
</Fragment>
|
||||
) : null}
|
||||
{change_facial_hair_color ? (
|
||||
<Box>
|
||||
<ColorBox color={facial_hair_color} mr={1} />
|
||||
<Button
|
||||
content="Change Facial Hair Color"
|
||||
onClick={() => act('facial_hair_color')}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerHair = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { hair_style, hair_styles } = data;
|
||||
|
||||
return (
|
||||
<Section title="Hair" fill scrollable>
|
||||
{hair_styles.map((hair) => (
|
||||
<Button
|
||||
key={hair.hairstyle}
|
||||
onClick={() => act('hair', { hair: hair.hairstyle })}
|
||||
selected={hair.hairstyle === hair_style}
|
||||
content={hair.hairstyle}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerFacialHair = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { facial_hair_style, facial_hair_styles } = data;
|
||||
|
||||
return (
|
||||
<Section title="Facial Hair" fill scrollable>
|
||||
{facial_hair_styles.map((hair) => (
|
||||
<Button
|
||||
key={hair.facialhairstyle}
|
||||
onClick={() =>
|
||||
act('facial_hair', { facial_hair: hair.facialhairstyle })
|
||||
}
|
||||
selected={hair.facialhairstyle === facial_hair_style}
|
||||
content={hair.facialhairstyle}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerEars = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { ear_style, ear_styles } = data;
|
||||
|
||||
return (
|
||||
<Section title="Ears" fill scrollable>
|
||||
<Button
|
||||
onClick={() => act('ear', { clear: true })}
|
||||
selected={ear_style === null}
|
||||
content="-- Not Set --"
|
||||
/>
|
||||
{sortBy((e) => e.name.toLowerCase())(ear_styles).map((ear) => (
|
||||
<Button
|
||||
key={ear.instance}
|
||||
onClick={() => act('ear', { ref: ear.instance })}
|
||||
selected={ear.name === ear_style}
|
||||
content={ear.name}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerTails = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { tail_style, tail_styles } = data;
|
||||
|
||||
return (
|
||||
<Section title="Tails" fill scrollable>
|
||||
<Button
|
||||
onClick={() => act('tail', { clear: true })}
|
||||
selected={tail_style === null}
|
||||
content="-- Not Set --"
|
||||
/>
|
||||
{sortBy((e) => e.name.toLowerCase())(tail_styles).map((tail) => (
|
||||
<Button
|
||||
key={tail.instance}
|
||||
onClick={() => act('tail', { ref: tail.instance })}
|
||||
selected={tail.name === tail_style}
|
||||
content={tail.name}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerWings = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { wing_style, wing_styles } = data;
|
||||
|
||||
return (
|
||||
<Section title="Wings" fill scrollable>
|
||||
<Button
|
||||
onClick={() => act('wing', { clear: true })}
|
||||
selected={wing_style === null}
|
||||
content="-- Not Set --"
|
||||
/>
|
||||
{sortBy((e) => e.name.toLowerCase())(wing_styles).map((wing) => (
|
||||
<Button
|
||||
key={wing.instance}
|
||||
onClick={() => act('wing', { ref: wing.instance })}
|
||||
selected={wing.name === wing_style}
|
||||
content={wing.name}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const AppearanceChangerMarkings = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { markings } = data;
|
||||
|
||||
return (
|
||||
<Section title="Markings" fill scrollable>
|
||||
<Box>
|
||||
<Button
|
||||
content="Add Marking"
|
||||
onClick={() => act('marking', { todo: 1, name: 'na' })}
|
||||
/>
|
||||
</Box>
|
||||
<LabeledList>
|
||||
{markings.map((m) => (
|
||||
<LabeledList.Item key={m.marking_name} label={m.marking_name}>
|
||||
<ColorBox color={m.marking_color} mr={1} />
|
||||
<Button
|
||||
content="Change Color"
|
||||
onClick={() => act('marking', { todo: 4, name: m.marking_name })}
|
||||
/>
|
||||
<Button
|
||||
content="-"
|
||||
onClick={() => act('marking', { todo: 0, name: m.marking_name })}
|
||||
/>
|
||||
<Button
|
||||
content="Move down"
|
||||
onClick={() => act('marking', { todo: 3, name: m.marking_name })}
|
||||
/>
|
||||
<Button
|
||||
content="Move up"
|
||||
onClick={() => act('marking', { todo: 2, name: m.marking_name })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
))}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, Flex, LabeledList, ProgressBar, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const ArcadeBattle = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
name,
|
||||
temp,
|
||||
enemyAction,
|
||||
enemyName,
|
||||
playerHP,
|
||||
playerMP,
|
||||
enemyHP,
|
||||
enemyMP,
|
||||
gameOver,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Window width={400} height={240} resizable>
|
||||
<Window.Content>
|
||||
<Section title={enemyName} textAlign="center">
|
||||
<Section color="label">
|
||||
<Box>{temp}</Box>
|
||||
<Box>{!gameOver && enemyAction}</Box>
|
||||
</Section>
|
||||
<Flex spacing={1}>
|
||||
<Flex.Item>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Player Health">
|
||||
<ProgressBar
|
||||
value={playerHP}
|
||||
minValue={0}
|
||||
maxValue={30}
|
||||
ranges={{
|
||||
olive: [31, Infinity],
|
||||
good: [20, 31],
|
||||
average: [10, 20],
|
||||
bad: [-Infinity, 10],
|
||||
}}>
|
||||
{playerHP}HP
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Player Magic">
|
||||
<ProgressBar
|
||||
value={playerMP}
|
||||
minValue={0}
|
||||
maxValue={10}
|
||||
ranges={{
|
||||
purple: [11, Infinity],
|
||||
violet: [3, 11],
|
||||
bad: [-Infinity, 3],
|
||||
}}>
|
||||
{playerMP}MP
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Enemy HP">
|
||||
<ProgressBar
|
||||
value={enemyHP}
|
||||
minValue={0}
|
||||
maxValue={45}
|
||||
ranges={{
|
||||
olive: [31, Infinity],
|
||||
good: [20, 31],
|
||||
average: [10, 20],
|
||||
bad: [-Infinity, 10],
|
||||
}}>
|
||||
{enemyHP}HP
|
||||
</ProgressBar>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
{(gameOver && (
|
||||
<Button
|
||||
fluid
|
||||
mt={1}
|
||||
color="green"
|
||||
content="New Game"
|
||||
onClick={() => act('newgame')}
|
||||
/>
|
||||
)) || (
|
||||
<Flex mt={2} justify="space-between" spacing={1}>
|
||||
<Flex.Item grow={1}>
|
||||
<Button
|
||||
fluid
|
||||
icon="fist-raised"
|
||||
tooltip="Go in for the kill!"
|
||||
tooltipPosition="top"
|
||||
onClick={() => act('attack')}
|
||||
content="Attack!"
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<Button
|
||||
fluid
|
||||
icon="band-aid"
|
||||
tooltip="Heal yourself!"
|
||||
tooltipPosition="top"
|
||||
onClick={() => act('heal')}
|
||||
content="Heal!"
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<Button
|
||||
fluid
|
||||
icon="magic"
|
||||
tooltip="Recharge your magic!"
|
||||
tooltipPosition="top"
|
||||
onClick={() => act('charge')}
|
||||
content="Recharge!"
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
)}
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Flex, Button, Box, LabeledList, Section } from '../components';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Window } from '../layouts';
|
||||
import { toTitleCase } from 'common/string';
|
||||
|
||||
export const AreaScrubberControl = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const [showArea, setShowArea] = useLocalState(context, 'showArea', false);
|
||||
|
||||
const { scrubbers } = data;
|
||||
|
||||
if (!scrubbers) {
|
||||
return (
|
||||
<Section title="Error">
|
||||
<Box color="bad">No Scrubbers Detected.</Box>
|
||||
<Button
|
||||
fluid
|
||||
icon="search"
|
||||
content="Scan"
|
||||
onClick={() => act('scan')}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Window width={600} height={400} resizable>
|
||||
<Window.Content scrollable>
|
||||
<Section>
|
||||
<Flex wrap="wrap">
|
||||
<Flex.Item m="2px" basis="49%">
|
||||
<Button
|
||||
textAlign="center"
|
||||
fluid
|
||||
icon="search"
|
||||
content="Scan"
|
||||
onClick={() => act('scan')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item m="2px" basis="49%" grow={1}>
|
||||
<Button
|
||||
textAlign="center"
|
||||
fluid
|
||||
icon="layer-group"
|
||||
content="Show Areas"
|
||||
selected={showArea}
|
||||
onClick={() => setShowArea(!showArea)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item m="2px" basis="49%">
|
||||
<Button
|
||||
textAlign="center"
|
||||
fluid
|
||||
icon="toggle-on"
|
||||
content="All On"
|
||||
onClick={() => act('allon')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item m="2px" basis="49%" grow={1}>
|
||||
<Button
|
||||
textAlign="center"
|
||||
fluid
|
||||
icon="toggle-off"
|
||||
content="All Off"
|
||||
onClick={() => act('alloff')}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
<Flex wrap="wrap">
|
||||
{scrubbers.map((scrubber) => (
|
||||
<Flex.Item m="2px" key={scrubber.id} basis="32%">
|
||||
<BigScrubber scrubber={scrubber} showArea={showArea} />
|
||||
</Flex.Item>
|
||||
))}
|
||||
</Flex>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const BigScrubber = (props, context) => {
|
||||
const { act } = useBackend(context);
|
||||
|
||||
const { scrubber, showArea } = props;
|
||||
|
||||
return (
|
||||
<Section title={scrubber.name}>
|
||||
<Button
|
||||
fluid
|
||||
icon="power-off"
|
||||
content={scrubber.on ? 'Enabled' : 'Disabled'}
|
||||
selected={scrubber.on}
|
||||
onClick={() => act('toggle', { id: scrubber.id })}
|
||||
/>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Pressure">
|
||||
{scrubber.pressure} kPa
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Flow Rate">
|
||||
{scrubber.flow_rate} L/s
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Load">{scrubber.load} W</LabeledList.Item>
|
||||
{showArea && (
|
||||
<LabeledList.Item label="Area">
|
||||
{toTitleCase(scrubber.area)}
|
||||
</LabeledList.Item>
|
||||
)}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { BooleanLike } from 'common/react';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
type Data = {
|
||||
on: BooleanLike;
|
||||
visible: BooleanLike;
|
||||
};
|
||||
|
||||
export const AssemblyInfrared = (props, context) => {
|
||||
const { act, data } = useBackend<Data>(context);
|
||||
const { on, visible } = data;
|
||||
return (
|
||||
<Window>
|
||||
<Window.Content>
|
||||
<Section title="Infrared Unit">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Laser">
|
||||
<Button
|
||||
icon="power-off"
|
||||
fluid
|
||||
selected={on}
|
||||
onClick={() => act('state')}>
|
||||
{on ? 'On' : 'Off'}
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Visibility">
|
||||
<Button
|
||||
icon="eye"
|
||||
fluid
|
||||
selected={visible}
|
||||
onClick={() => act('visible')}>
|
||||
{visible ? 'Able to be seen' : 'Invisible'}
|
||||
</Button>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { round } from 'common/math';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, NumberInput, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { formatTime } from '../format';
|
||||
|
||||
export const AssemblyProx = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { timing, time, range, maxRange, scanning } = data;
|
||||
return (
|
||||
<Window>
|
||||
<Window.Content>
|
||||
<Section title="Timing Unit">
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Timer"
|
||||
buttons={
|
||||
<Button
|
||||
icon="stopwatch"
|
||||
selected={timing}
|
||||
onClick={() => act('timing')}>
|
||||
{timing ? 'Counting Down' : 'Disabled'}
|
||||
</Button>
|
||||
}>
|
||||
<NumberInput
|
||||
animated
|
||||
fluid
|
||||
value={time / 10}
|
||||
minValue={0}
|
||||
maxValue={600}
|
||||
format={(val) => formatTime(round(val))}
|
||||
onDrag={(e, val) => act('set_time', { time: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section title="Prox Unit">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Range">
|
||||
<NumberInput
|
||||
minValue={1}
|
||||
value={range}
|
||||
maxValue={maxRange}
|
||||
onDrag={(e, val) => act('range', { range: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Armed">
|
||||
<Button
|
||||
mr={1}
|
||||
icon={scanning ? 'lock' : 'lock-open'}
|
||||
selected={scanning}
|
||||
onClick={() => act('scanning')}>
|
||||
{scanning ? 'ARMED' : 'Unarmed'}
|
||||
</Button>
|
||||
Movement sensor is active when armed!
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { round } from 'common/math';
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, NumberInput, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { formatTime } from '../format';
|
||||
|
||||
export const AssemblyTimer = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { timing, time } = data;
|
||||
return (
|
||||
<Window>
|
||||
<Window.Content>
|
||||
<Section title="Timing Unit">
|
||||
<LabeledList>
|
||||
<LabeledList.Item
|
||||
label="Timer"
|
||||
buttons={
|
||||
<Button
|
||||
icon="stopwatch"
|
||||
selected={timing}
|
||||
onClick={() => act('timing')}>
|
||||
{timing ? 'Counting Down' : 'Disabled'}
|
||||
</Button>
|
||||
}>
|
||||
<NumberInput
|
||||
animated
|
||||
fluid
|
||||
value={time / 10}
|
||||
minValue={0}
|
||||
maxValue={600}
|
||||
format={(val) => formatTime(round(val))}
|
||||
onDrag={(e, val) => act('set_time', { time: val })}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AtmosAlertConsole = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const priorityAlerts = data.priority_alarms || [];
|
||||
const minorAlerts = data.minor_alarms || [];
|
||||
return (
|
||||
<Window width={350} height={300} resizable>
|
||||
<Window.Content scrollable>
|
||||
<Section title="Alarms">
|
||||
<ul>
|
||||
{priorityAlerts.length === 0 && (
|
||||
<li className="color-good">No Priority Alerts</li>
|
||||
)}
|
||||
{priorityAlerts.map((alert) => (
|
||||
<li key={alert.name}>
|
||||
<Button
|
||||
icon="times"
|
||||
content={alert.name}
|
||||
color="bad"
|
||||
onClick={() => act('clear', { ref: alert.ref })}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{minorAlerts.length === 0 && (
|
||||
<li className="color-good">No Minor Alerts</li>
|
||||
)}
|
||||
{minorAlerts.map((alert) => (
|
||||
<li key={alert.name}>
|
||||
<Button
|
||||
icon="times"
|
||||
content={alert.name}
|
||||
color="average"
|
||||
onClick={() => act('clear', { ref: alert.ref })}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
import { sortBy } from 'common/collections';
|
||||
import { Window } from '../layouts';
|
||||
import { Fragment } from 'inferno';
|
||||
import { Button, Box, Tabs, Icon, Section, NanoMap } from '../components';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { createLogger } from '../logging';
|
||||
const logger = createLogger('fuck');
|
||||
|
||||
export const AtmosControl = (props, context) => {
|
||||
return (
|
||||
<Window width={600} height={440} resizable>
|
||||
<Window.Content scrollable>
|
||||
<AtmosControlContent />
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
export const AtmosControlContent = (props, context) => {
|
||||
const { act, data, config } = useBackend(context);
|
||||
|
||||
let sortedAlarms = sortBy((alarm) => alarm.name)(data.alarms || []);
|
||||
|
||||
// sortedAlarms = sortedAlarms.slice(1, 3);
|
||||
|
||||
const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0);
|
||||
const [zoom, setZoom] = useLocalState(context, 'zoom', 1);
|
||||
|
||||
let body;
|
||||
// Alarms View
|
||||
if (tabIndex === 0) {
|
||||
body = (
|
||||
<Section title="Alarms">
|
||||
{sortedAlarms.map((alarm) => (
|
||||
<Button
|
||||
key={alarm.name}
|
||||
content={alarm.name}
|
||||
color={
|
||||
alarm.danger === 2 ? 'bad' : alarm.danger === 1 ? 'average' : ''
|
||||
}
|
||||
onClick={() => act('alarm', { 'alarm': alarm.ref })}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
} else if (tabIndex === 1) {
|
||||
// Please note, if you ever change the zoom values,
|
||||
// you MUST update styles/components/Tooltip.scss
|
||||
// and change the @for scss to match.
|
||||
body = (
|
||||
<Box height="526px" mb="0.5rem" overflow="hidden">
|
||||
<NanoMap onZoom={(v) => setZoom(v)}>
|
||||
{sortedAlarms
|
||||
.filter((x) => ~~x.z === ~~config.mapZLevel)
|
||||
.map((cm) => (
|
||||
<NanoMap.Marker
|
||||
key={cm.ref}
|
||||
x={cm.x}
|
||||
y={cm.y}
|
||||
zoom={zoom}
|
||||
icon="bell"
|
||||
tooltip={cm.name}
|
||||
color={cm.danger ? 'red' : 'green'}
|
||||
onClick={() => act('alarm', { 'alarm': cm.ref })}
|
||||
/>
|
||||
))}
|
||||
</NanoMap>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Tabs>
|
||||
<Tabs.Tab
|
||||
key="AlarmView"
|
||||
selected={0 === tabIndex}
|
||||
onClick={() => setTabIndex(0)}>
|
||||
<Icon name="table" /> Alarm View
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
key="MapView"
|
||||
selected={1 === tabIndex}
|
||||
onClick={() => setTabIndex(1)}>
|
||||
<Icon name="map-marked-alt" /> Map View
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
<Box m={2}>{body}</Box>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, NumberInput, Section, AnimatedNumber, Box } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AtmosFilter = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const filterTypes = data.filter_types || [];
|
||||
return (
|
||||
<Window width={390} height={187} resizable>
|
||||
<Window.Content>
|
||||
<Section>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Power">
|
||||
<Button
|
||||
icon={data.on ? 'power-off' : 'times'}
|
||||
content={data.on ? 'On' : 'Off'}
|
||||
selected={data.on}
|
||||
onClick={() => act('power')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Transfer Rate">
|
||||
<Box inline mr={1}>
|
||||
<AnimatedNumber
|
||||
value={data.last_flow_rate}
|
||||
format={(val) => val + ' L/s'}
|
||||
/>
|
||||
</Box>
|
||||
<NumberInput
|
||||
animated
|
||||
value={parseFloat(data.rate)}
|
||||
width="63px"
|
||||
unit="L/s"
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
onDrag={(e, value) =>
|
||||
act('rate', {
|
||||
rate: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
ml={1}
|
||||
icon="plus"
|
||||
content="Max"
|
||||
disabled={data.rate === data.max_rate}
|
||||
onClick={() =>
|
||||
act('rate', {
|
||||
rate: 'max',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Filter">
|
||||
{filterTypes.map((filter) => (
|
||||
<Button
|
||||
key={filter.name}
|
||||
selected={filter.selected}
|
||||
content={filter.name}
|
||||
onClick={() =>
|
||||
act('filter', {
|
||||
filterset: filter.f_type,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { Button, LabeledList, NumberInput, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const AtmosMixer = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
return (
|
||||
<Window width={370} height={195} resizable>
|
||||
<Window.Content>
|
||||
<Section>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Power">
|
||||
<Button
|
||||
icon={data.on ? 'power-off' : 'times'}
|
||||
content={data.on ? 'On' : 'Off'}
|
||||
selected={data.on}
|
||||
onClick={() => act('power')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Output Pressure">
|
||||
<NumberInput
|
||||
animated
|
||||
value={parseFloat(data.set_pressure)}
|
||||
unit="kPa"
|
||||
width="75px"
|
||||
minValue={0}
|
||||
maxValue={data.max_pressure}
|
||||
step={10}
|
||||
onChange={(e, value) =>
|
||||
act('pressure', {
|
||||
pressure: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
ml={1}
|
||||
icon="plus"
|
||||
content="Max"
|
||||
disabled={data.set_pressure === data.max_pressure}
|
||||
onClick={() =>
|
||||
act('pressure', {
|
||||
pressure: 'max',
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Divider size={1} />
|
||||
<LabeledList.Item color="label">
|
||||
<u>Concentrations</u>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={'Node 1 (' + data.node1_dir + ')'}>
|
||||
<NumberInput
|
||||
animated
|
||||
value={data.node1_concentration}
|
||||
unit="%"
|
||||
width="60px"
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
stepPixelSize={2}
|
||||
onDrag={(e, value) =>
|
||||
act('node1', {
|
||||
concentration: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label={'Node 2 (' + data.node2_dir + ')'}>
|
||||
<NumberInput
|
||||
animated
|
||||
value={data.node2_concentration}
|
||||
unit="%"
|
||||
width="60px"
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
stepPixelSize={2}
|
||||
onDrag={(e, value) =>
|
||||
act('node2', {
|
||||
concentration: value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import { flow } from 'common/fp';
|
||||
import { filter, sortBy } from 'common/collections';
|
||||
import { useBackend, useSharedState } from '../backend';
|
||||
import { Box, Button, Flex, Input, Section, Dropdown } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { Materials } from './ExosuitFabricator';
|
||||
import { createSearch, toTitleCase } from 'common/string';
|
||||
|
||||
const canBeMade = (recipe, materials, mult = 1) => {
|
||||
if (recipe.requirements === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let recipeRequiredMaterials = Object.keys(recipe.requirements);
|
||||
|
||||
for (let mat_id of recipeRequiredMaterials) {
|
||||
let material = materials.find((val) => val.name === mat_id);
|
||||
if (!material) {
|
||||
continue; // yes, if we cannot find the material, we just ignore it :V
|
||||
}
|
||||
if (material.amount < recipe.requirements[mat_id] * mult) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const Autolathe = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { recipes, busy, materials, categories } = data;
|
||||
|
||||
const [category, setCategory] = useSharedState(context, 'category', 0);
|
||||
|
||||
const [searchText, setSearchText] = useSharedState(
|
||||
context,
|
||||
'search_text',
|
||||
''
|
||||
);
|
||||
|
||||
const testSearch = createSearch(searchText, (recipe) => recipe.name);
|
||||
|
||||
const recipesToShow = flow([
|
||||
filter((recipe) => recipe.category === categories[category]),
|
||||
searchText && filter(testSearch),
|
||||
sortBy((recipe) => recipe.name.toLowerCase()),
|
||||
])(recipes);
|
||||
|
||||
return (
|
||||
<Window width={550} height={700}>
|
||||
<Window.Content scrollable>
|
||||
<Section title="Materials">
|
||||
<Materials disableEject />
|
||||
</Section>
|
||||
<Section
|
||||
title="Recipes"
|
||||
buttons={
|
||||
<Dropdown
|
||||
width="190px"
|
||||
options={categories}
|
||||
selected={categories[category]}
|
||||
onSelected={(val) => setCategory(categories.indexOf(val))}
|
||||
/>
|
||||
}>
|
||||
<Input
|
||||
fluid
|
||||
placeholder="Search for..."
|
||||
onInput={(e, v) => setSearchText(v)}
|
||||
mb={1}
|
||||
/>
|
||||
{recipesToShow.map((recipe) => (
|
||||
<Flex justify="space-between" align="center" key={recipe.ref}>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
color={(recipe.hidden && 'red') || null}
|
||||
icon="hammer"
|
||||
iconSpin={busy === recipe.name}
|
||||
disabled={!canBeMade(recipe, materials, 1)}
|
||||
onClick={() => act('make', { make: recipe.ref })}>
|
||||
{toTitleCase(recipe.name)}
|
||||
</Button>
|
||||
{(!recipe.is_stack && (
|
||||
<Box as="span">
|
||||
<Button
|
||||
color={(recipe.hidden && 'red') || null}
|
||||
disabled={!canBeMade(recipe, materials, 5)}
|
||||
onClick={() =>
|
||||
act('make', { make: recipe.ref, multiplier: 5 })
|
||||
}>
|
||||
x5
|
||||
</Button>
|
||||
<Button
|
||||
color={(recipe.hidden && 'red') || null}
|
||||
disabled={!canBeMade(recipe, materials, 10)}
|
||||
onClick={() =>
|
||||
act('make', { make: recipe.ref, multiplier: 10 })
|
||||
}>
|
||||
x10
|
||||
</Button>
|
||||
</Box>
|
||||
)) ||
|
||||
null}
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
{(recipe.requirements &&
|
||||
Object.keys(recipe.requirements)
|
||||
.map(
|
||||
(mat) =>
|
||||
toTitleCase(mat) + ': ' + recipe.requirements[mat]
|
||||
)
|
||||
.join(', ')) || <Box>No resources required.</Box>}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
))}
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { useBackend } from '../backend';
|
||||
import { AnimatedNumber, Box, Button, LabeledList, ProgressBar, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const Batteryrack = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const {
|
||||
mode,
|
||||
transfer_max,
|
||||
output_load,
|
||||
input_load,
|
||||
equalise,
|
||||
blink_tick,
|
||||
cells_max,
|
||||
cells_cur,
|
||||
cells_list,
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<Window width={500} height={430} resizable>
|
||||
<Window.Content scrollable>
|
||||
<Section title="Controls">
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Current Mode">
|
||||
{(mode === 1 && <Box color="good">OUTPUT ONLY</Box>) ||
|
||||
(mode === 2 && <Box color="good">INPUT ONLY</Box>) ||
|
||||
(mode === 3 && <Box color="good">INPUT AND OUTPUT</Box>) || (
|
||||
<Box color="bad">OFFLINE</Box>
|
||||
)}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Input Status">
|
||||
<AnimatedNumber value={input_load} /> / {transfer_max} W
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Output Status">
|
||||
<AnimatedNumber value={output_load} /> / {transfer_max} W
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Control Panel">
|
||||
<Button
|
||||
content="OFF"
|
||||
selected={mode === 0}
|
||||
onClick={() => act('disable')}
|
||||
/>
|
||||
<Button
|
||||
content="OUT"
|
||||
selected={mode === 1}
|
||||
onClick={() => act('enable', { enable: 1 })}
|
||||
/>
|
||||
<Button
|
||||
content="IN"
|
||||
selected={mode === 2}
|
||||
onClick={() => act('enable', { enable: 2 })}
|
||||
/>
|
||||
<Button
|
||||
content="IN/OUT"
|
||||
selected={mode === 3}
|
||||
onClick={() => act('enable', { enable: 3 })}
|
||||
/>
|
||||
{(equalise && (
|
||||
<Button
|
||||
content="EQ"
|
||||
color={blink_tick ? 'red' : 'yellow'}
|
||||
onClick={() => act('equaliseoff')}
|
||||
/>
|
||||
)) || <Button content="EQ" onClick={() => act('equaliseon')} />}
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
<Section title="Cells">
|
||||
<Table>
|
||||
{cells_list.map((cell) => (
|
||||
<Table.Row key={cell.slot}>
|
||||
<Table.Cell collapsing>Cell {cell.slot}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<ProgressBar
|
||||
value={cell.used ? cell.percentage : 100}
|
||||
minValue={0}
|
||||
maxValue={100}
|
||||
color={cell.used ? 'good' : 'bad'}>
|
||||
{cell.used ? cell.percentage + '%' : 'N/C'}
|
||||
</ProgressBar>
|
||||
</Table.Cell>
|
||||
<Table.Cell collapsing>
|
||||
<Button
|
||||
icon="eject"
|
||||
disabled={!cell.used}
|
||||
onClick={() => act('ejectcell', { ejectcell: cell.id })}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import { toFixed, round } from 'common/math';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, Button, Icon, LabeledList, NumberInput, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const BeaconLocator = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { scan_ticks, degrees, rawfreq, minFrequency, maxFrequency } = data;
|
||||
|
||||
return (
|
||||
<Window width={300} height={220}>
|
||||
<Window.Content>
|
||||
<Section title="Beacon Locator">
|
||||
{(scan_ticks && <Box color="label">Scanning...</Box>) || null}
|
||||
{(degrees && (
|
||||
<Box textAlign="center">
|
||||
<Box textAlign="center">
|
||||
<Icon size={4} name="arrow-up" rotation={degrees} />
|
||||
</Box>
|
||||
Locked on. Follow the arrow.
|
||||
</Box>
|
||||
)) || <Box color="average">No lock.</Box>}
|
||||
<Button
|
||||
mt={1}
|
||||
mb={1}
|
||||
fluid
|
||||
icon="broadcast-tower"
|
||||
onClick={() => act('reset_tracking')}>
|
||||
Reset tracker
|
||||
</Button>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Frequency">
|
||||
<NumberInput
|
||||
animated
|
||||
unit="kHz"
|
||||
step={0.2}
|
||||
stepPixelSize={10}
|
||||
minValue={minFrequency / 10}
|
||||
maxValue={maxFrequency / 10}
|
||||
value={rawfreq / 10}
|
||||
format={(value) => toFixed(value, 1)}
|
||||
onDrag={(e, value) =>
|
||||
act('setFrequency', {
|
||||
freq: round(value * 10),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
@@ -1,195 +0,0 @@
|
||||
import { createSearch } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, Collapsible, Dropdown, Flex, Input, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const sortTypes = {
|
||||
'Alphabetical': (a, b) => a - b,
|
||||
'By availability': (a, b) => -(a.affordable - b.affordable),
|
||||
'By price': (a, b) => a.price - b.price,
|
||||
};
|
||||
|
||||
export const Biogenerator = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
return (
|
||||
<Window width={400} height={450} resizable>
|
||||
<Window.Content className="Layout__content--flexColumn" scrollable>
|
||||
{(data.processing && (
|
||||
<Section title="Processing">
|
||||
The biogenerator is processing reagents!
|
||||
</Section>
|
||||
)) || (
|
||||
<Fragment>
|
||||
<Section>
|
||||
{data.points} points available.
|
||||
<Button ml={1} icon="blender" onClick={() => act('activate')}>
|
||||
Activate
|
||||
</Button>
|
||||
<Button
|
||||
ml={1}
|
||||
icon="eject"
|
||||
disabled={!data.beaker}
|
||||
onClick={() => act('detach')}>
|
||||
Eject Beaker
|
||||
</Button>
|
||||
</Section>
|
||||
<BiogeneratorSearch />
|
||||
<BiogeneratorItems />
|
||||
</Fragment>
|
||||
)}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const BiogeneratorItems = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { points, items } = data;
|
||||
// Search thingies
|
||||
const [searchText, _setSearchText] = useLocalState(context, 'search', '');
|
||||
const [sortOrder, _setSortOrder] = useLocalState(
|
||||
context,
|
||||
'sort',
|
||||
'Alphabetical'
|
||||
);
|
||||
const [descending, _setDescending] = useLocalState(
|
||||
context,
|
||||
'descending',
|
||||
false
|
||||
);
|
||||
const searcher = createSearch(searchText, (item) => {
|
||||
return item[0];
|
||||
});
|
||||
|
||||
let has_contents = false;
|
||||
let contents = Object.entries(items).map((kv, _i) => {
|
||||
let items_in_cat = Object.entries(kv[1])
|
||||
.filter(searcher)
|
||||
.map((kv2) => {
|
||||
kv2[1].affordable = points >= kv2[1].price / data.build_eff;
|
||||
return kv2[1];
|
||||
})
|
||||
.sort(sortTypes[sortOrder]);
|
||||
if (items_in_cat.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (descending) {
|
||||
items_in_cat = items_in_cat.reverse();
|
||||
}
|
||||
|
||||
has_contents = true;
|
||||
return (
|
||||
<BiogeneratorItemsCategory
|
||||
key={kv[0]}
|
||||
title={kv[0]}
|
||||
items={items_in_cat}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<Flex.Item grow="1" overflow="auto">
|
||||
<Section>
|
||||
{has_contents ? (
|
||||
contents
|
||||
) : (
|
||||
<Box color="label">No items matching your criteria was found!</Box>
|
||||
)}
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const BiogeneratorSearch = (props, context) => {
|
||||
const [_searchText, setSearchText] = useLocalState(context, 'search', '');
|
||||
const [_sortOrder, setSortOrder] = useLocalState(context, 'sort', '');
|
||||
const [descending, setDescending] = useLocalState(
|
||||
context,
|
||||
'descending',
|
||||
false
|
||||
);
|
||||
return (
|
||||
<Box mb="0.5rem">
|
||||
<Flex width="100%">
|
||||
<Flex.Item grow="1" mr="0.5rem">
|
||||
<Input
|
||||
placeholder="Search by item name.."
|
||||
width="100%"
|
||||
onInput={(_e, value) => setSearchText(value)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item basis="30%">
|
||||
<Dropdown
|
||||
selected="Alphabetical"
|
||||
options={Object.keys(sortTypes)}
|
||||
width="100%"
|
||||
lineHeight="19px"
|
||||
onSelected={(v) => setSortOrder(v)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
icon={descending ? 'arrow-down' : 'arrow-up'}
|
||||
height="19px"
|
||||
tooltip={descending ? 'Descending order' : 'Ascending order'}
|
||||
tooltipPosition="bottom-end"
|
||||
ml="0.5rem"
|
||||
onClick={() => setDescending(!descending)}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const canBuyItem = (item, data) => {
|
||||
if (!item.affordable) {
|
||||
return false;
|
||||
}
|
||||
if (item.reagent && !data.beaker) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const BiogeneratorItemsCategory = (properties, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { title, items, ...rest } = properties;
|
||||
return (
|
||||
<Collapsible open title={title} {...rest}>
|
||||
{items.map((item) => (
|
||||
<Box key={item.name}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
lineHeight="20px"
|
||||
style={{
|
||||
float: 'left',
|
||||
}}>
|
||||
{item.name}
|
||||
</Box>
|
||||
<Button
|
||||
disabled={!canBuyItem(item, data)}
|
||||
content={(item.price / data.build_eff).toLocaleString('en-US')}
|
||||
width="15%"
|
||||
textAlign="center"
|
||||
style={{
|
||||
float: 'right',
|
||||
}}
|
||||
onClick={() =>
|
||||
act('purchase', {
|
||||
cat: title,
|
||||
name: item.name,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
clear: 'both',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
@@ -1,340 +0,0 @@
|
||||
import { capitalize } from 'common/string';
|
||||
import { useBackend } from '../backend';
|
||||
import { Box, ByondUi, Button, Flex, LabeledList, Section, ColorBox } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
export const BodyDesigner = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
const { menu, disk, diskStored, activeBodyRecord } = data;
|
||||
|
||||
let body = MenuToTemplate[menu];
|
||||
|
||||
return (
|
||||
<Window width={400} height={650}>
|
||||
<Window.Content>
|
||||
{disk ? (
|
||||
<Box>
|
||||
<Button
|
||||
icon="save"
|
||||
content="Save To Disk"
|
||||
onClick={() => act('savetodisk')}
|
||||
disabled={!activeBodyRecord}
|
||||
/>
|
||||
<Button
|
||||
icon="save"
|
||||
content="Load From Disk"
|
||||
onClick={() => act('loadfromdisk')}
|
||||
disabled={!diskStored}
|
||||
/>
|
||||
<Button
|
||||
icon="eject"
|
||||
content="Eject"
|
||||
onClick={() => act('ejectdisk')}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{body}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyDesignerMain = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
return (
|
||||
<Section title="Database Functions">
|
||||
<Button
|
||||
icon="eye"
|
||||
content="View Individual Body Records"
|
||||
onClick={() => act('menu', { menu: 'Body Records' })}
|
||||
/>
|
||||
<Button
|
||||
icon="eye"
|
||||
content="View Stock Body Records"
|
||||
onClick={() => act('menu', { menu: 'Stock Records' })}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyDesignerBodyRecords = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { bodyrecords } = data;
|
||||
return (
|
||||
<Section
|
||||
title="Body Records"
|
||||
buttons={
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => act('menu', { menu: 'Main' })}
|
||||
/>
|
||||
}>
|
||||
{bodyrecords.map((record) => (
|
||||
<Button
|
||||
icon="eye"
|
||||
key={record.name}
|
||||
content={record.name}
|
||||
onClick={() => act('view_brec', { view_brec: record.recref })}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyDesignerStockRecords = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { stock_bodyrecords } = data;
|
||||
return (
|
||||
<Section
|
||||
title="Stock Records"
|
||||
buttons={
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => act('menu', { menu: 'Main' })}
|
||||
/>
|
||||
}>
|
||||
{stock_bodyrecords.map((record) => (
|
||||
<Button
|
||||
icon="eye"
|
||||
key={record}
|
||||
content={record}
|
||||
onClick={() => act('view_stock_brec', { view_stock_brec: record })}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyDesignerSpecificRecord = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { activeBodyRecord, mapRef } = data;
|
||||
return activeBodyRecord ? (
|
||||
<Flex direction="column">
|
||||
<Flex.Item basis="165px">
|
||||
<Section
|
||||
title="Specific Record"
|
||||
buttons={
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => act('menu', { menu: 'Main' })}
|
||||
/>
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Name">
|
||||
{activeBodyRecord.real_name}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Species">
|
||||
{activeBodyRecord.speciesname}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Bio. Sex">
|
||||
<Button
|
||||
icon="pen"
|
||||
content={capitalize(activeBodyRecord.gender)}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: 'bio_gender',
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Synthetic">
|
||||
{activeBodyRecord.synthetic}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Mind Compat">
|
||||
{activeBodyRecord.locked}
|
||||
<Button
|
||||
ml={1}
|
||||
icon="eye"
|
||||
content="View OOC Notes"
|
||||
disabled={!activeBodyRecord.booc}
|
||||
onClick={() => act('boocnotes')}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
<Flex.Item basis="130px">
|
||||
<ByondUi
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '128px',
|
||||
}}
|
||||
params={{
|
||||
id: mapRef,
|
||||
type: 'map',
|
||||
}}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item basis="300px">
|
||||
<Section title="Customize" height="300px" style={{ overflow: 'auto' }}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Scale">
|
||||
<Button
|
||||
icon="pen"
|
||||
content={activeBodyRecord.scale}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: 'size_multiplier',
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
{Object.keys(activeBodyRecord.styles).map((key) => {
|
||||
const style = activeBodyRecord.styles[key];
|
||||
return (
|
||||
<LabeledList.Item key={key} label={key}>
|
||||
{style.styleHref ? (
|
||||
<Button
|
||||
icon="pen"
|
||||
content={style.style}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: style.styleHref,
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{style.colorHref ? (
|
||||
<Box>
|
||||
<Button
|
||||
icon="pen"
|
||||
content={style.color}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: style.colorHref,
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
verticalAlign="top"
|
||||
width="32px"
|
||||
height="20px"
|
||||
color={style.color}
|
||||
style={{
|
||||
border: '1px solid #fff',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
{style.colorHref2 ? (
|
||||
<Box>
|
||||
<Button
|
||||
icon="pen"
|
||||
content={style.color2}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: style.colorHref2,
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<ColorBox
|
||||
verticalAlign="top"
|
||||
width="32px"
|
||||
height="20px"
|
||||
color={style.color2}
|
||||
style={{
|
||||
border: '1px solid #fff',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</LabeledList.Item>
|
||||
);
|
||||
})}
|
||||
<LabeledList.Item label="Body Markings">
|
||||
<Button
|
||||
icon="plus"
|
||||
content="Add Marking"
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: 'marking_style',
|
||||
target_value: 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Flex wrap="wrap" justify="center" align="center">
|
||||
{Object.keys(activeBodyRecord.markings).map((key) => {
|
||||
const marking = activeBodyRecord.markings[key];
|
||||
return (
|
||||
<Flex.Item basis="100%" key={key}>
|
||||
<Flex>
|
||||
<Flex.Item>
|
||||
<Button
|
||||
mr={0.2}
|
||||
fluid
|
||||
icon="times"
|
||||
color="red"
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: 'marking_remove',
|
||||
target_value: key,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex.Item>
|
||||
<Flex.Item grow={1}>
|
||||
<Button
|
||||
fluid
|
||||
backgroundColor={marking}
|
||||
content={key}
|
||||
onClick={() =>
|
||||
act('href_conversion', {
|
||||
target_href: 'marking_color',
|
||||
target_value: key,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</LabeledList.Item>
|
||||
</LabeledList>
|
||||
</Section>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
) : (
|
||||
<Box color="bad">ERROR: Record Not Found!</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyDesignerOOCNotes = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { activeBodyRecord } = data;
|
||||
return (
|
||||
<Section
|
||||
title="Body OOC Notes (This is OOC!)"
|
||||
height="100%"
|
||||
scrollable
|
||||
buttons={
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => act('menu', { menu: 'Specific Record' })}
|
||||
/>
|
||||
}
|
||||
style={{ 'word-break': 'break-all' }}>
|
||||
{(activeBodyRecord && activeBodyRecord.booc) ||
|
||||
'ERROR: Body record not found!'}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuToTemplate = {
|
||||
'Main': <BodyDesignerMain />,
|
||||
'Body Records': <BodyDesignerBodyRecords />,
|
||||
'Stock Records': <BodyDesignerStockRecords />,
|
||||
'Specific Record': <BodyDesignerSpecificRecord />,
|
||||
'OOC Notes': <BodyDesignerOOCNotes />,
|
||||
};
|
||||
@@ -1,485 +0,0 @@
|
||||
import { round } from 'common/math';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { AnimatedNumber, Box, Button, Flex, Icon, LabeledList, ProgressBar, Section, Table, Tooltip } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
|
||||
const stats = [
|
||||
['good', 'Alive'],
|
||||
['average', 'Unconscious'],
|
||||
['bad', 'DEAD'],
|
||||
];
|
||||
|
||||
const abnormalities = [
|
||||
[
|
||||
'hasBorer',
|
||||
'bad',
|
||||
(occupant) =>
|
||||
'Large growth detected in frontal lobe,' +
|
||||
' possibly cancerous. Surgical removal is recommended.',
|
||||
],
|
||||
['hasVirus', 'bad', (occupant) => 'Viral pathogen detected in blood stream.'],
|
||||
['blind', 'average', (occupant) => 'Cataracts detected.'],
|
||||
[
|
||||
'colourblind',
|
||||
'average',
|
||||
(occupant) => 'Photoreceptor abnormalities detected.',
|
||||
],
|
||||
['nearsighted', 'average', (occupant) => 'Retinal misalignment detected.'],
|
||||
/* VOREStation Add */
|
||||
[
|
||||
'humanPrey',
|
||||
'average',
|
||||
(occupant) => {
|
||||
return 'Foreign Humanoid(s) detected: ' + occupant.humanPrey;
|
||||
},
|
||||
],
|
||||
[
|
||||
'livingPrey',
|
||||
'average',
|
||||
(occupant) => {
|
||||
return 'Foreign Creature(s) detected: ' + occupant.livingPrey;
|
||||
},
|
||||
],
|
||||
[
|
||||
'objectPrey',
|
||||
'average',
|
||||
(occupant) => {
|
||||
return 'Foreign Object(s) detected: ' + occupant.objectPrey;
|
||||
},
|
||||
],
|
||||
/* VOREStation Add End */
|
||||
];
|
||||
|
||||
const damages = [
|
||||
['Respiratory', 'oxyLoss'],
|
||||
['Brain', 'brainLoss'],
|
||||
['Toxin', 'toxLoss'],
|
||||
['Radiation', 'radLoss'],
|
||||
['Brute', 'bruteLoss'],
|
||||
['Genetic', 'cloneLoss'],
|
||||
['Burn', 'fireLoss'],
|
||||
['Paralysis', 'paralysis'],
|
||||
];
|
||||
|
||||
const damageRange = {
|
||||
average: [0.25, 0.5],
|
||||
bad: [0.5, Infinity],
|
||||
};
|
||||
|
||||
const mapTwoByTwo = (a, c) => {
|
||||
let result = [];
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
result.push(c(a[i], a[i + 1], i));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const reduceOrganStatus = (A) => {
|
||||
return A.length > 0
|
||||
? A.reduce((a, s) =>
|
||||
a === null ? (
|
||||
s
|
||||
) : (
|
||||
<Fragment>
|
||||
{a}
|
||||
{!!s && <Box>{s}</Box>}
|
||||
</Fragment>
|
||||
)
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
const germStatus = (i) => {
|
||||
if (i > 100) {
|
||||
if (i < 300) {
|
||||
return 'mild infection';
|
||||
}
|
||||
if (i < 400) {
|
||||
return 'mild infection+';
|
||||
}
|
||||
if (i < 500) {
|
||||
return 'mild infection++';
|
||||
}
|
||||
if (i < 700) {
|
||||
return 'acute infection';
|
||||
}
|
||||
if (i < 800) {
|
||||
return 'acute infection+';
|
||||
}
|
||||
if (i < 900) {
|
||||
return 'acute infection++';
|
||||
}
|
||||
if (i >= 900) {
|
||||
return 'septic';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
export const BodyScanner = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
const { occupied, occupant = {} } = data;
|
||||
const body = occupied ? (
|
||||
<BodyScannerMain occupant={occupant} />
|
||||
) : (
|
||||
<BodyScannerEmpty />
|
||||
);
|
||||
return (
|
||||
<Window width={690} height={600} resizable>
|
||||
<Window.Content scrollable className="Layout__content--flexColumn">
|
||||
{body}
|
||||
</Window.Content>
|
||||
</Window>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMain = (props) => {
|
||||
const { occupant } = props;
|
||||
return (
|
||||
<Box>
|
||||
<BodyScannerMainOccupant occupant={occupant} />
|
||||
<BodyScannerMainReagents occupant={occupant} />
|
||||
<BodyScannerMainAbnormalities occupant={occupant} />
|
||||
<BodyScannerMainDamage occupant={occupant} />
|
||||
<BodyScannerMainOrgansExternal organs={occupant.extOrgan} />
|
||||
<BodyScannerMainOrgansInternal organs={occupant.intOrgan} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainOccupant = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const { occupant } = data;
|
||||
return (
|
||||
<Section
|
||||
title="Occupant"
|
||||
buttons={
|
||||
<Fragment>
|
||||
<Button icon="user-slash" onClick={() => act('ejectify')}>
|
||||
Eject
|
||||
</Button>
|
||||
<Button icon="print" onClick={() => act('print_p')}>
|
||||
Print Report
|
||||
</Button>
|
||||
</Fragment>
|
||||
}>
|
||||
<LabeledList>
|
||||
<LabeledList.Item label="Name">{occupant.name}</LabeledList.Item>
|
||||
<LabeledList.Item label="Health">
|
||||
<ProgressBar
|
||||
min="0"
|
||||
max={occupant.maxHealth}
|
||||
value={occupant.health / occupant.maxHealth}
|
||||
ranges={{
|
||||
good: [0.5, Infinity],
|
||||
average: [0, 0.5],
|
||||
bad: [-Infinity, 0],
|
||||
}}
|
||||
/>
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Status" color={stats[occupant.stat][0]}>
|
||||
{stats[occupant.stat][1]}
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Temperature">
|
||||
<AnimatedNumber value={round(occupant.bodyTempC, 0)} />
|
||||
°C,
|
||||
<AnimatedNumber value={round(occupant.bodyTempF, 0)} />
|
||||
°F
|
||||
</LabeledList.Item>
|
||||
<LabeledList.Item label="Blood Volume">
|
||||
<AnimatedNumber value={round(occupant.blood.volume, 0)} />{' '}
|
||||
units (
|
||||
<AnimatedNumber value={round(occupant.blood.percent, 0)} />
|
||||
%)
|
||||
</LabeledList.Item>
|
||||
{/* VOREStation Add */}
|
||||
<LabeledList.Item label="Weight">
|
||||
{round(data.occupant.weight) +
|
||||
'lbs, ' +
|
||||
round(data.occupant.weight / 2.20463) +
|
||||
'kgs'}
|
||||
</LabeledList.Item>
|
||||
{/* VOREStation Add End */}
|
||||
</LabeledList>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainReagents = (props) => {
|
||||
const { occupant } = props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Section title="Blood Reagents">
|
||||
{occupant.reagents ? (
|
||||
<Table>
|
||||
<Table.Row header>
|
||||
<Table.Cell>Reagent</Table.Cell>
|
||||
<Table.Cell textAlign="right">Amount</Table.Cell>
|
||||
</Table.Row>
|
||||
{occupant.reagents.map((reagent) => (
|
||||
<Table.Row key={reagent.name}>
|
||||
<Table.Cell>{reagent.name}</Table.Cell>
|
||||
<Table.Cell textAlign="right">
|
||||
{reagent.amount} Units{' '}
|
||||
{reagent.overdose ? <Box color="bad">OVERDOSING</Box> : null}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<Box color="good">No Blood Reagents Detected</Box>
|
||||
)}
|
||||
</Section>
|
||||
<Section title="Stomach Reagents">
|
||||
{occupant.ingested ? (
|
||||
<Table>
|
||||
<Table.Row header>
|
||||
<Table.Cell>Reagent</Table.Cell>
|
||||
<Table.Cell textAlign="right">Amount</Table.Cell>
|
||||
</Table.Row>
|
||||
{occupant.ingested.map((reagent) => (
|
||||
<Table.Row key={reagent.name}>
|
||||
<Table.Cell>{reagent.name}</Table.Cell>
|
||||
<Table.Cell textAlign="right">
|
||||
{reagent.amount} Units{' '}
|
||||
{reagent.overdose ? <Box color="bad">OVERDOSING</Box> : null}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
) : (
|
||||
<Box color="good">No Stomach Reagents Detected</Box>
|
||||
)}
|
||||
</Section>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainAbnormalities = (props) => {
|
||||
const { occupant } = props;
|
||||
|
||||
let hasAbnormalities =
|
||||
occupant.hasBorer ||
|
||||
occupant.blind ||
|
||||
occupant.colourblind ||
|
||||
occupant.nearsighted ||
|
||||
occupant.hasVirus;
|
||||
|
||||
/* VOREStation Add */
|
||||
hasAbnormalities =
|
||||
hasAbnormalities ||
|
||||
occupant.humanPrey ||
|
||||
occupant.livingPrey ||
|
||||
occupant.objectPrey;
|
||||
/* VOREStation Add End */
|
||||
|
||||
if (!hasAbnormalities) {
|
||||
return (
|
||||
<Section title="Abnormalities">
|
||||
<Box color="label">No abnormalities found.</Box>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title="Abnormalities">
|
||||
{abnormalities.map((a, i) => {
|
||||
if (occupant[a[0]]) {
|
||||
return (
|
||||
<Box color={a[1]} bold={a[1] === 'bad'}>
|
||||
{a[2](occupant)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainDamage = (props) => {
|
||||
const { occupant } = props;
|
||||
return (
|
||||
<Section title="Damage">
|
||||
<Table>
|
||||
{mapTwoByTwo(damages, (d1, d2, i) => (
|
||||
<Fragment>
|
||||
<Table.Row color="label">
|
||||
<Table.Cell>{d1[0]}:</Table.Cell>
|
||||
<Table.Cell>{!!d2 && d2[0] + ':'}</Table.Cell>
|
||||
</Table.Row>
|
||||
<Table.Row>
|
||||
<Table.Cell>
|
||||
<BodyScannerMainDamageBar
|
||||
value={occupant[d1[1]]}
|
||||
marginBottom={i < damages.length - 2}
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{!!d2 && <BodyScannerMainDamageBar value={occupant[d2[1]]} />}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainDamageBar = (props) => {
|
||||
return (
|
||||
<ProgressBar
|
||||
min="0"
|
||||
max="100"
|
||||
value={props.value / 100}
|
||||
mt="0.5rem"
|
||||
mb={!!props.marginBottom && '0.5rem'}
|
||||
ranges={damageRange}>
|
||||
{round(props.value, 0)}
|
||||
</ProgressBar>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainOrgansExternal = (props) => {
|
||||
if (props.organs.length === 0) {
|
||||
return (
|
||||
<Section title="External Organs">
|
||||
<Box color="label">N/A</Box>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title="External Organs">
|
||||
<Table>
|
||||
<Table.Row header>
|
||||
<Table.Cell>Name</Table.Cell>
|
||||
<Table.Cell textAlign="center">Damage</Table.Cell>
|
||||
<Table.Cell textAlign="right">Injuries</Table.Cell>
|
||||
</Table.Row>
|
||||
{props.organs.map((o, i) => (
|
||||
<Table.Row key={i} textTransform="capitalize">
|
||||
<Table.Cell width="33%">{o.name}</Table.Cell>
|
||||
<Table.Cell textAlign="center" q>
|
||||
<ProgressBar
|
||||
min="0"
|
||||
max={o.maxHealth}
|
||||
mt={i > 0 && '0.5rem'}
|
||||
value={o.totalLoss / 100}
|
||||
ranges={damageRange}>
|
||||
<Box float="left" inline>
|
||||
{!!o.bruteLoss && (
|
||||
<Box inline position="relative">
|
||||
<Icon name="bone" />
|
||||
{round(o.bruteLoss, 0)}
|
||||
<Tooltip position="top" content="Brute damage" />
|
||||
</Box>
|
||||
)}
|
||||
{!!o.fireLoss && (
|
||||
<Box inline position="relative">
|
||||
<Icon name="fire" />
|
||||
{round(o.fireLoss, 0)}
|
||||
<Tooltip position="top" content="Burn damage" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box inline>{round(o.totalLoss, 0)}</Box>
|
||||
</ProgressBar>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign="right" width="33%">
|
||||
<Box color="average" inline>
|
||||
{reduceOrganStatus([
|
||||
o.internalBleeding && 'Internal bleeding',
|
||||
!!o.status.bleeding && 'External bleeding',
|
||||
o.lungRuptured && 'Ruptured lung',
|
||||
o.destroyed && 'Destroyed',
|
||||
!!o.status.broken && o.status.broken,
|
||||
germStatus(o.germ_level),
|
||||
!!o.open && 'Open incision',
|
||||
])}
|
||||
</Box>
|
||||
<Box inline>
|
||||
{reduceOrganStatus([
|
||||
!!o.status.splinted && 'Splinted',
|
||||
!!o.status.robotic && 'Robotic',
|
||||
!!o.status.dead && <Box color="bad">DEAD</Box>,
|
||||
])}
|
||||
{reduceOrganStatus(
|
||||
o.implants.map((s) => (s.known ? s.name : 'Unknown object'))
|
||||
)}
|
||||
</Box>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerMainOrgansInternal = (props) => {
|
||||
if (props.organs.length === 0) {
|
||||
return (
|
||||
<Section title="Internal Organs">
|
||||
<Box color="label">N/A</Box>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title="Internal Organs">
|
||||
<Table>
|
||||
<Table.Row header>
|
||||
<Table.Cell>Name</Table.Cell>
|
||||
<Table.Cell textAlign="center">Damage</Table.Cell>
|
||||
<Table.Cell textAlign="right">Injuries</Table.Cell>
|
||||
</Table.Row>
|
||||
{props.organs.map((o, i) => (
|
||||
<Table.Row key={i} textTransform="capitalize">
|
||||
<Table.Cell width="33%">{o.name}</Table.Cell>
|
||||
<Table.Cell textAlign="center">
|
||||
<ProgressBar
|
||||
min="0"
|
||||
max={o.maxHealth}
|
||||
value={o.damage / 100}
|
||||
mt={i > 0 && '0.5rem'}
|
||||
ranges={damageRange}>
|
||||
{round(o.damage, 0)}
|
||||
</ProgressBar>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign="right" width="33%">
|
||||
<Box color="average" inline>
|
||||
{reduceOrganStatus([
|
||||
germStatus(o.germ_level),
|
||||
!!o.inflamed && 'Appendicitis detected.',
|
||||
])}
|
||||
</Box>
|
||||
<Box inline>
|
||||
{reduceOrganStatus([
|
||||
o.robotic === 1 && 'Robotic',
|
||||
o.robotic === 2 && 'Assisted',
|
||||
!!o.dead && <Box color="bad">DEAD</Box>,
|
||||
])}
|
||||
</Box>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
const BodyScannerEmpty = () => {
|
||||
return (
|
||||
<Section textAlign="center" flexGrow="1">
|
||||
<Flex height="100%">
|
||||
<Flex.Item grow="1" align="center" color="label">
|
||||
<Icon name="user-slash" mb="0.5rem" size="5" />
|
||||
<br />
|
||||
No occupant detected.
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user