diff --git a/package-lock.json b/package-lock.json index b4b0250..fcc75ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "bootstrap": "^5.2.3", "crypto-js": "^4.1.1", "formik": "^2.2.9", + "javascript-color-gradient": "^2.4.4", "jsbn": "^1.1.0", "jsencrypt": "^3.3.2", "lodash": "^4.17.21", @@ -9927,6 +9928,11 @@ "node": ">=10" } }, + "node_modules/javascript-color-gradient": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/javascript-color-gradient/-/javascript-color-gradient-2.4.4.tgz", + "integrity": "sha512-kbt3Y1eltW4X789P1eQzCUEB5X7hFmLNpQT4bgDFMtj/cW2v+j8ms/rLSR7jndZUAP6yya4vyeZK6bRqh6yClA==" + }, "node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", diff --git a/package.json b/package.json index 877ea76..1f76570 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "bootstrap": "^5.2.3", "crypto-js": "^4.1.1", "formik": "^2.2.9", + "javascript-color-gradient": "^2.4.4", "jsbn": "^1.1.0", "jsencrypt": "^3.3.2", "lodash": "^4.17.21", diff --git a/src/App.js b/src/App.js index e713f7d..8b8ae45 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; -import { AppContext } from './Contexts/AppContext'; +import { AppContextProvider } from './Contexts/AppContext'; import Navigation from './Components/Navigation' import HomePage from './Pages/HomePage' @@ -19,15 +19,8 @@ import { loadButtonLabels } from './Services/Storage'; import './App.scss'; const App = () => { - const [buttonLabels, setButtonLabels] = useState(loadButtonLabels() ?? 'gp2040'); - - const appData = { - buttonLabels, - setButtonLabels, - }; - return ( - +
@@ -46,7 +39,7 @@ const App = () => {
-
+ ); } diff --git a/src/Contexts/AppContext.js b/src/Contexts/AppContext.js index fd582cf..0e7fe65 100644 --- a/src/Contexts/AppContext.js +++ b/src/Contexts/AppContext.js @@ -1,7 +1,63 @@ -import { createContext } from 'react'; +import React, { createContext, useState } from 'react'; -export const defaultAppData = { - buttonLabels: 'gp2040', -}; +export const AppContext = createContext(null); + +export const AppContextProvider = ({ children, ...props }) => { + const [buttonLabels, _setButtonLabels] = useState(localStorage.getItem('buttonLabels') || 'gp2040'); + const setButtonLabels = (buttonLabels) => { + localStorage.setItem('buttonLabels', buttonLabels); + _setButtonLabels(buttonLabels); + }; + + const [savedColors, _setSavedColors] = useState(localStorage.getItem('savedColors') ? localStorage.getItem('savedColors').split(',') : []); + const setSavedColors = (savedColors) => { + localStorage.setItem('savedColors', savedColors); + _setSavedColors(savedColors); + }; + + const [gradientNormalColor1, _setGradientNormalColor1] = useState('#00ffff'); + const setGradientNormalColor1 = (gradientNormalColor1) => { + localStorage.setItem('gradientNormalColor1', gradientNormalColor1); + _setGradientNormalColor1(gradientNormalColor1); + }; -export const AppContext = createContext(defaultAppData); + const [gradientNormalColor2, _setGradientNormalColor2] = useState('#ff00ff'); + const setGradientNormalColor2 = (gradientNormalColor2) => { + localStorage.setItem('gradientNormalColor2', gradientNormalColor2); + _setGradientNormalColor1(gradientNormalColor2); + }; + + const [gradientPressedColor1, _setGradientPressedColor1] = useState('#ff00ff'); + const setGradientPressedColor1 = (gradientPressedColor1) => { + localStorage.setItem('gradientPressedColor1', gradientPressedColor1); + _setGradientPressedColor1(gradientPressedColor1); + }; + + const [gradientPressedColor2, _setGradientPressedColor2] = useState('#00ffff'); + const setGradientPressedColor2 = (gradientPressedColor2) => { + localStorage.setItem('gradientPressedColor2', gradientPressedColor2); + _setGradientPressedColor1(gradientPressedColor2); + }; + + return ( + + {children} + + ); +}; diff --git a/src/Data/Buttons.js b/src/Data/Buttons.js index df6d893..ff511c7 100644 --- a/src/Data/Buttons.js +++ b/src/Data/Buttons.js @@ -135,3 +135,34 @@ export const BUTTONS = { export const AUX_BUTTONS = [ 'S1', 'S2', 'L3', 'R3', 'A1', 'A2' ]; export const MAIN_BUTTONS = [ 'Up', 'Down', 'Left', 'Right', 'B1', 'B2', 'B3', 'B4', 'L1', 'R1', 'L2', 'R2' ]; + +export const STICK_LAYOUT = [ + [null, 'Left', null], + ['Up', null, 'Down'], + [null, 'Right', null], + ['B3', null, 'B1'], + ['B4', null, 'B2'], + ['R1', null, 'R2'], + ['L1', null, 'L2'], +]; + +export const STICKLESS_LAYOUT = [ + ['Left', null, null], + ['Down', null, null], + ['Right', null, null], + [null, 'Up', null], + ['B3', 'B1', null], + ['B4', 'B2', null], + ['R1', 'R2', null], + ['L1', 'L2', null], +]; + +export const KEYBOARD_LAYOUT = [ + [null, 'Left'], + ['Up', 'Down'], + [null, 'Right'], + ['B3', 'B1'], + ['B4', 'B2'], + ['R1', 'R2'], + ['L1', 'L2'], +]; diff --git a/src/Pages/BackupPage.js b/src/Pages/BackupPage.js index e01898e..ebf4efc 100644 --- a/src/Pages/BackupPage.js +++ b/src/Pages/BackupPage.js @@ -10,7 +10,7 @@ const API_BINDING = { "display": {label: "Display", get: WebApi.getDisplayOptions, set: WebApi.setDisplayOptions}, "gamepad": {label: "Gamepad", get: WebApi.getGamepadOptions, set: WebApi.setGamepadOptions}, "led": {label: "LED", get: WebApi.getLedOptions, set: WebApi.setLedOptions}, - "ledTheme": {label: "Custom Theme", get: WebApi.getCustomTheme, set: WebApi.setCustomTheme}, + "ledTheme": {label: "Custom LED Theme", get: WebApi.getCustomTheme, set: WebApi.setCustomTheme}, "pinmappings": {label: "Pin Mappings", get: WebApi.getPinMappings, set: WebApi.setPinMappings}, "addons": {label: "Add-Ons", get: WebApi.getAddonsOptions, set: WebApi.setAddonsOptions}, // new api, add it here diff --git a/src/Pages/CustomThemePage.js b/src/Pages/CustomThemePage.js index ea2c4b5..973fbca 100644 --- a/src/Pages/CustomThemePage.js +++ b/src/Pages/CustomThemePage.js @@ -1,8 +1,8 @@ import React, { useContext, useEffect, useState } from 'react'; import Button from 'react-bootstrap/Button'; -import ButtonGroup from 'react-bootstrap/ButtonGroup'; import Col from 'react-bootstrap/Col'; import Container from 'react-bootstrap/Container'; +import Fade from 'react-bootstrap/Fade'; import Form from 'react-bootstrap/Form'; import FormCheck from 'react-bootstrap/FormCheck'; import Modal from 'react-bootstrap/Modal'; @@ -11,12 +11,13 @@ import Popover from 'react-bootstrap/Popover'; import Row from 'react-bootstrap/Row'; import Stack from 'react-bootstrap/Stack'; import { SketchPicker } from '@hello-pangea/color-picker'; +import Gradient from "javascript-color-gradient"; import { AppContext } from '../Contexts/AppContext'; import FormSelect from '../Components/FormSelect'; import Section from '../Components/Section'; import WebApi from '../Services/WebApi'; -import { BUTTONS, MAIN_BUTTONS, AUX_BUTTONS } from '../Data/Buttons'; +import { BUTTONS, MAIN_BUTTONS, AUX_BUTTONS, KEYBOARD_LAYOUT, STICK_LAYOUT, STICKLESS_LAYOUT } from '../Data/Buttons'; import LEDColors from '../Data/LEDColors'; import './CustomThemePage.scss'; @@ -26,16 +27,19 @@ const BUTTON_LAYOUTS = [ label: 'Stick', value: 0, stickLayout: 'standard', + matrix: STICK_LAYOUT, }, { label: 'Stickless', value: 1, stickLayout: 'stickless', + matrix: STICKLESS_LAYOUT, }, { label: 'WASD', value: 2, stickLayout: 'keyboard', + matrix: KEYBOARD_LAYOUT, }, ]; @@ -47,6 +51,10 @@ const defaultCustomTheme = Object.keys(BUTTONS.gp2040) }, {}); defaultCustomTheme['ALL'] = { normal: '#000000', pressed: '#000000' }; +defaultCustomTheme['GRADIENT NORMAL'] = { normal: '#00ffff', pressed: '#ff00ff' }; +defaultCustomTheme['GRADIENT PRESSED'] = { normal: '#ff00ff', pressed: '#00ffff' }; + +const specialButtons = ['ALL', 'GRADIENT NORMAL', 'GRADIENT PRESSED']; const LEDButton = ({ id, name, buttonType, buttonColor, buttonPressedColor, className, labelUnder, onClick, ...props }) => { const [pressed, setPressed] = useState(false); @@ -77,8 +85,23 @@ const LEDButton = ({ id, name, buttonType, buttonColor, buttonPressedColor, clas ); }; +const ledColors = LEDColors.map(c => ({ title: c.name, color: c.value})); +const customColors = (colors) => colors.map(c => ({ title: c, color: c })); + const CustomThemePage = () => { - const { buttonLabels } = useContext(AppContext); + const { + buttonLabels, + gradientNormalColor1, + gradientNormalColor2, + gradientPressedColor1, + gradientPressedColor2, + savedColors, + setGradientNormalColor1, + setGradientNormalColor2, + setGradientPressedColor1, + setGradientPressedColor2, + setSavedColors + } = useContext(AppContext); const [saveMessage, setSaveMessage] = useState(''); const [ledLayout, setLedLayout] = useState(0); const [pickerType, setPickerType] = useState(null); @@ -86,12 +109,12 @@ const CustomThemePage = () => { const [selectedColor, setSelectedColor] = useState('#000000'); const [hasCustomTheme, setHasCustomTheme] = useState(false); const [customTheme, setCustomTheme] = useState({ ...defaultCustomTheme }); - const [ledOverlayTarget, setLedOverlayTarget] = useState(null); + const [ledOverlayTarget, setLedOverlayTarget] = useState(document.body); + const [pickerVisible, setPickerVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false); + const [presetColors, setPresetColors] = useState([...ledColors, ...customColors(savedColors)]); const confirmClearAll = () => { - setLedOverlayTarget(null); - setSelectedButton(null); setSelectedColor(null); // Reset all custom LEDs @@ -100,10 +123,22 @@ const CustomThemePage = () => { customTheme[b][s] = '#000000'; }); }); + setCustomTheme(customTheme); setModalVisible(false); }; + const deleteCurrentColor = () => { + const colorIndex = savedColors.indexOf(selectedColor.hex); + if (colorIndex < 0) + return; + + const newColors = [...savedColors]; + newColors.splice(colorIndex, 1); + setSavedColors(newColors); + setPresetColors([...ledColors, ...customColors(newColors)]); + }; + const handleLedColorClick = (pickerType) => { setSelectedColor(customTheme[selectedButton][pickerType]); setPickerType({ type: pickerType, button: selectedButton }); @@ -111,16 +146,67 @@ const CustomThemePage = () => { const handleLedColorChange = (c) => { if (selectedButton) { - if (selectedButton === 'ALL') - Object.keys(customTheme).forEach(p => customTheme[p][pickerType.type] = c.hex); - else - customTheme[selectedButton][pickerType.type] = c.hex; + if (selectedButton === 'ALL') { + Object.keys(customTheme).filter(b => b === 'ALL' || specialButtons.indexOf(b) === -1).forEach(p => customTheme[p][pickerType.type] = c.hex); + } + else if (selectedButton === 'GRADIENT NORMAL' || selectedButton === 'GRADIENT PRESSED') { + customTheme[selectedButton][pickerType.type] = c.hex; + + // Apply the gradient across action buttons only, 7-8 columns + const matrix = BUTTON_LAYOUTS[ledLayout].matrix; + const count = matrix.length; + + let steps = [customTheme[selectedButton].normal]; + steps.push(...new Gradient() + .setColorGradient(customTheme[selectedButton].normal, customTheme[selectedButton].pressed) + .setMidpoint(count - 2) + .getColors() + ); + steps.push(customTheme[selectedButton].pressed); + + if (selectedButton === 'GRADIENT NORMAL') { + matrix.forEach((r, i) => r.filter(b => !!b).forEach(b => customTheme[b] = { normal: steps[i], pressed: customTheme[b].pressed })); + if (pickerType.type === 'pressed') { + setGradientNormalColor1(customTheme[selectedButton].normal); + setGradientNormalColor2(c.hex); + } + else { + setGradientNormalColor1(c.hex); + setGradientNormalColor2(customTheme[selectedButton].pressed); + } + } + else if (selectedButton === 'GRADIENT PRESSED') { + matrix.forEach((r, i) => r.filter(b => !!b).forEach(b => customTheme[b] = { normal: customTheme[b].normal, pressed: steps[i] })); + if (pickerType.type === 'pressed') { + setGradientPressedColor1(customTheme[selectedButton].normal); + setGradientPressedColor2(c.hex); + } + else { + setGradientPressedColor1(c.hex); + setGradientPressedColor2(customTheme[selectedButton].pressed); + } + } + } + else { + customTheme[selectedButton][pickerType.type] = c.hex; + } } setCustomTheme(customTheme); setSelectedColor(c); }; + const saveCurrentColor = () => { + const color = selectedColor.hex; + if (!color || presetColors.filter(c => c.color === color).length > 0) + return; + + const newColors = [...savedColors]; + newColors.push(selectedColor.hex); + setSavedColors(newColors); + setPresetColors([...ledColors, ...customColors(newColors)]); + }; + const toggleCustomTheme = (e) => { setHasCustomTheme(e.target.checked); }; @@ -128,20 +214,20 @@ const CustomThemePage = () => { const toggleSelectedButton = (e, buttonName) => { e.stopPropagation(); if (selectedButton === buttonName) { - setSelectedButton(null); - setLedOverlayTarget(null); + setPickerVisible(false); } else { setLedOverlayTarget(e.target); setSelectedButton(buttonName); setSelectedColor(buttonName === 'ALL' ? '#000000' : customTheme[buttonName].normal); setPickerType({ type: 'normal', button: buttonName }); + setPickerVisible(true); } }; const submit = async () => { const leds = { ...customTheme }; - delete leds['ALL']; + specialButtons.forEach(b => delete leds[b]); const success = await WebApi.setCustomTheme({ hasCustomTheme, customTheme: leds }); setSaveMessage(success ? 'Saved! Please Restart Your Device' : 'Unable to Save'); }; @@ -153,17 +239,25 @@ const CustomThemePage = () => { setHasCustomTheme(data.hasCustomTheme); if (!data.customTheme['ALL']) data.customTheme['ALL'] = { normal: '#000000', pressed: '#000000' }; + if (!data.customTheme['GRADIENT NORMAL']) + data.customTheme['GRADIENT NORMAL'] = { normal: '#00ffff', pressed: '#ff00ff' }; + if (!data.customTheme['GRADIENT PRESSED']) + data.customTheme['GRADIENT PRESSED'] = { normal: '#00ffff', pressed: '#ff00ff' }; + setCustomTheme(data.customTheme); } fetchData(); // Hide color picker when anywhere but picker is clicked - window.addEventListener('click', (e) => { - toggleSelectedButton(e, selectedButton); - }); + window.addEventListener('click', (e) => toggleSelectedButton(e, selectedButton)); }, []); + useEffect(() => { + if (!pickerVisible) + setTimeout(() => setSelectedButton(null), 250); // Delay enough to allow fade animation to finish + }, [pickerVisible]); + return <>
@@ -226,29 +320,30 @@ const CustomThemePage = () => {
- +
- + + +
} -1 ? 'top' : 'bottom'} container={this} containerPadding={20} - transition={null} > e.stopPropagation()}> -
{selectedButton === 'ALL' ? selectedButton : BUTTONS[buttonLabels][selectedButton]}
+
{specialButtons.indexOf(selectedButton) > -1 ? selectedButton : BUTTONS[buttonLabels][selectedButton]}
handleLedColorClick('normal')} > - Normal + {selectedButton?.startsWith('GRADIENT') ? 'Color 1' : 'Normal'}
{ className={`led-color-option ${pickerType?.type === 'pressed' ? 'selected' : ''}`} onClick={() => handleLedColorClick('pressed')} > - Pressed + {selectedButton?.startsWith('GRADIENT') ? 'Color 2' : 'Pressed'}
- + handleLedColorChange(c)} disableAlpha={true} - presetColors={LEDColors.map(c => ({ title: c.name, color: c.value}))} + presetColors={presetColors} width={180} /> +
+ + +
diff --git a/src/Services/Utilities.js b/src/Services/Utilities.js index 2c29bca..a1f3397 100644 --- a/src/Services/Utilities.js +++ b/src/Services/Utilities.js @@ -5,7 +5,7 @@ const hexToInt = (hex) => { // Convert a number to hex const intToHex = (d) => { - return ("0"+(Number(d).toString(16))).slice(-2).toUpperCase(); + return ("0"+(Number(d).toString(16))).slice(-2).toLowerCase(); }; // Convert a 32-bit ARGB value to hex format (no # prefix) @@ -32,8 +32,8 @@ const rgbArrayToHex = (values) => { }; export { - intToHex, hexToInt, + intToHex, rgbArrayToHex, rgbIntToHex, }; diff --git a/src/index.scss b/src/index.scss index efc25f3..85da99a 100644 --- a/src/index.scss +++ b/src/index.scss @@ -53,7 +53,7 @@ input.form-control { } .popover { - position: absolute; + position: fixed !important; z-index: 2; .cover { @@ -62,3 +62,11 @@ input.form-control { } } +.button-group { + display: flex; + justify-content: flex-start; + + button:not(:first-child) { + margin-left: 10px; + } +}