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_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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
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