diff --git a/www/src/Data/Pins.ts b/www/src/Data/Pins.ts index 94b7fa658..023aaa24a 100644 --- a/www/src/Data/Pins.ts +++ b/www/src/Data/Pins.ts @@ -45,14 +45,5 @@ export const BUTTON_ACTIONS = { CUSTOM_BUTTON_COMBO: 40, } as const; -// Hide from select options / Disable select if returned from board -export const NON_SELECTABLE_BUTTON_ACTIONS = [ - BUTTON_ACTIONS.RESERVED, - BUTTON_ACTIONS.ASSIGNED_TO_ADDON, - BUTTON_ACTIONS.BUTTON_PRESS_TURBO, - BUTTON_ACTIONS.BUTTON_PRESS_MACRO, - BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO, -] as const; - type PinActionKeys = keyof typeof BUTTON_ACTIONS; export type PinActionValues = (typeof BUTTON_ACTIONS)[PinActionKeys]; diff --git a/www/src/Pages/PinMapping.tsx b/www/src/Pages/PinMapping.tsx index f82da1168..78b1caa51 100644 --- a/www/src/Pages/PinMapping.tsx +++ b/www/src/Pages/PinMapping.tsx @@ -1,19 +1,12 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import { NavLink } from 'react-router-dom'; -import { Alert, Button, Col, Form, Nav, Row, Tab, Tabs } from 'react-bootstrap'; +import { Alert, Button, Col, Form, Nav, Row, Tab } from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import invert from 'lodash/invert'; import omit from 'lodash/omit'; -import zip from 'lodash/zip'; import { AppContext } from '../Contexts/AppContext'; -import usePinStore, { MaskPayload, setPinType } from '../Store/usePinStore'; +import usePinStore, { MaskPayload } from '../Store/usePinStore'; import useProfilesStore from '../Store/useProfilesStore'; import Section from '../Components/Section'; @@ -21,24 +14,53 @@ import CustomSelect from '../Components/CustomSelect'; import CaptureButton from '../Components/CaptureButton'; import { BUTTON_MASKS, DPAD_MASKS, getButtonLabels } from '../Data/Buttons'; -import { - BUTTON_ACTIONS, - NON_SELECTABLE_BUTTON_ACTIONS, - PinActionValues, -} from '../Data/Pins'; +import { BUTTON_ACTIONS, PinActionValues } from '../Data/Pins'; import './PinMapping.scss'; +import { ActionMeta, MultiValue, SingleValue } from 'react-select'; -type PinCell = [string, MaskPayload]; -type PinRow = [PinCell, PinCell]; -type PinList = [PinRow]; +type OptionType = { + label: string; + value: PinActionValues; + type: string; + customButtonMask: number; + customDpadMask: number; +}; + +type PinSectionType = { + title: string; + pins: { [key: string]: MaskPayload | PinActionValues }; + onChange: ( + pin: string, + ) => + | (( + newValue: MultiValue | SingleValue, + actionMeta: ActionMeta, + ) => void) + | undefined; + onSave: () => Promise; + isMulti?: boolean; +}; + +const disabledOptions = [ + BUTTON_ACTIONS.RESERVED, + BUTTON_ACTIONS.ASSIGNED_TO_ADDON, + BUTTON_ACTIONS.BUTTON_PRESS_TURBO, + BUTTON_ACTIONS.BUTTON_PRESS_MACRO, +]; const getMask = (maskArr, key) => maskArr.find( ({ label }) => label?.toUpperCase() === key.split('BUTTON_PRESS_')?.pop(), ); -const isNonSelectable = (value) => - NON_SELECTABLE_BUTTON_ACTIONS.includes(value); +const isNonSelectable = (action) => + [ + BUTTON_ACTIONS.NONE, + BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO, + ...disabledOptions, + ].includes(action); + +const isDisabled = (action) => disabledOptions.includes(action); const options = Object.entries(BUTTON_ACTIONS) .filter(([, value]) => !isNonSelectable(value)) @@ -59,279 +81,202 @@ const options = Object.entries(BUTTON_ACTIONS) }; }); -type PinsFormTypes = { - savePins: () => void; - pins: { [key: string]: MaskPayload }; - setPin: setPinType; - onCopy?: () => void; +const groupedOptions = [ + { + label: 'Buttons', + options: options.filter(({ type }) => type !== 'action'), + }, + { + label: 'Actions', + options: options.filter(({ type }) => type === 'action'), + }, +]; + +const getMultiValue = (pinData: MaskPayload) => { + if (pinData.action === BUTTON_ACTIONS.NONE) return; + if (isDisabled(pinData.action)) { + const actionKey = invert(BUTTON_ACTIONS)[pinData.action]; + return [{ label: actionKey, ...pinData }]; + } + + return pinData.action === BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO + ? options.filter( + ({ type, customButtonMask, customDpadMask }) => + (pinData.customButtonMask & customButtonMask && + type === 'customButtonMask') || + (pinData.customDpadMask & customDpadMask && + type === 'customDpadMask'), + ) + : options.filter((option) => option.value === pinData.action); +}; + +const getSingleValue = (pinData: PinActionValues) => { + const actionKey = invert(BUTTON_ACTIONS)[pinData]; + return { label: actionKey, value: pinData }; }; -const PinsForm = ({ savePins, pins, setPin, onCopy }: PinsFormTypes) => { +const PinSection = ({ + title, + pins, + onChange, + onSave, + isMulti = true, +}: PinSectionType) => { const { buttonLabels, updateUsedPins } = useContext(AppContext); + const { t } = useTranslation(''); const [saveMessage, setSaveMessage] = useState(''); const { buttonLabelType, swapTpShareLabels } = buttonLabels; const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels); const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']); - const { t } = useTranslation(''); - const handleSubmit = async (e) => { + const getOptionLabel = useCallback( + (option: OptionType) => { + const labelKey = option.label?.split('BUTTON_PRESS_')?.pop(); + // Need to fallback as some button actions are not part of button names + return ( + (labelKey && buttonNames[labelKey]) || + t(`PinMapping:actions.${option.label}`) + ); + }, + [buttonNames], + ); + + const handleSubmit = useCallback(async (e) => { e.preventDefault(); e.stopPropagation(); try { - await savePins(); + await onSave(); updateUsedPins(); setSaveMessage(t('Common:saved-success-message')); } catch (error) { setSaveMessage(t('Common:saved-error-message')); } - }; - - const pinList = useMemo(() => { - const pinArray = Object.entries(pins); - return zip( - pinArray.slice(0, pinArray.length / 2), - pinArray.slice(pinArray.length / 2, pinArray.length), - ); - }, [pins]); - - const createCell = useCallback( - ([pin, pinData]: PinCell) => ( -
-
- -
- - (pinData.customButtonMask & customButtonMask && - type === 'customButtonMask') || - (pinData.customDpadMask & customDpadMask && - type === 'customDpadMask'), - ) - : options.find((option) => option.value === pinData.action) - } - isDisabled={ - isNonSelectable(pinData.action) && - pinData.action !== BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO - } - getOptionLabel={(option) => { - const labelKey = option.label?.split('BUTTON_PRESS_')?.pop(); - // Need to fallback as some button actions are not part of button names - return ( - (labelKey && buttonNames[labelKey]) || - t(`PinMapping:actions.${option.label}`) - ); - }} - // Show multi selection - isMulti={ - pinData.action === BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO || - options.find(({ value }) => value === pinData.action)?.type !== - 'action' - } - onChange={(selected) => { - if (Array.isArray(selected) && selected.length) { - const actionOption = selected.find( - ({ type }) => type === 'action', - ); - // Replace current options to single option if choosing none mask type - if (actionOption) { - setPin(pin, { - action: actionOption.value, - customButtonMask: 0, - customDpadMask: 0, - }); - } else { - setPin( - pin, - selected.reduce( - (masks, option) => ({ - action: BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO, - customButtonMask: - option.type === 'customButtonMask' - ? masks.customButtonMask ^ option.customButtonMask - : masks.customButtonMask, - customDpadMask: - option.type === 'customDpadMask' - ? masks.customDpadMask ^ option.customDpadMask - : masks.customDpadMask, - }), - { customButtonMask: 0, customDpadMask: 0 }, - ), - ); - } - } else { - setPin(pin, { - action: selected?.value || BUTTON_ACTIONS.NONE, - customButtonMask: 0, - customDpadMask: 0, - }); - } - }} - /> -
- ), - [buttonNames], - ); + }, []); return ( -
-
-
- - setPin( - // Convert getHeldPins format to setPinMappings format - parseInt(pin) < 10 ? `pin0${pin}` : `pin${pin}`, - // Maps current mode buttons to actions - BUTTON_ACTIONS[ - `BUTTON_PRESS_${invert(buttonNames)[label].toUpperCase()}` - ], - ) - } - /> -
- {onCopy && ( -
- -
- )} + <> +
+ + Mapping buttons to pins that aren't connected or available can + leave the device in non-functional state. To clear the invalid + configuration go to the{' '} + Reset Settings page. + +
+
+ {t(`PinMapping:profile-pins-warning`)}
-
- {pinList.map(([cell1, cell2], i) => ( -
- {createCell(cell1)} - {createCell(cell2)} +
+ +
+ {Object.entries(pins).map(([pin, pinData], index) => ( +
+
+ +
+ +
+ ))}
- ))} -
- - {saveMessage && {saveMessage}} - + + {saveMessage && {saveMessage}} + + + ); }; - -// export default function PinMappingPage() { -// const { fetchPins, pins, savePins, setPin } = usePinStore(); -// const { -// fetchProfiles, -// profiles, -// saveProfiles, -// setProfileAction, -// setProfile, -// } = useProfilesStore(); - -// const [pressedPin, setPressedPin] = useState(null); - -// const { t } = useTranslation(''); - -// useEffect(() => { -// fetchPins(); -// fetchProfiles(); -// }, []); - -// const saveAll = useCallback(() => { -// savePins(); -// saveProfiles(); -// }, [savePins, saveProfiles]); - -// return ( -// <> -//
-// -// Mapping buttons to pins that aren't connected or available can -// leave the device in non-functional state. To clear the invalid -// configuration go to the{' '} -// Reset Settings page. -// -//
-//
-// {t(`PinMapping:profile-pins-warning`)} -//
-//
-//

{t('PinMapping:sub-header-text')}

-//
-// setPressedPin(pin)} -// /> -//
-// {pressedPin !== null && ( -//
-// {t('PinMapping:pin-pressed', { pressedPin })} -//
-// )} - -// -// -// -// -// {/* {profiles.map((profilePins, profileIndex) => ( -// -// { -// setProfileAction(profileIndex, pin, action); -// }} -// onCopy={() => { -// setProfile(profileIndex, pins); -// }} -// /> -// -// ))} */} -// -//
-// -// ); -// } - export default function PinMapping() { - const { buttonLabels, updateUsedPins } = useContext(AppContext); const { fetchPins, pins, savePins, setPin } = usePinStore(); + const { fetchProfiles, profiles, saveProfiles, setProfileAction } = + useProfilesStore(); const [pressedPin, setPressedPin] = useState(null); const { t } = useTranslation(''); - const { buttonLabelType, swapTpShareLabels } = buttonLabels; - const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels); - const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']); - useEffect(() => { fetchPins(); + fetchProfiles(); + console.log('WTF'); }, []); - const isDisabled = useCallback( - (action: PinActionValues) => - isNonSelectable(action) && action !== BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO, + const onBaseChange = useCallback( + (pin: string) => + (selected: MultiValue | SingleValue) => { + // Handle clearing + if (!selected || (Array.isArray(selected) && !selected.length)) { + setPin(pin, { + action: BUTTON_ACTIONS.NONE, + customButtonMask: 0, + customDpadMask: 0, + }); + } else if (Array.isArray(selected) && selected.length > 1) { + const lastSelected = selected[selected.length - 1]; + // Revert to single option if choosing action type + if (lastSelected.type === 'action') { + setPin(pin, { + action: lastSelected.value, + customButtonMask: 0, + customDpadMask: 0, + }); + } else { + setPin( + pin, + selected.reduce( + (masks, option) => ({ + ...masks, + customButtonMask: + option.type === 'customButtonMask' + ? masks.customButtonMask ^ option.customButtonMask + : masks.customButtonMask, + customDpadMask: + option.type === 'customDpadMask' + ? masks.customDpadMask ^ option.customDpadMask + : masks.customDpadMask, + }), + { + action: BUTTON_ACTIONS.CUSTOM_BUTTON_COMBO, + customButtonMask: 0, + customDpadMask: 0, + }, + ), + ); + } + } else { + setPin(pin, { + action: selected[0].value, + customButtonMask: 0, + customDpadMask: 0, + }); + } + }, [], ); - const getOptionLabel = useCallback( - (option) => { - const labelKey = option.label?.split('BUTTON_PRESS_')?.pop(); - // Need to fallback as some button actions are not part of button names - return ( - (labelKey && buttonNames[labelKey]) || - t(`PinMapping:actions.${option.label}`) - ); - }, - [buttonNames], + const onProfileChange = useCallback( + (pin: string, profileIndex: number) => + (selected: MultiValue | SingleValue) => { + setProfileAction( + profileIndex, + pin, + selected?.value === undefined ? BUTTON_ACTIONS.NONE : selected.value, + ); + }, + [], ); return ( @@ -353,7 +298,7 @@ export default function PinMapping() {
- {/*

{t('PinMapping:sub-header-text')}

*/} +

{t('PinMapping:sub-header-text')}

-
-
- {Object.entries(pins).map(([key, pinData], index) => ( -
-
- -
- -
- ))} -
-
+
- profile-2 - profile-3 - profile-4 + + {profiles.map((profilePins, profileIndex) => ( + + onProfileChange(pin, profileIndex)} + onSave={saveProfiles} + isMulti={false} + /> + + ))}