From e16c9ce7bae8734c6b689e5de0840d2e0e1411d9 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:34:50 -0700 Subject: [PATCH] [MIRROR] Overhaul vending machine UI from Bubber (#11435) Co-authored-by: ShadowLarkens Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- code/__defines/preferences.dm | 6 + .../client/preferences/types/game/ui.dm | 18 ++ code/modules/tgui/external.dm | 5 + code/modules/tgui/tgui.dm | 2 +- tgui/packages/tgui/interfaces/Vending.tsx | 237 ++++++++++++------ .../tgui/interfaces/common/LayoutToggle.tsx | 47 ++++ 6 files changed, 237 insertions(+), 78 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/common/LayoutToggle.tsx diff --git a/code/__defines/preferences.dm b/code/__defines/preferences.dm index 2245a06c05..6980c7f985 100644 --- a/code/__defines/preferences.dm +++ b/code/__defines/preferences.dm @@ -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 diff --git a/code/modules/client/preferences/types/game/ui.dm b/code/modules/client/preferences/types/game/ui.dm index c9712d9f12..22a51149a8 100644 --- a/code/modules/client/preferences/types/game/ui.dm +++ b/code/modules/client/preferences/types/game/ui.dm @@ -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" diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 722577899a..b65b772a58 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -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 diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 09d0a4b26c..f2df809925 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -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, diff --git a/tgui/packages/tgui/interfaces/Vending.tsx b/tgui/packages/tgui/interfaces/Vending.tsx index b108c53f27..880059c35d 100644 --- a/tgui/packages/tgui/interfaces/Vending.tsx +++ b/tgui/packages/tgui/interfaces/Vending.tsx @@ -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(); + const { panel, chargesMoney, user, guestNotice, coin } = data; + + return ( + + + + {chargesMoney ? ( + + + {user ? ( + + + + {user.name} | {user.job || 'Unemployed'} + + + ) : ( + guestNotice + )} + + + ) : null} + + + + + {!!coin && ( +
act('remove_coin')}> + Eject Coin + + } + /> + )} + + {panel ? ( + + + + ) : null} + + + + ); +}; + +export const VendingMain = (props) => { + const { act, data } = useBackend(); + const { coin, chargesMoney, user, userMoney, guestNotice, products } = data; + + const [searchText, setSearchText] = useState(''); + 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 ( +
+ + + {userMoney}₮ + + + + + setSearchText(val)} + /> + + + + } + > + +
+ ); +}; + +export const VendingProducts = (props: { + layout: string; + products: product[]; +}) => { + const { layout, products } = props; + + if (layout === LAYOUT.Grid) { + return ; + } else { + return ; + } +}; + +export const VendingProductsGrid = (props: { products: product[] }) => { + const { act } = useBackend(); + const { products } = props; + + return ( + + {products.map((product) => ( + + + {product.price ? `${product.price} ₮` : 'Free'} + + x{product.amount} + + } + onClick={() => + act('vend', { + vend: product.key, + }) + } + > + {product.name} + + ))} + + ); +}; + +export const VendingProductsList = (props: { products: product[] }) => { + const { products } = props; + + return ( + + {products.map((product) => ( + + ))} +
+ ); +}; + const VendingRow = (props: { product: product }) => { const { act, data } = useBackend(); const { actively_vending } = data; @@ -93,86 +248,14 @@ const VendingRow = (props: { product: product }) => { ); }; -export const Vending = (props) => { - const { data } = useBackend(); - const { panel } = data; - const [searchText, setSearchText] = useState(''); - - return ( - - - - {panel ? : null} - - - ); -}; - -export const VendingProducts = (props: { - searchText: string; - onSearch: React.Dispatch>; -}) => { - const { act, data } = useBackend(); - 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 && ( -
- {(user && ( - - Welcome, {user.name}, {user.job || 'Unemployed'}! -
- Your balance is {userMoney}₮ Thalers. -
- )) || {guestNotice}} -
- )} -
- - - - - - props.onSearch(value)} - /> - -
- - {myproducts.map((product, i) => ( - - ))} -
-
- {!!coin && ( -
act('remove_coin')}> - Eject Coin - - } - /> - )} - - ); -}; - export const VendingMaintenance = (props) => { const { act, data } = useBackend(); const { speaker } = data; return (
-
+ - } - /> + +
); }; diff --git a/tgui/packages/tgui/interfaces/common/LayoutToggle.tsx b/tgui/packages/tgui/interfaces/common/LayoutToggle.tsx new file mode 100644 index 0000000000..998ad542c3 --- /dev/null +++ b/tgui/packages/tgui/interfaces/common/LayoutToggle.tsx @@ -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 ( + +