Skip to content

Commit

Permalink
action button pin mapping profiles, part 3: start of UI page
Browse files Browse the repository at this point in the history
  • Loading branch information
bsstephan committed Jul 4, 2023
1 parent 31a0021 commit 98d90b5
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 16 deletions.
32 changes: 16 additions & 16 deletions src/configs/webconfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB2 = alt["pinButtonB2"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB3 = alt["pinButtonB3"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB4 = alt["pinButtonB4"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonL1 = alt["pinButtonL1"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonR1 = alt["pinButtonR1"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonL2 = alt["pinButtonL2"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonR2 = alt["pinButtonR2"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB1 = alt["B1"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB2 = alt["B2"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB3 = alt["B3"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonB4 = alt["B4"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonL1 = alt["L1"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonR1 = alt["R1"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonL2 = alt["L2"].as<int>();
profileOptions.alternativePinMappings[altsIndex].pinButtonR2 = alt["R2"].as<int>();
profileOptions.alternativePinMappings_count = ++altsIndex;
if (altsIndex > 2) break;
}
Expand All @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions www/server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions www/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +32,7 @@ const App = () => {
<Route path="/settings" element={<SettingsPage />} />
<Route path="/pin-mapping" element={<PinMappingPage />} />
<Route path="/keyboard-mapping" element={<KeyboardMappingPage />} />
<Route path="/profile-settings" element={<ProfileSettingsPage />} />
<Route path="/reset-settings" element={<ResetSettingsPage />} />
<Route path="/led-config" element={<LEDConfigPage />} />
<Route path="/custom-theme" element={<CustomThemePage />} />
Expand Down
1 change: 1 addition & 0 deletions www/src/Components/Navigation.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const Navigation = (props) => {
<NavDropdown title={t('Navigation:config-label')}>
<NavDropdown.Item as={NavLink} exact="true" to="/pin-mapping">{t('Navigation:pin-mapping-label')}</NavDropdown.Item>
<NavDropdown.Item as={NavLink} exact="true" to="/keyboard-mapping">{t('Navigation:keyboard-mapping-label')}</NavDropdown.Item>
<NavDropdown.Item as={NavLink} exact="true" to="/profile-settings">{t('Navigation:profile-settings-label')}</NavDropdown.Item>
<NavDropdown.Item as={NavLink} exact="true" to="/led-config">{t('Navigation:led-config-label')}</NavDropdown.Item>
<NavDropdown.Item as={NavLink} exact="true" to="/custom-theme">{t('Navigation:custom-theme-label')}</NavDropdown.Item>
<NavDropdown.Item as={NavLink} exact="true" to="/display-config">{t('Navigation:display-config-label')}</NavDropdown.Item>
Expand Down
190 changes: 190 additions & 0 deletions www/src/Pages/ProfileSettings.jsx
Original file line number Diff line number Diff line change
@@ -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 <td>
<Form.Control
type="number"
className="pin-input form-control-sm"
value={profileOptions['alternativePinMappings'][profile][button].pin}
min={-1}
max={boards[selectedBoard].maxPin}
isInvalid={profileOptions['alternativePinMappings'][profile][button].error}
onChange={(e) => handlePinChange(e, profile, button)}
></Form.Control>
<Form.Control.Feedback type="invalid">
{renderError(profile, button)}
</Form.Control.Feedback>
</td>
}

const renderError = (index, button) => {
if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.required) {
return <span key="required" className="error-message">{t('ProfileOptions:errors.required', {
button: BUTTONS[buttonLabelType][button]
})}</span>;
}
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 <span key="conflict" className="error-message">{t('ProfileOptions:errors.conflict', {
pin: profileOptions['alternativePinMappings'][index][button].pin,
conflictedMappings: conflictedMappings.join(', '),
})}</span>;
}
else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.invalid) {
console.log(profileOptions['alternativePinMappings'][index][button].pin);
return <span key="invalid" className="error-message">{t('ProfileOptions:errors.invalid', {
pin: profileOptions['alternativePinMappings'][index][button].pin
})}</span>;
}
else if (profileOptions['alternativePinMappings'][index][button].error === translatedErrorType.used) {
return <span key="used" className="error-message">{t('ProfileOptions:errors.used', {
pin: profileOptions['alternativePinMappings'][index][button].pin
})}</span>;
}

return <></>;
};

return (
<Section title={t('ProfileOptions:header-text')}>
<p>{t('ProfileOptions:profile-pins-desc')}</p>
<pre>B3 B4 R1 L1<br />
B1 B2 R2 L2</pre>
<Form noValidate validated={validated} onSubmit={handleSubmit}>
<table className="table table-sm pin-mapping-table">
<tr>
<th className="table-header-button-label">{BUTTONS[buttonLabelType].label}</th>
<th>{t('ProfileOptions:profile')} 2</th>
<th>{t('ProfileOptions:profile')} 3</th>
<th>{t('ProfileOptions:profile')} 4</th>
</tr>
{console.log(Object.keys(profileOptions['alternativePinMappings'][0]))}
{Object.keys(profileOptions['alternativePinMappings'][0]).map((key) => (
<tr key={key}>
<td>{BUTTONS[buttonLabelType][key]}</td>
{pinCell(0, key)}
{pinCell(1, key)}
{pinCell(2, key)}
</tr>
))}
</table>
<Button type="submit">{t('Common:button-save-label')}</Button>
{saveMessage ? <span className="alert">{saveMessage}</span> : null}
</Form>
</Section>
);
}
51 changes: 51 additions & 0 deletions www/src/Services/WebApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -385,6 +435,7 @@ const WebApi = {
setCustomTheme,
getPinMappings,
setPinMappings,
getProfileOptions,
getKeyMappings,
setKeyMappings,
getAddonsOptions,
Expand Down

0 comments on commit 98d90b5

Please sign in to comment.