diff --git a/www/src/Components/CaptureButton.tsx b/www/src/Components/CaptureButton.tsx index d1c62ab2b..5828361ef 100644 --- a/www/src/Components/CaptureButton.tsx +++ b/www/src/Components/CaptureButton.tsx @@ -94,7 +94,7 @@ const CaptureButton = ({ {hasNext && ( - skipButton()}> + skipButton()}> {t('CaptureButton:capture-button-modal-skip')} )} @@ -103,7 +103,7 @@ const CaptureButton = ({ - setTriggerCapture(true)}> + setTriggerCapture(true)}> {small ? '🎮' : `${ diff --git a/www/src/Pages/PinMapping.tsx b/www/src/Pages/PinMapping.tsx index 5cb8cd132..c4530b5d4 100644 --- a/www/src/Pages/PinMapping.tsx +++ b/www/src/Pages/PinMapping.tsx @@ -6,7 +6,7 @@ import invert from 'lodash/invert'; import omit from 'lodash/omit'; import { AppContext } from '../Contexts/AppContext'; -import usePinStore, { MaskPayload } from '../Store/usePinStore'; +import usePinStore from '../Store/usePinStore'; import useProfilesStore from '../Store/useProfilesStore'; import Section from '../Components/Section'; @@ -16,7 +16,7 @@ import CaptureButton from '../Components/CaptureButton'; import { BUTTON_MASKS, DPAD_MASKS, getButtonLabels } from '../Data/Buttons'; import { BUTTON_ACTIONS, PinActionValues } from '../Data/Pins'; import './PinMapping.scss'; -import { ActionMeta, MultiValue, SingleValue } from 'react-select'; +import { MultiValue, SingleValue } from 'react-select'; type OptionType = { label: string; @@ -26,19 +26,10 @@ type OptionType = { customDpadMask: number; }; -type PinSectionType = { +type ProfilePinSectionType = { title: string; - pins: { [key: string]: MaskPayload | PinActionValues }; - onChange: ( - pin: string, - ) => - | (( - newValue: MultiValue | SingleValue, - actionMeta: ActionMeta, - ) => void) - | undefined; - onSave: () => Promise; - isMulti?: boolean; + profilePins: { [key: string]: PinActionValues }; + profileIndex: number; }; const disabledOptions = [ @@ -92,7 +83,7 @@ const groupedOptions = [ }, ]; -const getMultiValue = (pinData: MaskPayload) => { +const getMultiValue = (pinData) => { if (pinData.action === BUTTON_ACTIONS.NONE) return; if (isDisabled(pinData.action)) { const actionKey = invert(BUTTON_ACTIONS)[pinData.action]; @@ -110,18 +101,29 @@ const getMultiValue = (pinData: MaskPayload) => { : options.filter((option) => option.value === pinData.action); }; -const getSingleValue = (pinData: PinActionValues) => { +const getSingleValue = (pinData) => { const actionKey = invert(BUTTON_ACTIONS)[pinData]; return { label: actionKey, value: pinData }; }; -const PinSection = ({ - title, - pins, - onChange, - onSave, - isMulti = true, -}: PinSectionType) => { +const PinMappingWarning = () => { + const { t } = useTranslation(''); + 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`)} + + ); +}; +const BasePinSection = () => { + const { pins, savePins, setPin } = usePinStore(); const { buttonLabels, updateUsedPins } = useContext(AppContext); const { t } = useTranslation(''); const [saveMessage, setSaveMessage] = useState(''); @@ -130,6 +132,59 @@ const PinSection = ({ const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels); const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']); + const onChange = 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: OptionType) => { const labelKey = option.label?.split('BUTTON_PRESS_')?.pop(); @@ -146,7 +201,7 @@ const PinSection = ({ e.preventDefault(); e.stopPropagation(); try { - await onSave(); + await savePins(); updateUsedPins(); setSaveMessage(t('Common:saved-success-message')); } catch (error) { @@ -156,18 +211,8 @@ const PinSection = ({ 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`)} - - + + {Object.entries(pins).map(([pin, pinData], index) => ( @@ -180,19 +225,35 @@ const PinSection = ({ ))} - + + setPin( + // Convert getHeldPins format to setPinMappings format + pin < 10 ? `pin0${pin}` : `pin${pin}`, + { + // Maps current mode buttons to actions + action: + BUTTON_ACTIONS[ + `BUTTON_PRESS_${invert(buttonNames)[label].toUpperCase()}` + ], + customButtonMask: 0, + customDpadMask: 0, + }, + ) + } + /> + {t('Common:button-save-label')} {saveMessage && {saveMessage}} @@ -201,74 +262,24 @@ const PinSection = ({ > ); }; -export default function PinMapping() { - const { fetchPins, pins, savePins, setPin } = usePinStore(); - const { fetchProfiles, profiles, saveProfiles, setProfileAction } = - useProfilesStore(); - const [pressedPin, setPressedPin] = useState(null); - const { t } = useTranslation(''); - useEffect(() => { - fetchPins(); - fetchProfiles(); - }, []); +const ProfilePinSection = ({ + title, + profilePins, + profileIndex, +}: ProfilePinSectionType) => { + const { saveProfiles, setProfileAction } = useProfilesStore(); + const { buttonLabels, updateUsedPins } = useContext(AppContext); + const { t } = useTranslation(''); + const [saveMessage, setSaveMessage] = useState(''); - 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 { buttonLabelType, swapTpShareLabels } = buttonLabels; + const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels); + const buttonNames = omit(CURRENT_BUTTONS, ['label', 'value']); - const onProfileChange = useCallback( + const onChange = useCallback( (pin: string, profileIndex: number) => - (selected: MultiValue | SingleValue) => { + (selected: SingleValue) => { setProfileAction( profileIndex, pin, @@ -278,6 +289,90 @@ export default function PinMapping() { [], ); + 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 saveProfiles(); + updateUsedPins(); + setSaveMessage(t('Common:saved-success-message')); + } catch (error) { + setSaveMessage(t('Common:saved-error-message')); + } + }, []); + + return ( + <> + + + + + {Object.entries(profilePins).map(([pin, pinData], index) => ( + + + {pin.toUpperCase()} + + + + ))} + + + setProfileAction( + profileIndex, + // Convert getHeldPins format to setPinMappings format + pin < 10 ? `pin0${pin}` : `pin${pin}`, + // Maps current mode buttons to actions + BUTTON_ACTIONS[ + `BUTTON_PRESS_${invert(buttonNames)[label].toUpperCase()}` + ], + ) + } + /> + + {t('Common:button-save-label')} + + {saveMessage && {saveMessage}} + + + > + ); +}; + +export default function PinMapping() { + const { fetchPins } = usePinStore(); + const { fetchProfiles, profiles } = useProfilesStore(); + const [pressedPin, setPressedPin] = useState(null); + const { t } = useTranslation(''); + + useEffect(() => { + fetchPins(); + fetchProfiles(); + }, []); + return ( @@ -314,12 +409,7 @@ export default function PinMapping() { - + {profiles.map((profilePins, profileIndex) => ( @@ -327,12 +417,10 @@ export default function PinMapping() { key={`profile-${profileIndex + 2}`} eventKey={`profile-${profileIndex + 2}`} > - onProfileChange(pin, profileIndex)} - onSave={saveProfiles} - isMulti={false} + profilePins={profilePins} + profileIndex={profileIndex} /> ))}