[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:
CHOMPStation2StaffMirrorBot
2025-08-15 19:34:50 -07:00
committed by GitHub
parent b74d08609a
commit e16c9ce7ba
6 changed files with 237 additions and 78 deletions

View File

@@ -63,6 +63,12 @@
#define EMOTE_SOUND_VOICE_FREQ "Voice Frequency"
#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_INSTANT 2
#define WRITE_PREF_MANUAL 3

View File

@@ -51,6 +51,24 @@
default_value = FALSE
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
category = PREFERENCE_CATEGORY_GAME_PREFERENCES
savefile_key = "TGUI_SAY"

View File

@@ -94,6 +94,11 @@
// If UI is not interactive or usr calling Topic is not the UI user, bail.
if(!ui || ui.status != STATUS_INTERACTIVE)
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

View File

@@ -281,7 +281,7 @@
"status" = status,
"interface" = list(
"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" = FALSE,

View File

@@ -5,14 +5,19 @@ import {
Box,
Button,
Icon,
ImageButton,
Input,
LabeledList,
NoticeBox,
Section,
Stack,
Table,
Tooltip,
} from 'tgui-core/components';
import { flow } from 'tgui-core/fp';
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 = {
chargesMoney: BooleanLike;
@@ -38,6 +43,156 @@ type product = {
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 { act, data } = useBackend<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) => {
const { act, data } = useBackend<Data>();
const { speaker } = data;
return (
<Section title="Maintenance Panel">
<Section
title="Speaker"
buttons={
<LabeledList>
<LabeledList.Item label="Speaker">
<Button
icon={speaker ? 'volume-up' : 'volume-off'}
selected={speaker}
@@ -180,8 +263,8 @@ export const VendingMaintenance = (props) => {
>
{speaker ? 'Enabled' : 'Disabled'}
</Button>
}
/>
</LabeledList.Item>
</LabeledList>
</Section>
);
};

View 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>
);
}