mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-29 16:18:01 +01:00
Shared tgui states, global Byond API object
This commit is contained in:
@@ -107,6 +107,14 @@
|
||||
/datum/proc/ui_host(mob/user)
|
||||
return src // Default src.
|
||||
|
||||
/**
|
||||
* global
|
||||
*
|
||||
* Associative list of JSON-encoded shared states that were set by
|
||||
* tgui clients.
|
||||
*/
|
||||
/datum/var/list/tgui_shared_states
|
||||
|
||||
/**
|
||||
* global
|
||||
*
|
||||
|
||||
+16
-13
@@ -42,8 +42,6 @@
|
||||
var/datum/tgui/master_ui
|
||||
/// Children of this UI.
|
||||
var/list/datum/tgui/children = list()
|
||||
// TODO: Remove in favor of useGlobal
|
||||
var/ui_screen = "home"
|
||||
|
||||
/**
|
||||
* public
|
||||
@@ -197,7 +195,6 @@
|
||||
json_data["config"] = list(
|
||||
"title" = title,
|
||||
"status" = status,
|
||||
"screen" = ui_screen,
|
||||
"interface" = interface,
|
||||
"fancy" = user.client.prefs.tgui_fancy,
|
||||
"locked" = user.client.prefs.tgui_lock,
|
||||
@@ -213,6 +210,10 @@
|
||||
if(!isnull(static_data))
|
||||
json_data["static_data"] = static_data
|
||||
|
||||
// Send shared states
|
||||
if(src_object.tgui_shared_states)
|
||||
json_data["shared"] = src_object.tgui_shared_states
|
||||
|
||||
// Generate the JSON.
|
||||
var/json = json_encode(json_data)
|
||||
// Strip #255/improper.
|
||||
@@ -238,9 +239,12 @@
|
||||
if("tgui:initialize")
|
||||
user << output(_initial_update, "[window_id].browser:update")
|
||||
initialized = TRUE
|
||||
if("tgui:view")
|
||||
if(params["screen"])
|
||||
ui_screen = params["screen"]
|
||||
if("tgui:setSharedState")
|
||||
var/key = params["key"]
|
||||
var/value = params["value"]
|
||||
if(!src_object.tgui_shared_states)
|
||||
src_object.tgui_shared_states = list()
|
||||
src_object.tgui_shared_states[key] = value
|
||||
SStgui.update_uis(src_object)
|
||||
if("tgui:log")
|
||||
// Force window to show frills on fatal errors
|
||||
@@ -249,14 +253,13 @@
|
||||
log_message(params["log"])
|
||||
if("tgui:link")
|
||||
user << link(params["url"])
|
||||
if("tgui:fancy")
|
||||
user.client.prefs.tgui_fancy = TRUE
|
||||
if("tgui:nofrills")
|
||||
user.client.prefs.tgui_fancy = FALSE
|
||||
else
|
||||
update_status(push = FALSE) // Update the window state.
|
||||
if(src_object.ui_act(action, params, src, state)) // Call ui_act() on the src_object.
|
||||
SStgui.update_uis(src_object) // Update if the object requested it.
|
||||
// Update the window state.
|
||||
update_status(push = FALSE)
|
||||
// Call ui_act() on the src_object.
|
||||
if(src_object.ui_act(action, params, src, state))
|
||||
// Update if the object requested it.
|
||||
SStgui.update_uis(src_object)
|
||||
|
||||
/**
|
||||
* private
|
||||
|
||||
+105
-18
@@ -1,6 +1,3 @@
|
||||
import { act } from './byond';
|
||||
import { UI_DISABLED, UI_INTERACTIVE } from './constants';
|
||||
|
||||
/**
|
||||
* This file provides a clear separation layer between backend updates
|
||||
* and what state our React app sees.
|
||||
@@ -10,21 +7,23 @@ import { UI_DISABLED, UI_INTERACTIVE } from './constants';
|
||||
* the response with already existing state.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a backend update action.
|
||||
*/
|
||||
import { UI_DISABLED, UI_INTERACTIVE } from './constants';
|
||||
import { callByond } from './byond';
|
||||
|
||||
export const backendUpdate = state => ({
|
||||
type: 'backendUpdate',
|
||||
type: 'backend/update',
|
||||
payload: state,
|
||||
});
|
||||
|
||||
/**
|
||||
* Precisely defines state changes.
|
||||
*/
|
||||
export const backendSetSharedState = (key, nextState) => ({
|
||||
type: 'backend/setSharedState',
|
||||
payload: { key, nextState },
|
||||
});
|
||||
|
||||
export const backendReducer = (state, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'backendUpdate') {
|
||||
if (type === 'backend/update') {
|
||||
// Merge config
|
||||
const config = {
|
||||
...state.config,
|
||||
@@ -36,6 +35,19 @@ export const backendReducer = (state, action) => {
|
||||
...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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Calculate our own fields
|
||||
const visible = config.status !== UI_DISABLED;
|
||||
const interactive = config.status === UI_INTERACTIVE;
|
||||
@@ -44,11 +56,23 @@ export const backendReducer = (state, action) => {
|
||||
...state,
|
||||
config,
|
||||
data,
|
||||
shared,
|
||||
visible,
|
||||
interactive,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/setSharedState') {
|
||||
const { key, nextState } = payload;
|
||||
return {
|
||||
...state,
|
||||
shared: {
|
||||
...state.shared,
|
||||
[key]: nextState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
@@ -84,15 +108,78 @@ export const backendReducer = (state, action) => {
|
||||
* }}
|
||||
*/
|
||||
export const useBackend = context => {
|
||||
// TODO: Dispatch "act" calls as Redux actions
|
||||
const { store } = context;
|
||||
const state = store.getState();
|
||||
const ref = state.config.ref;
|
||||
const boundAct = (action, params = {}) => {
|
||||
act(ref, action, params);
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
act: boundAct,
|
||||
const act = (action, params = {}) => {
|
||||
callByond('', {
|
||||
src: ref,
|
||||
action,
|
||||
...params,
|
||||
});
|
||||
};
|
||||
return { ...state, act };
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 {any} context React context.
|
||||
* @param {string} key Key which uniquely identifies this state in Redux store.
|
||||
* @param {any} initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useLocalState = (context, key, initialState) => {
|
||||
const { store } = context;
|
||||
const state = store.getState();
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
store.dispatch(backendSetSharedState(key, 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 {any} context React context.
|
||||
* @param {string} key Key which uniquely identifies this state in Redux store.
|
||||
* @param {any} initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useSharedState = (context, key, initialState) => {
|
||||
const { store } = context;
|
||||
const state = store.getState();
|
||||
const ref = state.config.ref;
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
callByond('', {
|
||||
src: ref,
|
||||
action: 'tgui:setSharedState',
|
||||
key,
|
||||
value: JSON.stringify(nextState) || '',
|
||||
});
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
+15
-39
@@ -1,12 +1,12 @@
|
||||
import { buildQueryString } from 'common/string';
|
||||
// Reference a global Byond object
|
||||
const { Byond } = window;
|
||||
|
||||
/**
|
||||
* Version of Trident engine used in Internet Explorer.
|
||||
* An integer number or `null` if this is not a trident engine.
|
||||
*
|
||||
* - IE 8 - Trident 4.0
|
||||
* - IE 11 - Trident 7.0
|
||||
*
|
||||
* @return An integer number or 'null' if this is not a trident engine.
|
||||
*/
|
||||
const tridentVersion = (() => {
|
||||
const groups = navigator.userAgent.match(/Trident\/(\d+).+?;/i);
|
||||
@@ -27,41 +27,23 @@ export const IS_IE8 = tridentVersion !== null
|
||||
&& tridentVersion <= 4;
|
||||
|
||||
/**
|
||||
* True if browser is a BYOND browser.
|
||||
* Makes a BYOND call.
|
||||
*
|
||||
* If path is empty, this will trigger a Topic call.
|
||||
* You can reference a specific object by setting the "src" parameter.
|
||||
*
|
||||
* See: https://secure.byond.com/docs/ref/skinparams.html
|
||||
*/
|
||||
// We're currently just checking, whether we're running on a localhost
|
||||
// with a path similar to a BYOND cache. I couldn't find a better/faster
|
||||
// non-invasive method of doing this.
|
||||
export const IS_BYOND = tridentVersion !== null
|
||||
&& location.hostname === '127.0.0.1'
|
||||
&& location.pathname.startsWith('/tmp');
|
||||
|
||||
/**
|
||||
* Helper to generate a BYOND href given 'params' as an object
|
||||
* (with an optional 'url' for eg winset).
|
||||
*/
|
||||
const createByondUrl = (path, params = {}) => {
|
||||
return 'byond://' + path + '?' + buildQueryString(params);
|
||||
};
|
||||
|
||||
export const callByond = (path, params = {}) => {
|
||||
// Abort BYOND calls when we're running in a normal browser
|
||||
if (!IS_BYOND) {
|
||||
return;
|
||||
}
|
||||
location.href = createByondUrl(path, params);
|
||||
Byond.call(path, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* A high-level abstraction of BYJAX. Makes a call to BYOND and returns
|
||||
* A high-level abstraction of BYOND calls. Makes a BYOND call and returns
|
||||
* a promise, which (if endpoint has a callback parameter) resolves
|
||||
* with the return value of that call.
|
||||
*/
|
||||
export const callByondAsync = (path, params = {}) => {
|
||||
// Abort BYOND calls when we're running in a normal browser
|
||||
if (!IS_BYOND) {
|
||||
return new Promise(() => {});
|
||||
}
|
||||
// Create a callback array if it doesn't exist yet
|
||||
window.__callbacks__ = window.__callbacks__ || [];
|
||||
// Create a Promise and push its resolve function into callback array
|
||||
@@ -71,7 +53,7 @@ export const callByondAsync = (path, params = {}) => {
|
||||
window.__callbacks__.push(resolve);
|
||||
});
|
||||
// Call BYOND client
|
||||
callByond(path, {
|
||||
Byond.call(path, {
|
||||
...params,
|
||||
callback: `__callbacks__[${callbackIndex}]`,
|
||||
});
|
||||
@@ -79,18 +61,12 @@ export const callByondAsync = (path, params = {}) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Literally types a command on the client.
|
||||
* Runs a BYOND skin command
|
||||
*
|
||||
* See: https://secure.byond.com/docs/ref/skinparams.html
|
||||
*/
|
||||
export const runCommand = command => callByond('winset', { command });
|
||||
|
||||
/**
|
||||
* Helper to make a BYOND ui_act() call on the UI 'src' given an 'action'
|
||||
* and optional 'params'.
|
||||
*/
|
||||
export const act = (src, action, params = {}) => {
|
||||
return callByond('', { src, action, ...params });
|
||||
};
|
||||
|
||||
/**
|
||||
* Calls 'winget' on a BYOND skin element, retrieving value by the 'key'.
|
||||
*/
|
||||
|
||||
@@ -11,15 +11,12 @@ import { setupHotReloading } from 'tgui-dev-server/link/client';
|
||||
import { backendUpdate } from './backend';
|
||||
import { IS_IE8 } from './byond';
|
||||
import { setupDrag } from './drag';
|
||||
import { createLogger } from './logging';
|
||||
import { logger } from './logging';
|
||||
import { createStore, StoreProvider } from './store';
|
||||
|
||||
const enteredBundleAt = Date.now();
|
||||
|
||||
const logger = createLogger();
|
||||
const store = createStore();
|
||||
let reactRoot;
|
||||
|
||||
let initialRender = true;
|
||||
|
||||
const renderLayout = () => {
|
||||
@@ -117,7 +114,7 @@ const setupApp = () => {
|
||||
// Subscribe for bankend updates
|
||||
window.update = stateJson => {
|
||||
// NOTE: stateJson can be an object only if called manually from console.
|
||||
// This is useful for debugging tgui in proper browsers, like Chrome.
|
||||
// This is useful for debugging tgui in external browsers, like Chrome.
|
||||
const state = typeof stateJson === 'string'
|
||||
? parseStateJson(stateJson)
|
||||
: stateJson;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toFixed } from 'common/math';
|
||||
import { decodeHtmlEntities } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, LabeledList, NumberInput, Section } from '../components';
|
||||
import { getGasLabel } from '../constants';
|
||||
import { Window } from '../layouts';
|
||||
@@ -114,19 +114,17 @@ const AIR_ALARM_ROUTES = {
|
||||
};
|
||||
|
||||
const AirAlarmControl = (props, context) => {
|
||||
const { act, config } = useBackend(context);
|
||||
const route = AIR_ALARM_ROUTES[config.screen] || AIR_ALARM_ROUTES.home;
|
||||
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={config.screen !== 'home' && (
|
||||
buttons={screen && (
|
||||
<Button
|
||||
icon="arrow-left"
|
||||
content="Back"
|
||||
onClick={() => act('tgui:view', {
|
||||
screen: 'home',
|
||||
})} />
|
||||
onClick={() => setScreen()} />
|
||||
)}>
|
||||
<Component />
|
||||
</Section>
|
||||
@@ -139,6 +137,7 @@ const AirAlarmControl = (props, context) => {
|
||||
|
||||
const AirAlarmControlHome = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [screen, setScreen] = useLocalState(context, 'screen');
|
||||
const {
|
||||
mode,
|
||||
atmos_alarm,
|
||||
@@ -166,30 +165,22 @@ const AirAlarmControlHome = (props, context) => {
|
||||
<Button
|
||||
icon="sign-out-alt"
|
||||
content="Vent Controls"
|
||||
onClick={() => act('tgui:view', {
|
||||
screen: 'vents',
|
||||
})} />
|
||||
onClick={() => setScreen('vents')} />
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="filter"
|
||||
content="Scrubber Controls"
|
||||
onClick={() => act('tgui:view', {
|
||||
screen: 'scrubbers',
|
||||
})} />
|
||||
onClick={() => setScreen('scrubbers')} />
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="cog"
|
||||
content="Operating Mode"
|
||||
onClick={() => act('tgui:view', {
|
||||
screen: 'modes',
|
||||
})} />
|
||||
onClick={() => setScreen('modes')} />
|
||||
<Box mt={1} />
|
||||
<Button
|
||||
icon="chart-bar"
|
||||
content="Alarm Thresholds"
|
||||
onClick={() => act('tgui:view', {
|
||||
screen: 'thresholds',
|
||||
})} />
|
||||
onClick={() => setScreen('thresholds')} />
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,10 +3,9 @@ import { flow } from 'common/fp';
|
||||
import { classes } from 'common/react';
|
||||
import { createSearch } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, ByondUi, Input, Section } from '../components';
|
||||
import { refocusLayout, Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
/**
|
||||
* Returns previous and next camera names relative to the currently
|
||||
@@ -95,7 +94,7 @@ export const CameraConsoleContent = (props, context) => {
|
||||
const [
|
||||
searchText,
|
||||
setSearchText,
|
||||
] = useGlobal(context, 'searchText', '');
|
||||
] = useLocalState(context, 'searchText', '');
|
||||
const { activeCamera } = data;
|
||||
const cameras = selectCameras(data.cameras, searchText);
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { toArray } from 'common/collections';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useSharedState } from '../backend';
|
||||
import { AnimatedNumber, Box, Button, Divider, Flex, LabeledList, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
export const Cargo = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [tab, setTab] = useGlobal(context, 'tab', 'catalog');
|
||||
const [tab, setTab] = useSharedState(context, 'tab', 'catalog');
|
||||
const {
|
||||
requestonly,
|
||||
} = data;
|
||||
@@ -125,7 +124,7 @@ export const CargoCatalog = (props, context) => {
|
||||
const [
|
||||
activeSupplyName,
|
||||
setActiveSupplyName,
|
||||
] = useGlobal(context, 'supply', supplies[0]?.name);
|
||||
] = useSharedState(context, 'supply', supplies[0]?.name);
|
||||
const activeSupply = supplies.find(supply => {
|
||||
return supply.name === activeSupplyName;
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AnimatedNumber, Section, LabeledList, Button, Box } from "../components";
|
||||
import { Fragment } from "inferno";
|
||||
import { InterfaceLockNoticeBox } from "./common/InterfaceLockNoticeBox";
|
||||
import { CargoCatalog } from "./Cargo";
|
||||
import { useBackend } from "../backend";
|
||||
import { Window } from "../layouts";
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { AnimatedNumber, Box, Button, LabeledList, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { CargoCatalog } from './Cargo';
|
||||
import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox';
|
||||
|
||||
export const CargoExpress = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Button, Flex, Input, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
export const ChemFilterPane = (props, context) => {
|
||||
const { act } = useBackend(context);
|
||||
@@ -48,8 +47,8 @@ export const ChemFilter = (props, context) => {
|
||||
left = [],
|
||||
right = [],
|
||||
} = data;
|
||||
const [leftName, setLeftName] = useGlobal(context, 'leftName', '');
|
||||
const [rightName, setRightName] = useGlobal(context, 'rightName', '');
|
||||
const [leftName, setLeftName] = useLocalState(context, 'leftName', '');
|
||||
const [rightName, setRightName] = useLocalState(context, 'rightName', '');
|
||||
return (
|
||||
<Window resizable>
|
||||
<Window.Content scrollable>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useSharedState } from '../backend';
|
||||
import { AnimatedNumber, Box, Button, ColorBox, LabeledList, NumberInput, Section, Table } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
export const ChemMaster = (props, context) => {
|
||||
const { data } = useBackend(context);
|
||||
@@ -221,19 +220,19 @@ const PackagingControls = (props, context) => {
|
||||
const [
|
||||
pillAmount,
|
||||
setPillAmount,
|
||||
] = useGlobal(context, 'pillAmount', 1);
|
||||
] = useSharedState(context, 'pillAmount', 1);
|
||||
const [
|
||||
patchAmount,
|
||||
setPatchAmount,
|
||||
] = useGlobal(context, 'patchAmount', 1);
|
||||
] = useSharedState(context, 'patchAmount', 1);
|
||||
const [
|
||||
bottleAmount,
|
||||
setBottleAmount,
|
||||
] = useGlobal(context, 'bottleAmount', 1);
|
||||
] = useSharedState(context, 'bottleAmount', 1);
|
||||
const [
|
||||
packAmount,
|
||||
setPackAmount,
|
||||
] = useGlobal(context, 'packAmount', 1);
|
||||
] = useSharedState(context, 'packAmount', 1);
|
||||
const {
|
||||
condi,
|
||||
chosenPillStyle,
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { map } from 'common/collections';
|
||||
import { classes } from 'common/react';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, Input, LabeledList, NumberInput, Section } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
export const ChemReactionChamber = (props, context) => {
|
||||
const { act, data } = useBackend(context);
|
||||
const [
|
||||
reagentName,
|
||||
setReagentName,
|
||||
] = useGlobal(context, 'reagentName', '');
|
||||
] = useLocalState(context, 'reagentName', '');
|
||||
const [
|
||||
reagentQuantity,
|
||||
setReagentQuantity,
|
||||
] = useGlobal(context, 'reagentQuantity', 1);
|
||||
] = useLocalState(context, 'reagentQuantity', 1);
|
||||
const emptying = data.emptying;
|
||||
const reagents = data.reagents || [];
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { BlockQuote, Box, Button, ByondUi, Collapsible, Icon, Input, Knob, LabeledList, NumberInput, ProgressBar, Section, Slider, Tabs, Tooltip } from '../components';
|
||||
import { DraggableControl } from '../components/DraggableControl';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
const COLORS_ARBITRARY = [
|
||||
'red',
|
||||
@@ -72,7 +71,7 @@ const PAGES = [
|
||||
];
|
||||
|
||||
export const KitchenSink = (props, context) => {
|
||||
const [theme] = useGlobal(context, 'kitchenSinkTheme');
|
||||
const [theme] = useLocalState(context, 'kitchenSinkTheme');
|
||||
return (
|
||||
<Window
|
||||
theme={theme}
|
||||
@@ -470,7 +469,7 @@ const KitchenSinkByondUi = (props, context) => {
|
||||
};
|
||||
|
||||
const KitchenSinkThemes = (props, context) => {
|
||||
const [theme, setTheme] = useGlobal(context, 'kitchenSinkTheme');
|
||||
const [theme, setTheme] = useLocalState(context, 'kitchenSinkTheme');
|
||||
return (
|
||||
<Box>
|
||||
<LabeledList>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createSearch, decodeHtmlEntities } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, Input, Section, Table, Tabs } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
const MAX_SEARCH_RESULTS = 25;
|
||||
|
||||
@@ -17,11 +16,11 @@ export const MalfunctionModulePicker = (props, context) => {
|
||||
const [
|
||||
hoveredItem,
|
||||
setHoveredItem,
|
||||
] = useGlobal(context, 'hoveredItem', {});
|
||||
] = useLocalState(context, 'hoveredItem', {});
|
||||
const [
|
||||
searchText,
|
||||
setSearchText,
|
||||
] = useGlobal(context, 'searchText', '');
|
||||
] = useLocalState(context, 'searchText', '');
|
||||
const testSearch = createSearch(searchText, item => {
|
||||
return item.name + item.desc;
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createSearch, decodeHtmlEntities } from 'common/string';
|
||||
import { Fragment } from 'inferno';
|
||||
import { useBackend } from '../backend';
|
||||
import { useBackend, useLocalState } from '../backend';
|
||||
import { Box, Button, Input, Section, Table, Tabs } from '../components';
|
||||
import { Window } from '../layouts';
|
||||
import { useGlobal } from '../store';
|
||||
|
||||
const MAX_SEARCH_RESULTS = 25;
|
||||
|
||||
@@ -18,11 +17,11 @@ export const Uplink = (props, context) => {
|
||||
const [
|
||||
hoveredItem,
|
||||
setHoveredItem,
|
||||
] = useGlobal(context, 'hoveredItem', {});
|
||||
] = useLocalState(context, 'hoveredItem', {});
|
||||
const [
|
||||
searchText,
|
||||
setSearchText,
|
||||
] = useGlobal(context, 'searchText', '');
|
||||
] = useLocalState(context, 'searchText', '');
|
||||
const testSearch = createSearch(searchText, item => {
|
||||
return item.name + item.desc;
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sendLogEntry } from 'tgui-dev-server/link/client';
|
||||
import { act } from './byond';
|
||||
import { callByond } from './byond';
|
||||
|
||||
const LEVEL_DEBUG = 0;
|
||||
const LEVEL_LOG = 1;
|
||||
@@ -27,7 +27,9 @@ const log = (level, ns, ...args) => {
|
||||
.filter(value => value)
|
||||
.join(' ')
|
||||
+ '\nUser Agent: ' + navigator.userAgent;
|
||||
act(window.__ref__, 'tgui:log', {
|
||||
callByond({
|
||||
src: window.__ref__,
|
||||
action: 'tgui:log',
|
||||
log: logEntry,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,6 +19,53 @@ window.__ref__ = document
|
||||
if (window.__ref__ === '[' + 'tgui:ref' + ']') {
|
||||
window.__ref__ = null;
|
||||
}
|
||||
// BYOND API object
|
||||
window.Byond = (function () {
|
||||
var Byond = {};
|
||||
// Utility functions
|
||||
var hasOwn = Object.prototype.hasOwnProperty;
|
||||
// Basic checks to detect whether this page runs in BYOND
|
||||
var isByond = !!navigator.userAgent.match(/Trident\/(\d+).+?;/i)
|
||||
&& location.hostname === '127.0.0.1'
|
||||
&& location.pathname.indexOf('/tmp') === 0;
|
||||
// Makes a BYOND call.
|
||||
// See: https://secure.byond.com/docs/ref/skinparams.html
|
||||
Byond.call = function (path, params) {
|
||||
// Not running in BYOND, abort.
|
||||
if (!isByond) {
|
||||
return;
|
||||
}
|
||||
// Build the URL
|
||||
var url = (path || '') + '?';
|
||||
var i = 0;
|
||||
if (params) {
|
||||
for (var key in params) {
|
||||
if (hasOwn.call(params, key)) {
|
||||
if (i++ > 0) {
|
||||
url += '&';
|
||||
}
|
||||
var value = params[key];
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
}
|
||||
url += encodeURIComponent(key)
|
||||
+ '=' + encodeURIComponent(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Perform a standard call via location.href
|
||||
if (url.length < 2048) {
|
||||
location.href = 'byond://' + url;
|
||||
return;
|
||||
}
|
||||
// Send an HTTP request to DreamSeeker's HTTP server.
|
||||
// Allows sending much bigger payloads.
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url);
|
||||
xhr.send();
|
||||
};
|
||||
return Byond;
|
||||
})();
|
||||
// Global error handling
|
||||
window.onerror = function (msg, url, line, col, error) {
|
||||
// Proper stacktrace
|
||||
@@ -44,10 +91,13 @@ window.onerror = function (msg, url, line, col, error) {
|
||||
errorStack.textContent = stack;
|
||||
}
|
||||
}
|
||||
// Send to the backend
|
||||
location.href = 'byond://?src=' + window.__ref__
|
||||
+ '&action=tgui:log&fatal=1'
|
||||
+ '&log=' + encodeURIComponent(stack);
|
||||
// Send logs to the server
|
||||
Byond.call('', {
|
||||
src: window.__ref__,
|
||||
action: 'tgui:log',
|
||||
fatal: '1',
|
||||
log: stack,
|
||||
});
|
||||
// Short-circuit further updates
|
||||
window.__updateQueue__ = [];
|
||||
window.update = function () {};
|
||||
@@ -59,8 +109,10 @@ window.__updateQueue__ = [];
|
||||
window.update = function (stateJson) {
|
||||
window.__updateQueue__.push(stateJson);
|
||||
};
|
||||
location.href = 'byond://?src=' + window.__ref__
|
||||
+ '&action=tgui:initialize';
|
||||
Byond.call('', {
|
||||
src: window.__ref__,
|
||||
action: 'tgui:initialize',
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Styles -->
|
||||
|
||||
@@ -11,7 +11,6 @@ export const createStore = () => {
|
||||
// Global state reducers
|
||||
backendReducer,
|
||||
hotKeyReducer,
|
||||
globalStateReducer,
|
||||
]);
|
||||
const middleware = [
|
||||
// loggingMiddleware,
|
||||
@@ -34,47 +33,3 @@ export class StoreProvider extends Component {
|
||||
export const useDispatch = context => {
|
||||
return context.store.dispatch;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allocates a global variable on Redux store.
|
||||
*
|
||||
* Great when you want to store some UI state globally, without having to
|
||||
* modify DM object you're working with to have that var and export it via
|
||||
* ui_data.
|
||||
*
|
||||
* @param {any} context React context.
|
||||
* @param {string} key Key which uniquely identifies this state in Redux store.
|
||||
* @param {any} initialState Initializes your global variable with this value.
|
||||
*/
|
||||
export const useGlobal = (context, key, initialState) => {
|
||||
const { store } = context;
|
||||
const globalObj = store.getState().global ?? {};
|
||||
const state = (key in globalObj)
|
||||
? globalObj[key]
|
||||
: initialState;
|
||||
const setState = nextState => {
|
||||
store.dispatch({
|
||||
type: 'setGlobal',
|
||||
payload: { key, nextState },
|
||||
});
|
||||
};
|
||||
return [state, setState];
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer, which handles actions coming from useGlobal.
|
||||
*/
|
||||
const globalStateReducer = (state, action) => {
|
||||
const { type, payload } = action;
|
||||
if (type === 'setGlobal') {
|
||||
const { key, nextState } = payload;
|
||||
return {
|
||||
...state,
|
||||
global: {
|
||||
...state.global,
|
||||
[key]: nextState,
|
||||
},
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user