diff --git a/esbuild.mjs b/esbuild.mjs index 1e8d64782..d68693d89 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -79,6 +79,7 @@ const buildOptions = { loader: { // todo use external or resolve issues with duplicating '.png': 'dataurl', + '.svg': 'dataurl', '.map': 'empty', '.vert': 'text', '.frag': 'text', diff --git a/package.json b/package.json index 4bbeaf814..6905a8b76 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "browserify-zlib": "^0.2.0", "buffer": "^6.0.3", "constants-browserify": "^1.0.0", - "contro-max": "^0.1.6", + "contro-max": "^0.1.7", "crypto-browserify": "^3.12.0", "cypress": "^10.11.0", "cypress-esbuild-preprocessor": "^1.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beb0c0a9c..dcacb13aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -268,8 +268,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 contro-max: - specifier: ^0.1.6 - version: 0.1.6(typescript@5.5.0-beta) + specifier: ^0.1.7 + version: 0.1.7(typescript@5.5.0-beta) crypto-browserify: specifier: ^3.12.0 version: 3.12.0 @@ -3811,8 +3811,8 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} - contro-max@0.1.6: - resolution: {integrity: sha512-QsoOcAlbtNgkCGBvwKsh+GUVZ2c5zfMgYQCu+v4MplX5VolkWhMwAcEOBRxt8oENbnRXOKUGQr816Ey1G4/jpg==} + contro-max@0.1.7: + resolution: {integrity: sha512-HIYF1Dl50tUyTKaDsX+mPMDv2OjleNMVedYuBTX0n1wKNm9WxjWu2w74ATjz/8fHVL9GgmziIxAlFStd2je6kg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} convert-source-map@1.9.0: @@ -12851,7 +12851,7 @@ snapshots: content-type@1.0.5: {} - contro-max@0.1.6(typescript@5.5.0-beta): + contro-max@0.1.7(typescript@5.5.0-beta): dependencies: events: 3.3.0 lodash-es: 4.17.21 diff --git a/src/controls.ts b/src/controls.ts index 6dd47be29..6e38b90c2 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -6,21 +6,24 @@ import { proxy, subscribe } from 'valtio' import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' +import { UserOverridesConfig } from 'contro-max/build/types/store' import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState } from './globalState' import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './inventoryWindows' import { chatInputValueGlobal } from './react/Chat' import { fsState } from './loadSave' +import { customCommandsConfig } from './customCommands' +import { CustomCommand } from './react/KeybindingsCustom' import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' import { getItemFromBlock } from './botUtils' import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor' -// todo move this to shared file with component -export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) + +export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig subscribe(customKeymaps, () => { - localStorage.keymap = JSON.parse(customKeymaps) + localStorage.keymap = JSON.stringify(customKeymaps) }) const controlOptions = { @@ -53,7 +56,8 @@ export const contro = new ControMax({ }, advanced: { lockUrl: ['KeyY'], - } + }, + custom: {} as Record, // waila: { // showLookingBlockRecipe: ['Numpad3'], // showLookingBlockUsages: ['Numpad4'] @@ -81,6 +85,8 @@ export const contro = new ControMax({ window.controMax = contro export type Command = CommandEventArgument['command'] +// updateCustomBinds() + export const setDoPreventDefault = (state: boolean) => { controlOptions.preventDefault = state } @@ -285,6 +291,20 @@ function cycleHotbarSlot (dir: 1 | -1) { bot.setQuickBarSlot(newHotbarSlot) } +// custom commands hamdler +const customCommandsHandler = (buttonData: { code?: string, button?: string, state: boolean }) => { + if (!buttonData.state || !isGameActive(true)) return + + const codeOrButton = buttonData.code ?? buttonData.button + const inputType = buttonData.code ? 'keys' : 'gamepad' + for (const value of Object.values(contro.userConfig!.custom)) { + if (value[inputType]?.includes(codeOrButton!)) { + customCommandsConfig[(value as CustomCommand).type].handler((value as CustomCommand).inputs) + } + } +} +contro.on('pressedKeyOrButtonChanged', customCommandsHandler) + contro.on('trigger', ({ command }) => { const willContinue = !isGameActive(true) alwaysPressedHandledCommand(command) diff --git a/src/cross_playstation_console_controller_gamepad_icon.svg b/src/cross_playstation_console_controller_gamepad_icon.svg new file mode 100644 index 000000000..d7d176e27 --- /dev/null +++ b/src/cross_playstation_console_controller_gamepad_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/customCommands.ts b/src/customCommands.ts new file mode 100644 index 000000000..eddfc89e2 --- /dev/null +++ b/src/customCommands.ts @@ -0,0 +1,91 @@ +import { guiOptionsScheme, tryFindOptionConfig } from './optionsGuiScheme' +import { options } from './optionsStorage' + +export const customCommandsConfig = { + chat: { + input: [ + { + type: 'text', + placeholder: 'Command to send e.g. gamemode creative' + } + ], + handler ([command]) { + bot.chat(`/${command.replace(/^\//, '')}`) + } + }, + setOrToggleSetting: { + input: [ + { + type: 'select', + // maybe title case? + options: Object.keys(options) + }, + { + type: 'select', + options: ['toggle', 'set'] + }, + ([setting = '', action = ''] = []) => { + const value = options[setting] + if (!action || value === undefined || action === 'toggle') return null + if (action === 'set') { + const getBase = () => { + const config = tryFindOptionConfig(setting as any) + if (config && 'values' in config) { + return { + type: 'select', + options: config.values + } + } + if (config?.type === 'toggle' || typeof value === 'boolean') { + return { + type: 'select', + options: ['true', 'false'] + } + } + if (config?.type === 'slider' || value.type === 'number') { + return { + type: 'number', + } + } + return { + type: 'text' + } + } + return { + ...getBase(), + placeholder: value + } + } + } + ], + handler ([setting, action, value]) { + if (action === 'toggle') { + const value = options[setting] + const config = tryFindOptionConfig(setting) + if (config && 'values' in config && config.values) { + const { values } = config + const currentIndex = values.indexOf(value) + const nextIndex = (currentIndex + 1) % values.length + options[setting] = values[nextIndex] + } else { + options[setting] = typeof value === 'boolean' ? !value : typeof value === 'number' ? value + 1 : value + } + } else { + options[setting] = value + } + } + }, + jsScripts: { + input: [ + { + type: 'text', + placeholder: 'JavaScript code to run in main thread (sensitive!)' + } + ], + handler ([code]) { + // eslint-disable-next-line no-new-func -- this is a feature, not a bug + new Function(code)() + } + }, + // openCommandsScreen: {} +} diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 1c92a39c0..b847fe11d 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -184,7 +184,16 @@ export const guiOptionsScheme: { custom () { return Keyboard & Mouse }, - // keybindings + }, + { + custom () { + return + }, mouseSensX: {}, mouseSensY: { min: -1, @@ -282,3 +291,15 @@ const Category = ({ children }) =>
{children}
+ +export const tryFindOptionConfig = (option: keyof AppOptions) => { + for (const group of Object.values(guiOptionsScheme)) { + for (const optionConfig of group) { + if (option in optionConfig) { + return optionConfig[option] + } + } + } + + return null +} diff --git a/src/react/KeybindingsCustom.tsx b/src/react/KeybindingsCustom.tsx new file mode 100644 index 000000000..bee991bfc --- /dev/null +++ b/src/react/KeybindingsCustom.tsx @@ -0,0 +1,171 @@ +import { useEffect, useState, useContext } from 'react' +import { customCommandsConfig } from '../customCommands' +import { ButtonWithMatchesAlert, Context } from './KeybindingsScreen' +import Button from './Button' +import styles from './KeybindingsScreen.module.css' +import Input from './Input' + + +export type CustomCommand = { + keys: undefined | string[] + gamepad: undefined | string[] + type: string + inputs: any[] +} + +export type CustomCommandsMap = Record + +export default ( + { + customCommands, + updateCurrBind, + resetBinding, + }: { + customCommands: CustomCommandsMap, + updateCurrBind: (group: string, action: string) => void, + resetBinding: (group: string, action: string, inputType: string) => void, + } +) => { + const { userConfig, setUserConfig } = useContext(Context) + const [customConfig, setCustomConfig] = useState({ ...customCommands }) + + useEffect(() => { + setUserConfig({ ...userConfig, custom: { ...customConfig } }) + }, [customConfig]) + + const addNewCommand = (type: string) => { + // max key + 1 + const newKey = String(Math.max(...Object.keys(customConfig).map(Number).filter(key => !isNaN(key)), 0) + 1) + setCustomConfig(prev => { + const newCustomConf = { ...prev } + newCustomConf[newKey] = { + keys: undefined as string[] | undefined, + gamepad: undefined as string[] | undefined, + type, + inputs: [] as any[] + } + return newCustomConf + }) + } + + return <> +
+ {Object.entries(customCommandsConfig).map(([group, { input }]) => ( +
+
{group}
+ {Object.entries(customConfig).filter(([key, data]) => data.type === group).map((commandData, indexOption) => { + return + })} +
+ ))} +
+ +} + +const CustomCommandContainer = ( + { + indexOption, + commandData, + updateCurrBind, + setCustomConfig, + resetBinding, + groupData + } +) => { + const { userConfig } = useContext(Context) + + const [commandKey, { keys, gamepad, inputs }] = commandData + const [group, { input }] = groupData + + const setInputValue = (optionKey, indexInput, value) => { + setCustomConfig(prev => { + const newConfig = { ...prev } + newConfig[optionKey].inputs = [...prev[optionKey].inputs] + newConfig[optionKey].inputs[indexInput] = value + return newConfig + }) + } + + return
+ {input.map((obj, indexInput) => { + const config = typeof obj === 'function' ? obj(inputs) : obj + if (!config) return null + + return config.type === 'select' + ? + : setInputValue(commandKey, indexInput, e.target.value)} /> + })} +
+ { + userConfig?.['custom']?.[commandKey]?.keys ?
+
+} diff --git a/src/react/KeybindingsScreen.module.css b/src/react/KeybindingsScreen.module.css new file mode 100644 index 000000000..e8a9f69b3 --- /dev/null +++ b/src/react/KeybindingsScreen.module.css @@ -0,0 +1,88 @@ +.container { + overflow-x: auto; + width: 100%; + display: flex; + flex-direction: column; + gap: 10px; +} + +@media (max-width: 590px) { + .container { + width: 90vw; + } +} + +.group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.group-category { + font-size: 1rem; + text-align: center; + margin-top: 15px; + margin-bottom: 7px; + text-transform: capitalize; +} + +.actionBinds { + position: relative; + display: flex; +} + +.warning-container { + flex-basis: 25%; + display: flex; + flex-direction: column; + height: inherit; +} + +.actionName { + flex-basis: 30%; + margin-right: 5px; + flex-wrap: wrap; + align-self: center; + font-size: 10px; +} + +.undo-keyboard, +.undo-gamepad { + aspect-ratio: 1; +} + +.button { + width: 100%; + font-size: 7px; +} + +.margin-left { + margin-left: 25px; +} + +.matched-bind { + border: 1px solid red; + border-bottom: 1px solid red; +} + +.matched-bind-warning { + display: flex; + color: yellow; + font-family: inherit; + font-size: 5px; + width: fit-content; +} + +.matched-bind-warning a { + color: inherit; +} + +/* ~~~~ custom bindings styles */ + +.chat-command { + font: inherit; + color: white; + display: block; + height: 100%; + flex-grow: 1; +} diff --git a/src/react/KeybindingsScreen.tsx b/src/react/KeybindingsScreen.tsx index 19038a71b..971266552 100644 --- a/src/react/KeybindingsScreen.tsx +++ b/src/react/KeybindingsScreen.tsx @@ -1,12 +1,382 @@ +import { useState, useEffect, useRef, createContext, useContext } from 'react' +import { UserOverridesConfig } from 'contro-max/build/types/store' +import { contro as controEx } from '../controls' +import { hideModal } from '../globalState' +import triangle from './ps_icons/playstation_triangle_console_controller_gamepad_icon.svg' +import square from './ps_icons/playstation_square_console_controller_gamepad_icon.svg' +import circle from './ps_icons/circle_playstation_console_controller_gamepad_icon.svg' +import cross from './ps_icons/cross_playstation_console_controller_gamepad_icon.svg' +import PixelartIcon from './PixelartIcon' +import KeybindingsCustom, { CustomCommandsMap } from './KeybindingsCustom' +import { BindingActionsContext } from './KeybindingsScreenProvider' +import Button from './Button' import Screen from './Screen' +import styles from './KeybindingsScreen.module.css' -export default ({ - onBack, - onReset, - onSet, - keybindings + +type HandleClick = (group: string, action: string, index: number, type: string | null) => void + +type setBinding = (data: any, group: string, command: string, buttonIndex: number) => void + +export const Context = createContext( + { + isPS: false as boolean | undefined, + userConfig: controEx?.userConfig ?? {} as UserOverridesConfig | undefined, + setUserConfig (config) { }, + handleClick: (() => { }) as HandleClick, + parseBindingName (binding) { return '' as string }, + bindsMap: { keyboard: {} as any, gamepad: {} as any } + } +) + +export default ( + { + contro, + isPS, + }: { + contro: typeof controEx, + isPS?: boolean + } +) => { + const containerRef = useRef(null) + const bindsMap = useRef({ keyboard: {} as any, gamepad: {} as any }) + const { commands } = contro.inputSchema + const [userConfig, setUserConfig] = useState(contro.userConfig ?? {}) + const [awaitingInputType, setAwaitingInputType] = useState(null as null | 'keyboard' | 'gamepad') + const [groupName, setGroupName] = useState('') + const [actionName, setActionName] = useState('') + const [buttonNum, setButtonNum] = useState(0) + const { updateBinds } = useContext(BindingActionsContext) + const [customCommands, setCustomCommands] = useState(userConfig.custom as CustomCommandsMap ?? {}) + + const updateCurrBind = (group: string, action: string) => { + setGroupName(prev => group) + setActionName(prev => action) + } + + const handleClick: HandleClick = (group, action, index, type) => { + //@ts-expect-error + setAwaitingInputType(type) + updateCurrBind(group, action) + setButtonNum(prev => index) + } + + const setBinding: setBinding = (data, group, command, buttonIndex) => { + setUserConfig(prev => { + const newConfig = { ...prev } + newConfig[group] ??= {} + newConfig[group][command] ??= {} + + // keys and buttons should always exist in commands + const type = 'code' in data ? 'keys' : 'button' in data ? 'gamepad' : null + if (type) { + newConfig[group][command][type] ??= group === 'custom' ? [] : [...contro.inputSchema.commands[group][command][type]] + newConfig[group][command][type]![buttonIndex] = data.code ?? data.button + } + + return newConfig + }) + } + + const resetBinding = (group: string, command: string, inputType: string) => { + if (!userConfig?.[group]?.[command]) return + + setUserConfig(prev => { + const newConfig = { ...prev } + const prop = inputType === 'keyboard' ? 'keys' : 'gamepad' + newConfig[group][command][prop] = undefined + return newConfig + }) + } + + useEffect(() => { + updateBinds(userConfig) + setCustomCommands({ ...userConfig.custom as CustomCommandsMap }) + + updateBindMap() + }, [userConfig]) + + // const updateKeyboardBinding = (e: import('react').KeyboardEvent) => { + // if (!e.code || e.key === 'Escape' || !awaitingInputType) return + // setBinding({ code: e.code, state: true }, groupName, actionName, buttonNum) + // } + + const updateBinding = (data: any) => { + if ((!data.state && awaitingInputType) || !awaitingInputType) { + setAwaitingInputType(null) + return + } + if ('code' in data) { + if (data.code === 'Escape' || ['Mouse0', 'Mouse1', 'Mouse2'].includes(data.code)) { + setAwaitingInputType(null) + return + } + setBinding({ code: data.code, state: true }, groupName, actionName, buttonNum) + } + if ('button' in data) { + contro.enabled = false + void Promise.resolve().then(() => { contro.enabled = true }) + setBinding(data, groupName, actionName, buttonNum) + } + + setAwaitingInputType(null) + } + + const updateBindMap = () => { + bindsMap.current = { keyboard: {} as any, gamepad: {} as any } + if (commands) { + for (const [group, actions] of Object.entries(commands)) { + for (const [action, { keys, gamepad }] of Object.entries(actions)) { + if (keys) { + let currKeys + if (userConfig?.[group]?.[action]?.keys) { + currKeys = userConfig[group][action].keys + } else { + currKeys = keys + } + for (const [index, key] of currKeys.entries()) { + bindsMap.current.keyboard[key] ??= [] + if (!bindsMap.current.keyboard[key].some(obj => obj.group === group && obj.action === action && obj.index === index)) { + bindsMap.current.keyboard[key].push({ group, action, index }) + } + } + } + if (gamepad) { + let currButtons + if (userConfig?.[group]?.[action]?.gamepad) { + currButtons = userConfig[group][action].gamepad + } else { + currButtons = gamepad + } + if (currButtons.length > 0) { + bindsMap.current.gamepad[currButtons[0]] ??= [] + bindsMap.current.gamepad[currButtons[0]].push({ group, action, index: 0 }) + } + } + } + } + } + } + + // fill binds map + useEffect(() => { + updateBindMap() + }, []) + + useEffect(() => { + contro.on('pressedKeyOrButtonChanged', updateBinding) + + return () => { + contro.off('pressedKeyOrButtonChanged', updateBinding) + } + }, [groupName, actionName, awaitingInputType]) + + + return + + {awaitingInputType && } +
+ + + {Object.entries(commands).map(([group, actions], index) => { + if (group === 'custom') return null + return
+
{group}
+ {group === 'general' ? ( +
+ Note: Left, right and middle click keybindings are hardcoded and cannot be changed currently. +
+ ) : null} + {Object.entries(actions).map(([action, { keys, gamepad }]) => { + return
+
{parseActionName(action)}
+ +
+ })} +
+ })} + + +
+
+
+} + +export const ButtonWithMatchesAlert = ({ + group, + action, + index, + inputType, + keys, + gamepad, }) => { - return -

Here you can change the keybindings for the game.

-
+ const { isPS, userConfig, handleClick, parseBindingName, bindsMap } = useContext(Context) + const [buttonSign, setButtonSign] = useState('') + + useEffect(() => { + const type = inputType === 'keyboard' ? 'keys' : 'gamepad' + + const customValue = userConfig?.[group]?.[action]?.[type]?.[index] + if (customValue) { + if (type === 'keys') { + setButtonSign(parseBindingName(customValue)) + } else { + setButtonSign(isPS && buttonsMap[customValue] ? buttonsMap[customValue] : customValue) + } + } else if (type === 'keys') { + setButtonSign(keys?.length ? parseBindingName(keys[index]) : '') + } else { + setButtonSign(gamepad?.[0] ? + isPS ? + buttonsMap[gamepad[0]] ?? gamepad[0] + : gamepad[0] + : '') + } + }, [userConfig, isPS]) + + return
+ + {userConfig?.[group]?.[action]?.[inputType === 'keyboard' ? 'keys' : 'gamepad']?.some( + key => Object.keys(bindsMap[inputType]).includes(key) + && bindsMap[inputType][key].length > 1 + && bindsMap[inputType][key].some( + prop => prop.index === index + && prop.group === group + && prop.action === action + ) + ) ? ( +
+ +
+ This bind is already in use. +
+
+ ) : null} +
+} + +export const AwaitingInputOverlay = ({ isGamepad }) => { + return
+
+ {isGamepad ? 'Press the button on the gamepad ' : 'Press the key, side mouse button '} + or ESC to cancel. +
+ +
+} + +const parseActionName = (action: string) => { + const parts = action.split(/(?=[A-Z])/) + parts[0] = parts[0].charAt(0).toUpperCase() + parts[0].slice(1) + return parts.join(' ') +} + +const parseBindingName = (binding: string | undefined) => { + if (!binding) return '' + const cut = binding.replaceAll(/(Numpad|Digit|Key)/g, '') + const parts = cut.split(/(?=[A-Z\d])/) + return parts.reverse().join(' ') +} + +const buttonsMap = { + 'A': cross, + 'B': circle, + 'X': square, + 'Y': triangle } diff --git a/src/react/KeybindingsScreenApp.tsx b/src/react/KeybindingsScreenApp.tsx deleted file mode 100644 index e0630eab3..000000000 --- a/src/react/KeybindingsScreenApp.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useState } from 'react' -import { contro } from '../controls' -import Button from './Button' -import Screen from './Screen' - -export default () => { - const { commands } = contro.inputSchema - const [awaitingInputType, setAwaitingInputType] = useState(null as null | 'keyboard' | 'gamepad') - - // const - - return - {awaitingInputType && } -

Here you can change the keybindings for the game.

-
- {Object.entries(commands).map(([group, actions]) => { - return
-

{group}

- {Object.entries(actions).map(([action, { keys, gamepadButtons }]) => { - return
- - -
- })} -
- })} -
-
-} - -const AwaitingInputOverlay = ({ isGamepad }) => { - return
- {isGamepad ? 'Press the button on the gamepad' : 'Press the key'}. - Press ESC to cancel. -
-} diff --git a/src/react/KeybindingsScreenProvider.tsx b/src/react/KeybindingsScreenProvider.tsx new file mode 100644 index 000000000..97f04a622 --- /dev/null +++ b/src/react/KeybindingsScreenProvider.tsx @@ -0,0 +1,50 @@ +import { createContext, useState } from 'react' +import { contro } from '../controls' +import KeybindingsScreen from './KeybindingsScreen' +import { useIsModalActive } from './utilsApp' + + +export const updateBinds = (commands: any) => { + contro.inputSchema.commands.custom = Object.fromEntries(Object.entries(commands?.custom ?? {}).map(([key, value]) => { + return [key, { + keys: [], + gamepad: [], + type: '', + inputs: [] + }] + })) + + for (const [group, actions] of Object.entries(commands)) { + contro.userConfig![group] = Object.fromEntries(Object.entries(actions).map(([key, value]) => { + const newValue = { + keys: value?.keys ?? undefined, + gamepad: value?.gamepad ?? undefined, + } + + if (group === 'custom') { + newValue['type'] = (value).type + newValue['inputs'] = (value).inputs + } + + return [key, newValue] + })) + } +} + +const bindingActions = { + updateBinds +} + +export const BindingActionsContext = createContext(bindingActions) + +export default () => { + const [bindActions, setBindActions] = useState(bindingActions) + const isModalActive = useIsModalActive('keybindings') + + if (!isModalActive) return null + + const hasPsGamepad = [...(navigator.getGamepads?.() ?? [])].some(gp => gp?.id.match(/playstation|dualsense|dualshock/i)) // todo: use last used gamepad detection + return + + +} diff --git a/src/react/OptionsGroup.tsx b/src/react/OptionsGroup.tsx index bbfac00e7..81df531b0 100644 --- a/src/react/OptionsGroup.tsx +++ b/src/react/OptionsGroup.tsx @@ -3,7 +3,7 @@ import { options } from '../optionsStorage' import { OptionsGroupType, guiOptionsScheme } from '../optionsGuiScheme' import OptionsItems, { OptionMeta } from './OptionsItems' -const optionValueToType = (optionValue: any, item: OptionMeta) => { +export const optionValueToType = (optionValue: any, item: OptionMeta) => { if (typeof optionValue === 'boolean' || item.values) return 'toggle' if (typeof optionValue === 'number') return 'slider' if (typeof optionValue === 'string') return 'element' diff --git a/src/react/globals.d.ts b/src/react/globals.d.ts index 842d27c4c..108fae54f 100644 --- a/src/react/globals.d.ts +++ b/src/react/globals.d.ts @@ -25,6 +25,10 @@ declare module '*.png' { const png: string export default png } +declare module '*.svg' { + const svg: string + export default svg +} interface PromiseConstructor { withResolvers (): { diff --git a/src/react/ps_icons/circle_playstation_console_controller_gamepad_icon.svg b/src/react/ps_icons/circle_playstation_console_controller_gamepad_icon.svg new file mode 100644 index 000000000..d49af3365 --- /dev/null +++ b/src/react/ps_icons/circle_playstation_console_controller_gamepad_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/react/ps_icons/cross_playstation_console_controller_gamepad_icon.svg b/src/react/ps_icons/cross_playstation_console_controller_gamepad_icon.svg new file mode 100644 index 000000000..d7d176e27 --- /dev/null +++ b/src/react/ps_icons/cross_playstation_console_controller_gamepad_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/react/ps_icons/playstation_square_console_controller_gamepad_icon.svg b/src/react/ps_icons/playstation_square_console_controller_gamepad_icon.svg new file mode 100644 index 000000000..9f08790ec --- /dev/null +++ b/src/react/ps_icons/playstation_square_console_controller_gamepad_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/react/ps_icons/playstation_triangle_console_controller_gamepad_icon.svg b/src/react/ps_icons/playstation_triangle_console_controller_gamepad_icon.svg new file mode 100644 index 000000000..a397c5171 --- /dev/null +++ b/src/react/ps_icons/playstation_triangle_console_controller_gamepad_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/react/storageProvider.ts b/src/react/storageProvider.ts new file mode 100644 index 000000000..df97d2282 --- /dev/null +++ b/src/react/storageProvider.ts @@ -0,0 +1,13 @@ +import { CustomCommand } from './KeybindingsCustom' + +type StorageData = { + customCommands: Record + // ... +} + +export const getStoredValue = (name: T): StorageData[T] | undefined => { + return localStorage[name] ? JSON.parse(localStorage[name]) : undefined +} +export const setStoredValue = (name: T, value: StorageData[T]) => { + localStorage[name] = JSON.stringify(value) +} diff --git a/src/reactUi.tsx b/src/reactUi.tsx index 9a79de340..e384fbfd9 100644 --- a/src/reactUi.tsx +++ b/src/reactUi.tsx @@ -36,6 +36,7 @@ import Crosshair from './react/Crosshair' import ButtonAppProvider from './react/ButtonAppProvider' import ServersListProvider from './react/ServersListProvider' import GamepadUiCursor from './react/GamepadUiCursor' +import KeybindingsScreenProvider from './react/KeybindingsScreenProvider' import HeldMapUi from './react/HeldMapUi' const RobustPortal = ({ children, to }) => { @@ -157,6 +158,7 @@ const App = () => { + diff --git a/src/screens.css b/src/screens.css index c4e7fe9bc..32765506d 100644 --- a/src/screens.css +++ b/src/screens.css @@ -33,6 +33,7 @@ margin-top: 35px; /* todo remove it but without it in chrome android the screen is not scrollable */ overflow: auto; + height: fit-content; /* todo I'm not sure about it */ /* margin-top: calc(100% / 6 - 16px); */ align-items: center;