From 98d90b502af3574092f115f13a000ae2ab70f8c6 Mon Sep 17 00:00:00 2001 From: "Brian S. Stephan" Date: Mon, 3 Jul 2023 21:59:17 -0500 Subject: [PATCH] action button pin mapping profiles, part 3: start of UI page --- src/configs/webconfig.cpp | 32 ++--- www/server/app.js | 33 ++++++ www/src/App.jsx | 2 + www/src/Components/Navigation.jsx | 1 + www/src/Pages/ProfileSettings.jsx | 190 ++++++++++++++++++++++++++++++ www/src/Services/WebApi.js | 51 ++++++++ 6 files changed, 293 insertions(+), 16 deletions(-) create mode 100644 www/src/Pages/ProfileSettings.jsx diff --git a/src/configs/webconfig.cpp b/src/configs/webconfig.cpp index 72329c752..d74562ac5 100644 --- a/src/configs/webconfig.cpp +++ b/src/configs/webconfig.cpp @@ -510,14 +510,14 @@ std::string setProfileOptions() JsonArray alts = options["alternativePinMappings"]; int altsIndex = 0; for (JsonObject alt : alts) { - profileOptions.alternativePinMappings[altsIndex].pinButtonB1 = alt["pinButtonB1"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonB2 = alt["pinButtonB2"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonB3 = alt["pinButtonB3"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonB4 = alt["pinButtonB4"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonL1 = alt["pinButtonL1"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonR1 = alt["pinButtonR1"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonL2 = alt["pinButtonL2"].as(); - profileOptions.alternativePinMappings[altsIndex].pinButtonR2 = alt["pinButtonR2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB1 = alt["B1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB2 = alt["B2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB3 = alt["B3"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonB4 = alt["B4"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonL1 = alt["L1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonR1 = alt["R1"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonL2 = alt["L2"].as(); + profileOptions.alternativePinMappings[altsIndex].pinButtonR2 = alt["R2"].as(); profileOptions.alternativePinMappings_count = ++altsIndex; if (altsIndex > 2) break; } @@ -534,14 +534,14 @@ std::string getProfileOptions() JsonArray alts = doc.createNestedArray("alternativePinMappings"); for (int i = 0; i < profileOptions.alternativePinMappings_count; i++) { JsonObject altMappings = alts.createNestedObject(); - altMappings["pinButtonB1"] = profileOptions.alternativePinMappings[i].pinButtonB1; - altMappings["pinButtonB2"] = profileOptions.alternativePinMappings[i].pinButtonB2; - altMappings["pinButtonB3"] = profileOptions.alternativePinMappings[i].pinButtonB3; - altMappings["pinButtonB4"] = profileOptions.alternativePinMappings[i].pinButtonB4; - altMappings["pinButtonL1"] = profileOptions.alternativePinMappings[i].pinButtonL1; - altMappings["pinButtonR1"] = profileOptions.alternativePinMappings[i].pinButtonR1; - altMappings["pinButtonL2"] = profileOptions.alternativePinMappings[i].pinButtonL2; - altMappings["pinButtonR2"] = profileOptions.alternativePinMappings[i].pinButtonR2; + altMappings["B1"] = profileOptions.alternativePinMappings[i].pinButtonB1; + altMappings["B2"] = profileOptions.alternativePinMappings[i].pinButtonB2; + altMappings["B3"] = profileOptions.alternativePinMappings[i].pinButtonB3; + altMappings["B4"] = profileOptions.alternativePinMappings[i].pinButtonB4; + altMappings["L1"] = profileOptions.alternativePinMappings[i].pinButtonL1; + altMappings["R1"] = profileOptions.alternativePinMappings[i].pinButtonR1; + altMappings["L2"] = profileOptions.alternativePinMappings[i].pinButtonL2; + altMappings["R2"] = profileOptions.alternativePinMappings[i].pinButtonR2; } return serialize_json(doc); diff --git a/www/server/app.js b/www/server/app.js index f707e0cff..76fa76479 100644 --- a/www/server/app.js +++ b/www/server/app.js @@ -227,6 +227,39 @@ app.get("/api/getKeyMappings", (req, res) => res.send(mapValues(DEFAULT_KEYBOARD_MAPPING)) ); +app.get("/api/getProfileOptions", (req, res) => { + return res.send({ + alternativePinMappings: [{ + B1: 12, + B2: 8, + B3: 11, + B4: 10, + L1: 9, + R1: 5, + L2: 7, + R2: 6 + },{ + B1: 12, + B2: 11, + B3: 10, + B4: 9, + L1: 8, + R1: 6, + L2: 7, + R2: 5 + },{ + B1: 9, + B2: 8, + B3: 7, + B4: 6, + L1: 12, + R1: 11, + L2: 10, + R2: 9 + }] + }); +}); + app.get("/api/getAddonsOptions", (req, res) => { return res.send({ turboPin: -1, diff --git a/www/src/App.jsx b/www/src/App.jsx index d040982df..b6f646ab9 100644 --- a/www/src/App.jsx +++ b/www/src/App.jsx @@ -7,6 +7,7 @@ import Navigation from './Components/Navigation' import HomePage from './Pages/HomePage' import PinMappingPage from "./Pages/PinMapping"; +import ProfileSettingsPage from "./Pages/ProfileSettings"; import KeyboardMappingPage from "./Pages/KeyboardMapping"; import ResetSettingsPage from './Pages/ResetSettingsPage'; import SettingsPage from './Pages/SettingsPage'; @@ -31,6 +32,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/www/src/Components/Navigation.jsx b/www/src/Components/Navigation.jsx index f0eaa75d5..b8fb89678 100644 --- a/www/src/Components/Navigation.jsx +++ b/www/src/Components/Navigation.jsx @@ -51,6 +51,7 @@ const Navigation = (props) => { {t('Navigation:pin-mapping-label')} {t('Navigation:keyboard-mapping-label')} + {t('Navigation:profile-settings-label')} {t('Navigation:led-config-label')} {t('Navigation:custom-theme-label')} {t('Navigation:display-config-label')} diff --git a/www/src/Pages/ProfileSettings.jsx b/www/src/Pages/ProfileSettings.jsx new file mode 100644 index 000000000..4b801aa71 --- /dev/null +++ b/www/src/Pages/ProfileSettings.jsx @@ -0,0 +1,190 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { NavLink } from "react-router-dom"; +import { Button, Form } from 'react-bootstrap'; +import { AppContext } from '../Contexts/AppContext'; +import Section from '../Components/Section'; +import WebApi, { baseProfileOptions } from '../Services/WebApi'; +import boards from '../Data/Boards.json'; +import { BUTTONS } from '../Data/Buttons'; +import './PinMappings.scss'; +import { Trans, useTranslation } from 'react-i18next'; + +const requiredButtons = ['S2']; +const errorType = { + required: 'errors.required', + conflict: 'errors.conflict', + invalid: 'errors.invalid', + used: 'errors.used', +}; + +export default function ProfileOptionsPage() { + const { buttonLabels, setButtonLabels, usedPins, updateUsedPins } = useContext(AppContext); + const [validated, setValidated] = useState(false); + const [saveMessage, setSaveMessage] = useState(''); + const [profileOptions, setProfileOptions] = useState(baseProfileOptions); + const [selectedController] = useState(import.meta.env.VITE_GP2040_CONTROLLER); + const [selectedBoard] = useState(import.meta.env.VITE_GP2040_BOARD); + const { buttonLabelType } = buttonLabels; + const { setLoading } = useContext(AppContext); + + const { t } = useTranslation(''); + + const translatedErrorType = Object.keys(errorType).reduce((a, k) => ({ ...a, [k]: t(`ProfileOptions:${errorType[k]}`) }), {}); + + useEffect(() => { + async function fetchData() { + setProfileOptions(await WebApi.getProfileOptions(setLoading)); + setButtonLabels(); + } + + fetchData(); + }, [setProfileOptions, selectedController]); + + const handlePinChange = (e, index, key) => { + const newProfileOptions = { ...profileOptions }; + if (e.target.value) + newProfileOptions['alternativePinMappings'][index][key].pin = parseInt(e.target.value); + else + newProfileOptions['alternativePinMappings'][index][key].pin = ''; + + validateMappings(newProfileOptions); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + e.stopPropagation(); + + let mappings = { ...profileOptions }; + validateMappings(mappings); + + if (Object.keys(mappings).filter(p => mappings[p].error).length > 0) { + setSaveMessage(t('Common:errors.validation-error')); + return; + } + + const success = await WebApi.setProfileOptions(mappings); + if (success) + updateUsedPins(); + setSaveMessage(success ? t('Common:saved-success-message') : t('Common:saved-error-message')); + }; + + const validateMappings = (mappings) => { + profileOptions['alternativePinMappings'].forEach((altMappings) => { + const buttons = Object.keys(altMappings); + + // Create some mapped pin groups for easier error checking + const mappedPins = buttons + .filter(p => altMappings[p].pin > -1) + .reduce((a, p) => { + a.push(altMappings[p].pin); + return a; + }, []); + const mappedPinCounts = mappedPins.reduce((a, p) => ({ ...a, [p]: (a[p] || 0) + 1 }), {}); + const uniquePins = mappedPins.filter((p, i, a) => a.indexOf(p) === i); + const conflictedPins = Object.keys(mappedPinCounts).filter(p => mappedPinCounts[p] > 1).map(parseInt); + const invalidPins = uniquePins.filter(p => boards[selectedBoard].invalidPins.indexOf(p) > -1); + const otherPins = usedPins.filter(p => uniquePins.indexOf(p) === -1); + + for (let button of buttons) { + altMappings[button].error = ''; + + // Validate required button + if ((altMappings[button].pin < boards[selectedBoard].minPin || altMappings[button].pin > boards[selectedBoard].maxPin) && requiredButtons.filter(b => b === button).length) + altMappings[button].error = translatedErrorType.required; + + // Identify conflicted pins + else if (conflictedPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.conflict; + + // Identify invalid pin assignments + else if (invalidPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.invalid; + + // Identify used pins + else if (otherPins.indexOf(altMappings[button].pin) > -1) + altMappings[button].error = translatedErrorType.used; + } + }); + + setProfileOptions(mappings); + setValidated(true); + }; + + const pinCell = (profile, button) => { + return + handlePinChange(e, profile, button)} + > + + {renderError(profile, button)} + + + } + + const renderError = (index, button) => { + if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.required) { + return {t('ProfileOptions:errors.required', { + button: BUTTONS[buttonLabelType][button] + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.conflict) { + const conflictedMappings = Object.keys(profileOptions) + .filter(b => b !== button) + .filter(b => profileOptions[b].pin === profileOptions['alternativePinMappings'][index][button].pin) + .map(b => BUTTONS[buttonLabelType][b]); + + return {t('ProfileOptions:errors.conflict', { + pin: profileOptions['alternativePinMappings'][index][button].pin, + conflictedMappings: conflictedMappings.join(', '), + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.invalid) { + console.log(profileOptions['alternativePinMappings'][index][button].pin); + return {t('ProfileOptions:errors.invalid', { + pin: profileOptions['alternativePinMappings'][index][button].pin + })}; + } + else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.used) { + return {t('ProfileOptions:errors.used', { + pin: profileOptions['alternativePinMappings'][index][button].pin + })}; + } + + return <>; + }; + + return ( +
+

{t('ProfileOptions:profile-pins-desc')}

+
B3 B4 R1 L1
+ B1 B2 R2 L2
+
+ + + + + + + + {console.log(Object.keys(profileOptions['alternativePinMappings'][0]))} + {Object.keys(profileOptions['alternativePinMappings'][0]).map((key) => ( + + + {pinCell(0, key)} + {pinCell(1, key)} + {pinCell(2, key)} + + ))} +
{BUTTONS[buttonLabelType].label}{t('ProfileOptions:profile')} 2{t('ProfileOptions:profile')} 3{t('ProfileOptions:profile')} 4
{BUTTONS[buttonLabelType][key]}
+ + {saveMessage ? {saveMessage} : null} +
+
+ ); +} diff --git a/www/src/Services/WebApi.js b/www/src/Services/WebApi.js index a6187f41d..3cb1b92ab 100644 --- a/www/src/Services/WebApi.js +++ b/www/src/Services/WebApi.js @@ -25,6 +25,37 @@ export const baseButtonMappings = { Fn: { pin: -1, key: 0, error: null }, }; +export const baseProfileOptions = { + alternativePinMappings: [{ + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + },{ + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + },{ + B1: { pin: -1, key: 0, error: null }, + B2: { pin: -1, key: 0, error: null }, + B3: { pin: -1, key: 0, error: null }, + B4: { pin: -1, key: 0, error: null }, + L1: { pin: -1, key: 0, error: null }, + R1: { pin: -1, key: 0, error: null }, + L2: { pin: -1, key: 0, error: null }, + R2: { pin: -1, key: 0, error: null }, + }] +}; + async function resetSettings() { return axios.get(`${baseUrl}/api/resetSettings`) .then((response) => response.data) @@ -235,6 +266,25 @@ async function setPinMappings(mappings) { }); } +async function getProfileOptions(setLoading) { + setLoading(true); + + try { + const response = await axios.get(`${baseUrl}/api/getProfileOptions`); + let profileOptions = { ...baseProfileOptions }; + response.data['alternativePinMappings'].forEach((altButtons, index) => { + for (let prop of Object.keys(altButtons)) + profileOptions['alternativePinMappings'][index][prop].pin = parseInt( + response.data['alternativePinMappings'][index][prop] + ); + }); + return profileOptions; + } catch (error) { + console.error(error); + return false; + } +} + async function getKeyMappings(setLoading) { setLoading(true); @@ -385,6 +435,7 @@ const WebApi = { setCustomTheme, getPinMappings, setPinMappings, + getProfileOptions, getKeyMappings, setKeyMappings, getAddonsOptions,