diff --git a/www/server/app.js b/www/server/app.js index 94a40af69..1e2d2dfdd 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -390,7 +390,6 @@ app.get('/api/getProfileOptions', (req, res) => { alternativePinMappings: [ createPinMappings({ profileLabel: 'Profile 2' }), createPinMappings({ profileLabel: 'Profile 3' }), - createPinMappings({ profileLabel: 'Profile 4' }), ], }); }); diff --git a/www/src/Locales/en/PinMapping.jsx b/www/src/Locales/en/PinMapping.jsx index bd8450d93..4d0bef4d3 100644 --- a/www/src/Locales/en/PinMapping.jsx +++ b/www/src/Locales/en/PinMapping.jsx @@ -12,6 +12,10 @@ export default { 'Max 16 characters. Letters, numbers, and spaces allowed.', 'profile-pin-mapping-title': '{{profileLabel}} - Pin Mapping', 'profile-label-default': 'Profile {{profileNumber}}', + 'profile-add-button': '+ Add Profile', + 'profile-disabled': 'Disabled', + 'profile-enabled-tooltip': + 'Disabled profiles will not be available when using hotkeys to change profile.', 'profile-pins-warning': 'Try to avoid changing the buttons and/or directions used for the switch profile hotkeys. Otherwise, it will be difficult to understand what profile is being selected!', 'profile-copy-base': 'Copy base profile', diff --git a/www/src/Pages/PinMapping.tsx b/www/src/Pages/PinMapping.tsx index 3b088090a..82419fd9b 100644 --- a/www/src/Pages/PinMapping.tsx +++ b/www/src/Pages/PinMapping.tsx @@ -7,13 +7,24 @@ import React, { } from 'react'; import { NavLink } from 'react-router-dom'; import { useShallow } from 'zustand/react/shallow'; -import { Alert, Button, Col, Form, Nav, Row, Tab } from 'react-bootstrap'; +import { + Alert, + Button, + Col, + Form, + FormCheck, + Nav, + OverlayTrigger, + Row, + Tab, + Tooltip, +} from 'react-bootstrap'; import { Trans, useTranslation } from 'react-i18next'; import invert from 'lodash/invert'; import omit from 'lodash/omit'; import { AppContext } from '../Contexts/AppContext'; -import useProfilesStore from '../Store/useProfilesStore'; +import useProfilesStore, { MAX_PROFILES } from '../Store/useProfilesStore'; import Section from '../Components/Section'; import CustomSelect from '../Components/CustomSelect'; @@ -23,6 +34,7 @@ import { BUTTON_MASKS, DPAD_MASKS, getButtonLabels } from '../Data/Buttons'; import { BUTTON_ACTIONS, PinActionValues } from '../Data/Pins'; import './PinMapping.scss'; import { MultiValue, SingleValue } from 'react-select'; +import InfoCircle from '../Icons/InfoCircle'; type OptionType = { label: string; @@ -141,8 +153,11 @@ const PinSelectList = memo(function PinSelectList({ profileIndex: number; }) { const setProfilePin = useProfilesStore((state) => state.setProfilePin); + const pins = useProfilesStore( - useShallow((state) => omit(state.profiles[profileIndex], 'profileLabel')), + useShallow((state) => + omit(state.profiles[profileIndex], ['profileLabel', 'enabled']), + ), ); const { t } = useTranslation(''); const { buttonLabels } = useContext(AppContext); @@ -242,11 +257,18 @@ const PinSection = memo(function PinSection({ const copyBaseProfile = useProfilesStore((state) => state.copyBaseProfile); const setProfilePin = useProfilesStore((state) => state.setProfilePin); const saveProfiles = useProfilesStore((state) => state.saveProfiles); + const toggleProfileEnabled = useProfilesStore( + (state) => state.toggleProfileEnabled, + ); + const enabled = useProfilesStore( + (state) => state.profiles[profileIndex].enabled, + ); const profileLabel = useProfilesStore((state) => state.profiles[profileIndex].profileLabel) || t('PinMapping:profile-label-default', { profileNumber: profileIndex + 1, }); + const { updateUsedPins, buttonLabels } = useContext(AppContext); const { buttonLabelType, swapTpShareLabels } = buttonLabels; const CURRENT_BUTTONS = getButtonLabels(buttonLabelType, swapTpShareLabels); @@ -285,7 +307,36 @@ const PinSection = memo(function PinSection({ })} >
- +
+ + {profileIndex > 0 && ( +
+ + {t('PinMapping:profile-enabled-tooltip')} + + } + > +
+ + +
+ + } + type="switch" + reverse + checked={!enabled} + onChange={() => { + toggleProfileEnabled(profileIndex); + }} + /> +
+ )} +

@@ -328,7 +379,10 @@ const PinSection = memo(function PinSection({ export default function PinMapping() { const fetchProfiles = useProfilesStore((state) => state.fetchProfiles); + const addProfile = useProfilesStore((state) => state.addProfile); const profiles = useProfilesStore((state) => state.profiles); + const loadingProfiles = useProfilesStore((state) => state.loadingProfiles); + const [pressedPin, setPressedPin] = useState(null); const { t } = useTranslation(''); @@ -340,17 +394,36 @@ export default function PinMapping() { + {loadingProfiles && ( +
+ +
+ )}

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

@@ -369,18 +442,11 @@ export default function PinMapping() { - - - - - - - - - - - - + {profiles.map((_, index) => ( + + + + ))}
diff --git a/www/src/Store/useProfilesStore.ts b/www/src/Store/useProfilesStore.ts index a666ef80c..6ccc591bd 100644 --- a/www/src/Store/useProfilesStore.ts +++ b/www/src/Store/useProfilesStore.ts @@ -1,6 +1,9 @@ import { create } from 'zustand'; import WebApi from '../Services/WebApi'; -import { BUTTON_ACTIONS, PinActionValues } from '../Data/Pins'; +import { PinActionValues } from '../Data/Pins'; + +// Max number of profiles that can be created, including the base profile +export const MAX_PROFILES = 4; type CustomMasks = { customButtonMask: number; @@ -43,6 +46,7 @@ export type PinsType = { pin28: MaskPayload; pin29: MaskPayload; profileLabel: string; + enabled: boolean; }; type State = { @@ -57,65 +61,37 @@ export type SetProfilePinType = ( ) => void; type Actions = { - fetchProfiles: () => void; - setProfilePin: SetProfilePinType; + addProfile: () => void; copyBaseProfile: (profileIndex: number) => void; - setProfileLabel: (profileIndex: number, profileLabel: string) => void; + fetchProfiles: () => void; saveProfiles: () => Promise; -}; - -const DEFAULT_PIN_STATE = { - action: BUTTON_ACTIONS.NONE, - customButtonMask: 0, - customDpadMask: 0, -}; - -const defaultProfilePins: PinsType = { - pin00: DEFAULT_PIN_STATE, - pin01: DEFAULT_PIN_STATE, - pin02: DEFAULT_PIN_STATE, - pin03: DEFAULT_PIN_STATE, - pin04: DEFAULT_PIN_STATE, - pin05: DEFAULT_PIN_STATE, - pin06: DEFAULT_PIN_STATE, - pin07: DEFAULT_PIN_STATE, - pin08: DEFAULT_PIN_STATE, - pin09: DEFAULT_PIN_STATE, - pin10: DEFAULT_PIN_STATE, - pin11: DEFAULT_PIN_STATE, - pin12: DEFAULT_PIN_STATE, - pin13: DEFAULT_PIN_STATE, - pin14: DEFAULT_PIN_STATE, - pin15: DEFAULT_PIN_STATE, - pin16: DEFAULT_PIN_STATE, - pin17: DEFAULT_PIN_STATE, - pin18: DEFAULT_PIN_STATE, - pin19: DEFAULT_PIN_STATE, - pin20: DEFAULT_PIN_STATE, - pin21: DEFAULT_PIN_STATE, - pin22: DEFAULT_PIN_STATE, - pin23: DEFAULT_PIN_STATE, - pin24: DEFAULT_PIN_STATE, - pin25: DEFAULT_PIN_STATE, - pin26: DEFAULT_PIN_STATE, - pin27: DEFAULT_PIN_STATE, - pin28: DEFAULT_PIN_STATE, - pin29: DEFAULT_PIN_STATE, - profileLabel: '', + setProfileLabel: (profileIndex: number, profileLabel: string) => void; + setProfilePin: SetProfilePinType; + toggleProfileEnabled: (profileIndex: number) => void; }; const INITIAL_STATE: State = { profiles: [ - defaultProfilePins, - defaultProfilePins, - defaultProfilePins, - defaultProfilePins, + // Profiles will be populated dynamically ], loadingProfiles: false, }; const useProfilesStore = create()((set, get) => ({ ...INITIAL_STATE, + addProfile: () => { + if (get().profiles.length < MAX_PROFILES) { + set((state) => ({ + profiles: [ + ...state.profiles, + { + ...state.profiles[0], + profileLabel: `Profile ${state.profiles.length + 1}`, + }, + ], + })); + } + }, fetchProfiles: async () => { set({ loadingProfiles: true }); @@ -172,6 +148,15 @@ const useProfilesStore = create()((set, get) => ({ WebApi.setProfileOptions(profiles), ]); }, + toggleProfileEnabled: (profileIndex) => + set((state) => { + const profiles = [...state.profiles]; + profiles[profileIndex] = { + ...profiles[profileIndex], + enabled: !profiles[profileIndex].enabled, + }; + return { ...state, profiles }; + }), })); export default useProfilesStore;