Skip to content
This repository has been archived by the owner on May 17, 2023. It is now read-only.

Commit

Permalink
Ability to customize Player LEDs (PWM and RGB) (#66)
Browse files Browse the repository at this point in the history
* Fix jumpy page when open/closing picker

* Allow saving colors

* Allow gradient selection

* Fix bug when saving color without selection

* Save gradient color selections

* Fix ALL button resetting gradient values

* Update led theme label on backup page

* Create ColorPicker component

* Get PLED config working

* Fix build

* Fix label

* Fix custom validators and usedPins in AppContext

* Centralize usedPins array in frontend AppContext

* Finish up PLED interface

* Fix usedPins on dev server

* Logging for fallback requests on dev server

* Fix bug with post data for LED config

* Fix possible double submit on LED config page

* Fix blinky popover

* Add dynamic suggested start index for RGB PLEDs

* Remove intro text for PLED section
  • Loading branch information
FeralAI authored May 15, 2023
1 parent 08331e5 commit 4013eae
Show file tree
Hide file tree
Showing 17 changed files with 725 additions and 132 deletions.
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@hello-pangea/color-picker": "^3.2.2",
"axios": "^1.3.5",
"bootstrap": "^5.2.3",
"bootstrap-icons": "^1.10.5",
"crypto-js": "^4.1.1",
"formik": "^2.2.9",
"javascript-color-gradient": "^2.4.4",
Expand Down
11 changes: 11 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ app.use((req, res, next) => {
next();
});

app.get("/api/getUsedPins", (req, res) => {
return res.send({ usedPins: Object.values(picoController) });
})

app.get("/api/resetSettings", (req, res) => {
return res.send({ success: true });
});
Expand Down Expand Up @@ -119,6 +123,12 @@ app.get("/api/getLedOptions", (req, res) => {
A2: null,
},
usedPins: Object.values(picoController),
pledType: 1,
pledPin1: 12,
pledPin2: 13,
pledPin3: 14,
pledPin4: 15,
pledColor: 65280,
});
});

Expand Down Expand Up @@ -270,6 +280,7 @@ app.get("/api/getMemoryReport", (req, res) => {
});

app.post("/api/*", (req, res) => {
console.log(req.body);
return res.send(req.body);
});

Expand Down
2 changes: 2 additions & 0 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useContext, useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";

import { AppContextProvider } from './Contexts/AppContext';

import Navigation from './Components/Navigation'

import HomePage from './Pages/HomePage'
Expand Down
112 changes: 112 additions & 0 deletions src/Components/ColorPicker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, { useContext, useEffect, useState } from 'react';
import Button from 'react-bootstrap/Button';
import Col from 'react-bootstrap/Col';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import Overlay from 'react-bootstrap/Overlay';
import Popover from 'react-bootstrap/Popover';
import Row from 'react-bootstrap/Row';
import { SketchPicker } from '@hello-pangea/color-picker';

import { AppContext } from '../Contexts/AppContext';
import LEDColors from '../Data/LEDColors';

import './ColorPicker.scss';

const ledColors = LEDColors.map(c => ({ title: c.name, color: c.value}));
const customColors = (colors) => colors.map(c => ({ title: c, color: c }));

const ColorPicker = ({ types, onChange, onDismiss, pickerOnly, placement, show, target, title, ...props }) => {
const { savedColors, setSavedColors } = useContext(AppContext);
const [colorPalette, setColorPalette] = useState([...ledColors, ...customColors(savedColors)]);
const [colorTypes, setColorTypes] = useState(types);
const [selectedColor, setSelectedColor] = useState('#000000');
const [selectedColorType, setSelectedColorType] = useState(types[0]);

const deleteCurrentColor = () => {
const colorIndex = savedColors.indexOf(selectedColor);
if (colorIndex < 0)
return;

const newColors = [...savedColors];
newColors.splice(colorIndex, 1);
setSavedColors(newColors);
setColorPalette([...ledColors, ...customColors(newColors)]);
};

const saveCurrentColor = () => {
if (!selectedColor || colorPalette.filter(c => c.color === selectedColor).length > 0)
return;

const newColors = [...savedColors];
newColors.push(selectedColor);
setSavedColors(newColors);
setColorPalette([...ledColors, ...customColors(newColors)]);
};

const selectColor = (c, e) => {
if (onChange)
onChange(c.hex, e);

selectedColorType.value = c.hex;

setSelectedColor(c.hex);
setColorTypes(colorTypes);
};

useEffect(() => {
// Hide color picker when anywhere but picker is clicked
window.addEventListener('click', (e) => onDismiss(e));
setSelectedColorType(colorTypes[0]);
setSelectedColor(colorTypes[0].value);
}, []);

return (
<Overlay
show={show}
target={target}
placement={placement || 'bottom'}
container={this}
containerPadding={20}
>
<Popover onClick={(e) => e.stopPropagation()}>
<Container className="color-picker">
<h6 className="text-center">{title}</h6>
<Row>
{colorTypes.map(((o, i) =>
<Form.Group as={Col}
key={`colorType${i}`}
className={`${o === selectedColorType ? 'selected' : ''}`}
onClick={() => setSelectedColorType(o)}
>
{o.title && <Form.Label>{o.title}</Form.Label>}
<div
className={`color color-normal`}
style={{ backgroundColor: o.value }}
>
</div>
</Form.Group>
))}
</Row>
<Row>
<Col>
<SketchPicker
color={selectedColorType.value}
onChange={(c, e) => selectColor(c, e)}
disableAlpha={true}
presetColors={colorPalette}
width={180}
/>
</Col>
</Row>
<div className="button-group d-flex justify-content-between mt-2">
<Button size="sm" onClick={() => saveCurrentColor()}>Save Color</Button>
<Button size="sm" onClick={() => deleteCurrentColor()}>Delete Color</Button>
</div>
</Container>
</Popover>
</Overlay>
)
};

export default ColorPicker;
39 changes: 39 additions & 0 deletions src/Components/ColorPicker.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.color-picker {
user-select: none;
padding-bottom: 10px;

h6 {
margin-top: 8px;
}

.color-option {
justify-content: flex-start;
cursor: pointer;
font-size: 0.8rem;

label {
cursor: pointer;
margin: 0 auto 4px;
text-align: center;
width: 100%;
font-size: 0.85rem;
}

&.selected {
background-color: #DEDEDE;
}
}

.color {
display: flex;

height: 40px !important;
width: 40px !important;
margin: 0 auto 10px;
border: 1px solid black;
}

.sketch-picker {
margin-top: 10px;
}
}
6 changes: 3 additions & 3 deletions src/Components/FormControl.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import { Form } from 'react-bootstrap';

const FormControl = ({ label, error, groupClassName, ...props }) => {
const FormControl = ({ onClick, label, error, groupClassName, hidden, labelClassName, ...props }) => {
return (
<Form.Group className={groupClassName}>
<Form.Label>{label}</Form.Label>
<Form.Group className={groupClassName} onClick={onClick} hidden={hidden}>
<Form.Label className={labelClassName}>{label}</Form.Label>
<Form.Control {...props} />
<Form.Control.Feedback type="invalid">{error}</Form.Control.Feedback>
</Form.Group>
Expand Down
87 changes: 86 additions & 1 deletion src/Contexts/AppContext.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,64 @@
import React, { createContext, useState } from 'react';
import React, { createContext, useEffect, useState } from 'react';
import * as yup from 'yup';

import WebApi from '../Services/WebApi';

export const AppContext = createContext(null);

let checkPins = null;

yup.addMethod(yup.string, 'validateColor', function(this: yup.StringSchema, name) {
return this.test('', 'Valid hex color required', (value) => value?.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i));
});

yup.addMethod(yup.NumberSchema, 'validateSelectionWhenValue', function(this: yup.NumberSchema, name, choices) {
return this.when(name, {
is: value => !isNaN(parseInt(value)),
then: () => this.required().oneOf(choices.map(o => o.value)),
otherwise: () => yup.mixed().notRequired()
})
});

yup.addMethod(yup.NumberSchema, 'validateNumberWhenValue', function(this: yup.NumberSchema, name) {
return this.when(name, {
is: value => !isNaN(parseInt(value)),
then: () => this.required(),
otherwise: () => yup.mixed().notRequired().strip()
})
});

yup.addMethod(yup.NumberSchema, 'validateMinWhenEqualTo', function(this: yup.NumberSchema, name, compareValue, min) {
return this.when(name, {
is: value => parseInt(value) === compareValue,
then: () => this.required().min(min),
otherwise: () => yup.mixed().notRequired().strip()
})
});

yup.addMethod(yup.NumberSchema, 'validateRangeWhenValue', function(this: yup.NumberSchema, name, min, max) {
return this.when(name, {
is: value => !isNaN(parseInt(value)),
then: () => this.required().min(min).max(max),
otherwise: () => yup.mixed().notRequired().strip()
});
});

yup.addMethod(yup.NumberSchema, 'validatePinWhenEqualTo', function(this: yup.NumberSchema, name, compareName, compareValue) {
return this.when(compareName, {
is: value => parseInt(value) === compareValue,
then: () => this.validatePinWhenValue(name),
otherwise: () => yup.mixed().notRequired().strip()
})
});

yup.addMethod(yup.NumberSchema, 'validatePinWhenValue', function(this: yup.NumberSchema, name) {
return this.checkUsedPins();
});

yup.addMethod(yup.NumberSchema, 'checkUsedPins', function(this: yup.NumberSchema) {
return this.test('', '${originalValue} is unavailable/already assigned!', (value) => checkPins(value));
});

export const AppContextProvider = ({ children, ...props }) => {
const [buttonLabels, _setButtonLabels] = useState(localStorage.getItem('buttonLabels') || 'gp2040');
const setButtonLabels = (buttonLabels) => {
Expand Down Expand Up @@ -39,6 +96,31 @@ export const AppContextProvider = ({ children, ...props }) => {
_setGradientPressedColor1(gradientPressedColor2);
};

const [usedPins, setUsedPins] = useState([]);

const updateUsedPins = async () => {
const data = await WebApi.getUsedPins();
setUsedPins(data.usedPins);
console.log('usedPins updated:', data.usedPins);
return data;
};

useEffect(() => {
updateUsedPins();
}, []);

useEffect(() => {
checkPins = (value) => {
const hasValue = value > -1;
const isValid = value === undefined
|| value === -1
|| (hasValue && value < 30 && (usedPins || []).indexOf(value) === -1);
return isValid;
};
}, [usedPins, setUsedPins]);

console.log('usedPins:', usedPins);

return (
<AppContext.Provider
{...props}
Expand All @@ -49,12 +131,15 @@ export const AppContextProvider = ({ children, ...props }) => {
gradientPressedColor1,
gradientPressedColor2,
savedColors,
usedPins,
setButtonLabels,
setGradientNormalColor1,
setGradientNormalColor2,
setGradientPressedColor1,
setGradientPressedColor2,
setSavedColors,
setUsedPins,
updateUsedPins,
}}
>
{children}
Expand Down
Loading

0 comments on commit 4013eae

Please sign in to comment.