mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-10 10:12:45 +00:00
[MIRROR] Overhaul vending machine UI from Bubber (#11435)
Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com> Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b74d08609a
commit
e16c9ce7ba
@@ -63,6 +63,12 @@
|
|||||||
#define EMOTE_SOUND_VOICE_FREQ "Voice Frequency"
|
#define EMOTE_SOUND_VOICE_FREQ "Voice Frequency"
|
||||||
#define EMOTE_SOUND_VOICE_LIST "Voice Sound"
|
#define EMOTE_SOUND_VOICE_LIST "Voice Sound"
|
||||||
|
|
||||||
|
// Choose grid or list TGUI layouts for UI's, when possible.
|
||||||
|
/// Force grid layout, even if default is a list.
|
||||||
|
#define TGUI_LAYOUT_GRID "grid"
|
||||||
|
/// Force list layout, even if default is a grid.
|
||||||
|
#define TGUI_LAYOUT_LIST "list"
|
||||||
|
|
||||||
#define WRITE_PREF_NORMAL 1
|
#define WRITE_PREF_NORMAL 1
|
||||||
#define WRITE_PREF_INSTANT 2
|
#define WRITE_PREF_INSTANT 2
|
||||||
#define WRITE_PREF_MANUAL 3
|
#define WRITE_PREF_MANUAL 3
|
||||||
|
|||||||
@@ -51,6 +51,24 @@
|
|||||||
default_value = FALSE
|
default_value = FALSE
|
||||||
savefile_identifier = PREFERENCE_PLAYER
|
savefile_identifier = PREFERENCE_PLAYER
|
||||||
|
|
||||||
|
/datum/preference/choiced/tgui_layout
|
||||||
|
savefile_key = "tgui_layout"
|
||||||
|
savefile_identifier = PREFERENCE_PLAYER
|
||||||
|
|
||||||
|
/datum/preference/choiced/tgui_layout/init_possible_values()
|
||||||
|
return list(
|
||||||
|
TGUI_LAYOUT_GRID,
|
||||||
|
TGUI_LAYOUT_LIST,
|
||||||
|
)
|
||||||
|
|
||||||
|
/datum/preference/choiced/tgui_layout/create_default_value()
|
||||||
|
return TGUI_LAYOUT_GRID
|
||||||
|
|
||||||
|
/datum/preference/choiced/tgui_layout/apply_to_client(client/client, value)
|
||||||
|
for (var/datum/tgui/tgui as anything in client.mob?.tgui_open_uis)
|
||||||
|
// Force it to reload either way
|
||||||
|
tgui.update_tgui_static_data(client.mob)
|
||||||
|
|
||||||
/datum/preference/toggle/tgui_say
|
/datum/preference/toggle/tgui_say
|
||||||
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
|
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
|
||||||
savefile_key = "TGUI_SAY"
|
savefile_key = "TGUI_SAY"
|
||||||
|
|||||||
@@ -94,6 +94,11 @@
|
|||||||
// If UI is not interactive or usr calling Topic is not the UI user, bail.
|
// If UI is not interactive or usr calling Topic is not the UI user, bail.
|
||||||
if(!ui || ui.status != STATUS_INTERACTIVE)
|
if(!ui || ui.status != STATUS_INTERACTIVE)
|
||||||
return TRUE
|
return TRUE
|
||||||
|
if(action == "change_ui_state")
|
||||||
|
var/mob/living/user = ui.user
|
||||||
|
//write_preferences will make sure it's valid for href exploits.
|
||||||
|
user.client.prefs.write_preference(GLOB.preference_entries[/datum/preference/choiced/tgui_layout], params["new_state"])
|
||||||
|
to_world("We have the curent [user.client.prefs.read_preference(/datum/preference/choiced/tgui_layout)]")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* public
|
* public
|
||||||
|
|||||||
@@ -281,7 +281,7 @@
|
|||||||
"status" = status,
|
"status" = status,
|
||||||
"interface" = list(
|
"interface" = list(
|
||||||
"name" = interface,
|
"name" = interface,
|
||||||
"layout" = null, // user.read_preference(/datum/preference/choiced/tgui_layout), // unused
|
"layout" = user.read_preference(/datum/preference/choiced/tgui_layout),
|
||||||
),
|
),
|
||||||
//"refreshing" = refreshing,
|
//"refreshing" = refreshing,
|
||||||
"refreshing" = FALSE,
|
"refreshing" = FALSE,
|
||||||
|
|||||||
@@ -5,14 +5,19 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Icon,
|
Icon,
|
||||||
|
ImageButton,
|
||||||
Input,
|
Input,
|
||||||
|
LabeledList,
|
||||||
|
NoticeBox,
|
||||||
Section,
|
Section,
|
||||||
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'tgui-core/components';
|
} from 'tgui-core/components';
|
||||||
import { flow } from 'tgui-core/fp';
|
import { flow } from 'tgui-core/fp';
|
||||||
import { type BooleanLike, classes } from 'tgui-core/react';
|
import { type BooleanLike, classes } from 'tgui-core/react';
|
||||||
import { createSearch } from 'tgui-core/string';
|
import { capitalizeAll, createSearch } from 'tgui-core/string';
|
||||||
|
import { getLayoutState, LAYOUT, LayoutToggle } from './common/LayoutToggle';
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
chargesMoney: BooleanLike;
|
chargesMoney: BooleanLike;
|
||||||
@@ -38,6 +43,156 @@ type product = {
|
|||||||
max_amount: number; // Not used?
|
max_amount: number; // Not used?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Vending = (props) => {
|
||||||
|
const { act, data } = useBackend<Data>();
|
||||||
|
const { panel, chargesMoney, user, guestNotice, coin } = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Window width={450} height={600}>
|
||||||
|
<Window.Content>
|
||||||
|
<Stack fill vertical>
|
||||||
|
{chargesMoney ? (
|
||||||
|
<Stack.Item>
|
||||||
|
<NoticeBox info>
|
||||||
|
{user ? (
|
||||||
|
<Box>
|
||||||
|
<Icon name="id-card" mr={1} />
|
||||||
|
<i>
|
||||||
|
{user.name} | {user.job || 'Unemployed'}
|
||||||
|
</i>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
guestNotice
|
||||||
|
)}
|
||||||
|
</NoticeBox>
|
||||||
|
</Stack.Item>
|
||||||
|
) : null}
|
||||||
|
<Stack.Item grow>
|
||||||
|
<VendingMain />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{!!coin && (
|
||||||
|
<Section
|
||||||
|
title={`${coin} deposited`}
|
||||||
|
buttons={
|
||||||
|
<Button icon="eject" onClick={() => act('remove_coin')}>
|
||||||
|
Eject Coin
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack.Item>
|
||||||
|
{panel ? (
|
||||||
|
<Stack.Item>
|
||||||
|
<VendingMaintenance />
|
||||||
|
</Stack.Item>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VendingMain = (props) => {
|
||||||
|
const { act, data } = useBackend<Data>();
|
||||||
|
const { coin, chargesMoney, user, userMoney, guestNotice, products } = data;
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [toggleLayout, setToggleLayout] = useState(getLayoutState());
|
||||||
|
|
||||||
|
// Just in case we still have undefined values in the list
|
||||||
|
let myproducts = products.filter((item) => !!item);
|
||||||
|
myproducts = prepareSearch(myproducts, searchText);
|
||||||
|
myproducts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section
|
||||||
|
title="Products"
|
||||||
|
fill
|
||||||
|
scrollable
|
||||||
|
buttons={
|
||||||
|
<Stack>
|
||||||
|
<Stack.Item>
|
||||||
|
<Box inline color="good" fontSize={1.2} mr={1}>
|
||||||
|
{userMoney}₮
|
||||||
|
<Icon ml={1} name="coins" color="gold" />
|
||||||
|
</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Input
|
||||||
|
inline
|
||||||
|
expensive
|
||||||
|
placeholder="Search..."
|
||||||
|
onChange={(val) => setSearchText(val)}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<LayoutToggle state={toggleLayout} setState={setToggleLayout} />
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<VendingProducts layout={toggleLayout} products={myproducts} />
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VendingProducts = (props: {
|
||||||
|
layout: string;
|
||||||
|
products: product[];
|
||||||
|
}) => {
|
||||||
|
const { layout, products } = props;
|
||||||
|
|
||||||
|
if (layout === LAYOUT.Grid) {
|
||||||
|
return <VendingProductsGrid products={products} />;
|
||||||
|
} else {
|
||||||
|
return <VendingProductsList products={products} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VendingProductsGrid = (props: { products: product[] }) => {
|
||||||
|
const { act } = useBackend();
|
||||||
|
const { products } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{products.map((product) => (
|
||||||
|
<ImageButton
|
||||||
|
key={product.key}
|
||||||
|
asset={product.isatom ? ['vending32x32', product.path] : undefined}
|
||||||
|
tooltip={`${capitalizeAll(product.name)}${product.desc ? ` - ${product.desc}` : ''}`}
|
||||||
|
tooltipPosition="bottom-end"
|
||||||
|
buttonsAlt={
|
||||||
|
<Stack fontSize={0.85}>
|
||||||
|
<Stack.Item grow textAlign="left" color="gold">
|
||||||
|
{product.price ? `${product.price} ₮` : 'Free'}
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item color="lightgray">x{product.amount}</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
act('vend', {
|
||||||
|
vend: product.key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{product.name}
|
||||||
|
</ImageButton>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VendingProductsList = (props: { products: product[] }) => {
|
||||||
|
const { products } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
{products.map((product) => (
|
||||||
|
<VendingRow key={product.key} product={product} />
|
||||||
|
))}
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const VendingRow = (props: { product: product }) => {
|
const VendingRow = (props: { product: product }) => {
|
||||||
const { act, data } = useBackend<Data>();
|
const { act, data } = useBackend<Data>();
|
||||||
const { actively_vending } = data;
|
const { actively_vending } = data;
|
||||||
@@ -93,86 +248,14 @@ const VendingRow = (props: { product: product }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Vending = (props) => {
|
|
||||||
const { data } = useBackend<Data>();
|
|
||||||
const { panel } = data;
|
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Window width={450} height={600}>
|
|
||||||
<Window.Content scrollable>
|
|
||||||
<VendingProducts searchText={searchText} onSearch={setSearchText} />
|
|
||||||
{panel ? <VendingMaintenance /> : null}
|
|
||||||
</Window.Content>
|
|
||||||
</Window>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VendingProducts = (props: {
|
|
||||||
searchText: string;
|
|
||||||
onSearch: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
}) => {
|
|
||||||
const { act, data } = useBackend<Data>();
|
|
||||||
const { coin, chargesMoney, user, userMoney, guestNotice, products } = data;
|
|
||||||
|
|
||||||
// Just in case we still have undefined values in the list
|
|
||||||
let myproducts = products.filter((item) => !!item);
|
|
||||||
myproducts = prepareSearch(myproducts, props.searchText);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!!chargesMoney && (
|
|
||||||
<Section title="User">
|
|
||||||
{(user && (
|
|
||||||
<Box>
|
|
||||||
Welcome, <b>{user.name}</b>, <b>{user.job || 'Unemployed'}</b>!
|
|
||||||
<br />
|
|
||||||
Your balance is <b>{userMoney}₮ Thalers</b>.
|
|
||||||
</Box>
|
|
||||||
)) || <Box color="light-grey">{guestNotice}</Box>}
|
|
||||||
</Section>
|
|
||||||
)}
|
|
||||||
<Section title="Products">
|
|
||||||
<Table mb={1}>
|
|
||||||
<Table.Cell width="8%">
|
|
||||||
<Icon name="search" ml={1.5} />
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell>
|
|
||||||
<Input
|
|
||||||
fluid
|
|
||||||
placeholder="Search for products..."
|
|
||||||
onChange={(value: string) => props.onSearch(value)}
|
|
||||||
/>
|
|
||||||
</Table.Cell>
|
|
||||||
</Table>
|
|
||||||
<Table>
|
|
||||||
{myproducts.map((product, i) => (
|
|
||||||
<VendingRow key={i} product={product} />
|
|
||||||
))}
|
|
||||||
</Table>
|
|
||||||
</Section>
|
|
||||||
{!!coin && (
|
|
||||||
<Section
|
|
||||||
title={`${coin} deposited`}
|
|
||||||
buttons={
|
|
||||||
<Button icon="eject" onClick={() => act('remove_coin')}>
|
|
||||||
Eject Coin
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const VendingMaintenance = (props) => {
|
export const VendingMaintenance = (props) => {
|
||||||
const { act, data } = useBackend<Data>();
|
const { act, data } = useBackend<Data>();
|
||||||
const { speaker } = data;
|
const { speaker } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title="Maintenance Panel">
|
<Section title="Maintenance Panel">
|
||||||
<Section
|
<LabeledList>
|
||||||
title="Speaker"
|
<LabeledList.Item label="Speaker">
|
||||||
buttons={
|
|
||||||
<Button
|
<Button
|
||||||
icon={speaker ? 'volume-up' : 'volume-off'}
|
icon={speaker ? 'volume-up' : 'volume-off'}
|
||||||
selected={speaker}
|
selected={speaker}
|
||||||
@@ -180,8 +263,8 @@ export const VendingMaintenance = (props) => {
|
|||||||
>
|
>
|
||||||
{speaker ? 'Enabled' : 'Disabled'}
|
{speaker ? 'Enabled' : 'Disabled'}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</LabeledList.Item>
|
||||||
/>
|
</LabeledList>
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
47
tgui/packages/tgui/interfaces/common/LayoutToggle.tsx
Normal file
47
tgui/packages/tgui/interfaces/common/LayoutToggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useBackend } from 'tgui/backend';
|
||||||
|
import { Button, Stack } from 'tgui-core/components';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/** Current layout state, which will be passed. */
|
||||||
|
state: string;
|
||||||
|
/** useState function that must be passed in order to make a toggle functional. */
|
||||||
|
setState: (newState: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum LAYOUT {
|
||||||
|
Grid = 'grid',
|
||||||
|
List = 'list',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLayoutState() {
|
||||||
|
const { config } = useBackend();
|
||||||
|
return config.interface.layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the user to toggle between grid and list layouts, if preference on Default value.
|
||||||
|
* Otherwise it'll be controlled by preferences.
|
||||||
|
*/
|
||||||
|
export function LayoutToggle(props: Props) {
|
||||||
|
const { setState, state } = props;
|
||||||
|
const { act } = useBackend();
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
const newState = state === LAYOUT.Grid ? LAYOUT.List : LAYOUT.Grid;
|
||||||
|
setState(newState);
|
||||||
|
act('change_ui_state', {
|
||||||
|
new_state: newState,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
icon={state === LAYOUT.Grid ? 'list' : 'border-all'}
|
||||||
|
tooltip={state === LAYOUT.Grid ? 'View as List' : 'View as Grid'}
|
||||||
|
tooltipPosition={'bottom-end'}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user