Source: components/strip_settings/index.js

import RANGE_CONSTANTS from '../../utils/constants';
import NumericSlider from "../numeric_slider";
import { useState, useEffect } from "preact/hooks";
import { getStripSettings, updateStripSettings } from "../../utils/api";
import { useConnectivity } from "../../context/online_context";
import useInterval from "../../utils/use_interval";
import { LabelSpinner } from '../spinner';
import SimpleChooser from '../single_chooser';
import style from './style.css';
import PatternSettings from '../pattern_settings';
import PatternModal from "../../components/pattern_modal";

/**
 * @brief A group of UI components for configuring LED strip settings on the NanoLux device.
 *
 * This component fetches initial strip settings from the device, allows users to
 * modify parameters (noise threshold, pattern count, mode, transparency), and
 * pushes updates back to the device.
 *
 * @param patterns   An array of pattern definitions passed down to PatternSettings.
 * @param advanced   A boolean indicating whether advanced settings should be displayed (default: false).
 *
 * @returns The group of StripSettings UI elements.
 */
const StripSettings = ({patterns, advanced = false}) => {
	// Controls whether the Pattern help modal is open.
	const [isModalOpen, setIsModalOpen] = useState(false);
	
	// Checks if the web app is connected to the device.
	const { isConnected } = useConnectivity();

	// Flag representing if initial data has been obtained from
	// the device.
	const [loading, setLoading] = useState(true);

	// Flag that tells the object to update the NanoLux device with new data.
	const [updated, setUpdated] = useState(false);

	// Stores the subpattern currently selected as an object state.
	const [selectedPattern, setPattern] = useState(0);

	// Strip-level data structure
	const [data, setData] = useState({
		pattern_count: 1,
		alpha: 0,
		mode: 0,
		noise: 20
	});

	/**
	 * @brief Manages initial querying of the data from the NanoLux device.
	 * 		  Sets the loading flag to false when done.
	 */
	useEffect(() => {
        if (isConnected) {
            getStripSettings()
				.then(data => setData(data))
				.then(setLoading(false));
        }  
    }, [isConnected])

	/**
	 * @brief Updates the pattern on the Nanolux device 
	 * if it is connected and has modified data,
	 * then flags the data structure.
	 */
	useInterval(() => {
        if (isConnected && updated) {
            updateStripSettings(data);
			setUpdated(false);
        }
    }, 100);

	/**
	 * @brief Updates a parameter in the pattern data structure with a new value.
	 * @param ref The string reference to update in the data structure
	 * @param value The new value to update the data structure with
	 */
	const update = (ref, value) => {
		if(!loading){		
			setData((oldData) => {
				let newData = Object.assign({}, oldData);
				newData[ref] = value;
				return newData;
			})
		}
		setUpdated(true);
	}

	/**
	 * @brief Increments the amount of patterns displayed. If the device is at
	 * maximum patterns, the function will refuse to increment.
	 */
	const incrementPatterns = async () => {
		if(data.pattern_count < RANGE_CONSTANTS.PATTERN_MAX){
			update("pattern_count", data.pattern_count + 1);
		}
	}

	/**
	 * @brief Decrements the amount of patterns displayed. If the device is at
	 * 1 pattern, the function will refuse to decrement. Moves to the
	 * previous pattern if the existing one is deleted.
	 */
	const decrementPatterns = async () => {
		if(data.pattern_count > 1){
			update("pattern_count", data.pattern_count - 1);
			if(data.pattern_count >= selectedPattern - 1){
				setPattern(data.pattern_count - 2);
			}	
		}
	}

	/** @brief Opens the pattern help modal. */
	const openModal = () => {
		setIsModalOpen(true);
	}

	/** @brief Closes the pattern help modal. */
	const closeModal = () => {
		setIsModalOpen(false);
	}

	/**
	 * @brief Generates a list from 0 to end. Analagous to Python's
	 * range() function.
	 * @param end The value to end at.
	 */
	function inRange(end){
		var l = [];
		for(var i = 0; i < end; i++){
			l.push({idx: i});
		}
		return l;
	}

	/**
	 * @brief Generates the Pattern UI element and it's selected subpattern.
	 */
	return (
		(!loading ?
			<div>
				{advanced && (
					<>
					<br />
					<SimpleChooser
						label="Mode"
						tooltip={{
							id: 'mode',
							content: 'The mode that patterns will be displayed in.',
						}}
						options={[
							{option : "Strip Splitting", idx : RANGE_CONSTANTS.STRIP_SPLITTING_ID, tooltip : 'Splits the LED strip into sections up to 4 times based on how many patterns are added to the device.'},
							{option : "Z-Layering", idx : RANGE_CONSTANTS.Z_LAYERING_ID, tooltip : "Renders up to 2 patterns that are layered on top of each other over the entire LED strip."},
						]}
						noSelection={false}
						initial={data.mode}
						structure_ref="mode"
						update={update}
					/>
					<br />
					<NumericSlider
						className={style.settings_control}
						label="Transparency"
						tooltip={{
							id: 'transparency',
							content: 'Adjusts how opaque or transparent a pattern is.',
						}}
						min={RANGE_CONSTANTS.ALPHA_MIN}
						max={RANGE_CONSTANTS.ALPHA_MAX}
						initial={data.alpha}
						structure_ref="alpha"
						update={update}
					/>
					<br />
					</>
				)}
				<NumericSlider
					className={style.settings_control}
					label="Noise Threshold"
					tooltip={{
						id: 'threshold',
						content: 'Adjusts how much noise it takes to display a pattern.',
					}}
					min={RANGE_CONSTANTS.NOISE_MIN}
					max={RANGE_CONSTANTS.NOISE_MAX}
					initial={data.noise}
					structure_ref="noise"
					update={update}
				/>
				{advanced && (
					<>
					<br />
					<button className={style.incBtn} onClick={incrementPatterns} aria-label='Increase pattern count'>+</button>
					<button className={style.incBtn} onClick={decrementPatterns} aria-label='Decrease pattern count'>-</button>
					<br />
					<div className={style.patternRow}>
						{inRange(data.pattern_count).map((data) => {
							if(data.idx == selectedPattern){
								return <button className={style.patternBtn} onClick={function() {setPattern(data.idx);}} key={data.idx} style="border-style:inset;" aria-pressed='true'>
									Pattern {data.idx + 1}
								</button>
							}else{
								return <button className={style.patternBtn} onClick={function() {setPattern(data.idx);}} key={data.idx} aria-pressed='false'>
									Pattern {data.idx + 1}
								</button>
							}
						})}
					</div>
					</>
				)}
				<button
				  className={style.modalBtn}
				  type="button"
				  onClick={openModal}
				  aria-haspopup='dialog'
				  aria-label='Open help dialog for pattern descriptions'>
				  Help
				</button>
				<br />
				<hr />

				<PatternSettings
				  num={selectedPattern}
				  patterns={patterns}
				  advanced={advanced}
				  key={selectedPattern}
				/>
				<PatternModal
					isOpen={isModalOpen}
					onClose={closeModal}
				/>
			</div>
		: <LabelSpinner />)
	);
}

export default StripSettings;