mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-04 07:24:20 +01:00
437 lines
10 KiB
JavaScript
437 lines
10 KiB
JavaScript
import { default as iro } from '@jaames/iro';
|
|
import { styleInject, styleVisible } from './core.mjs';
|
|
import { sendAction } from './connection.mjs';
|
|
import { mergeTemplate, loadTemplate } from './template.mjs';
|
|
import { addEnumerables, variableListeners } from './settings.mjs';
|
|
|
|
/**
|
|
* @param {iro.Color} color
|
|
* @returns {string}
|
|
*/
|
|
function colorToHsvString(color) {
|
|
const hsv =
|
|
[color.hsv.h, color.hsv.s, color.hsv.v]
|
|
.filter((value) => value !== undefined)
|
|
.map(Math.round);
|
|
|
|
if (hsv.length !== 3) {
|
|
return "0,0,0";
|
|
}
|
|
|
|
return hsv.join(",");
|
|
}
|
|
|
|
/**
|
|
* @param {string} string
|
|
*/
|
|
function hsvStringToColor(string) {
|
|
const parts = string.split(",")
|
|
.map(parseInt);
|
|
|
|
return {
|
|
h: parts[0] ?? 0,
|
|
s: parts[1] ?? 0,
|
|
v: parts[2] ?? 0,
|
|
};
|
|
}
|
|
|
|
/** @param {string} type */
|
|
function colorSlider(type) {
|
|
return {component: iro.ui.Slider, options: {sliderType: type}};
|
|
}
|
|
|
|
function colorWheel() {
|
|
return {component: iro.ui.Wheel, options: {}};
|
|
}
|
|
|
|
function colorBox() {
|
|
return {component: iro.ui.Box, options: {}};
|
|
}
|
|
|
|
/** @param {function(HTMLElement): void} callback */
|
|
function withPicker(callback) {
|
|
callback(/** @type {!HTMLElement} */
|
|
(document.getElementById("light-picker")));
|
|
}
|
|
|
|
/**
|
|
* @param {"rgb" | "hsv"} mode
|
|
* @param {string} value
|
|
*/
|
|
function colorUpdate(mode, value) {
|
|
withPicker((elem) => {
|
|
elem.dispatchEvent(
|
|
new CustomEvent("color:string", {
|
|
detail: {mode, value}}));
|
|
});
|
|
}
|
|
|
|
/** @param {number} id */
|
|
function lightStateHideRelay(id) {
|
|
styleInject([
|
|
styleVisible(`.relay-control-${id}`, false)
|
|
]);
|
|
}
|
|
|
|
/** @param {function(HTMLInputElement): void} callback */
|
|
function withState(callback) {
|
|
callback(/** @type {!HTMLInputElement} */
|
|
(document.querySelector("input[name=light-state-value]")));
|
|
}
|
|
|
|
function initLightState() {
|
|
withState((elem) => {
|
|
elem.addEventListener("change", (event) => {
|
|
event.preventDefault();
|
|
const state = /** @type {!HTMLInputElement} */
|
|
(event.target).checked;
|
|
sendAction("light", {state});
|
|
});
|
|
|
|
});
|
|
}
|
|
|
|
/** @param {boolean} value */
|
|
function updateLightState(value) {
|
|
withState((elem) => {
|
|
elem.checked = value;
|
|
lightClass(value, "light-on");
|
|
});
|
|
}
|
|
|
|
/** @param {boolean} value */
|
|
function colorEnabled(value) {
|
|
lightClass(value, "light-color");
|
|
}
|
|
|
|
/** @param {boolean} value */
|
|
function colorInit(value) {
|
|
// TODO: ref. #2451, input:change causes pretty fast updates.
|
|
// either make sure we don't cause any issue on the esp, or switch to
|
|
// color:change instead (which applies after input ends)
|
|
|
|
/** @type {function(iro.Color): void} */
|
|
let change = () => {
|
|
};
|
|
|
|
/** @type {string[]} */
|
|
const rules = [];
|
|
|
|
/** @type {{"component": any, "options": any}[]} */
|
|
const layout = [];
|
|
|
|
// RGB
|
|
if (value) {
|
|
layout.push(colorWheel());
|
|
change = (color) => {
|
|
sendAction("light", {
|
|
rgb: color.hexString
|
|
});
|
|
};
|
|
// HSV
|
|
} else {
|
|
layout.push(colorBox());
|
|
layout.push(colorSlider("hue"));
|
|
layout.push(colorSlider("saturation"));
|
|
change = (color) => {
|
|
sendAction("light", {
|
|
hsv: colorToHsvString(color)
|
|
});
|
|
};
|
|
}
|
|
|
|
layout.push(colorSlider("value"));
|
|
styleInject(rules);
|
|
|
|
withPicker((elem) => {
|
|
// TODO w/ the current bundle, this is not a ctor
|
|
const picker = iro.ColorPicker(elem, {layout});
|
|
picker.on("input:change", change);
|
|
|
|
elem.addEventListener("color:string", (event) => {
|
|
const color = /** @type {CustomEvent<{mode: string, value: string}>} */
|
|
(event).detail;
|
|
switch (color.mode) {
|
|
case "rgb":
|
|
picker.color.hexString = color.value;
|
|
break;
|
|
case "hsv":
|
|
picker.color.hsv = hsvStringToColor(color.value);
|
|
break;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/** @param {function(HTMLInputElement): void} callback */
|
|
function withMiredsValue(callback) {
|
|
const elem = document.getElementById("mireds-value");
|
|
if (elem instanceof HTMLInputElement) {
|
|
callback(elem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLElement} elem
|
|
* @param {string} text
|
|
*/
|
|
function textForNextSibling(elem, text) {
|
|
const next = elem.nextElementSibling;
|
|
if (!next) {
|
|
return;
|
|
}
|
|
|
|
next.textContent = text;
|
|
}
|
|
|
|
/**
|
|
* @param {HTMLInputElement} elem
|
|
* @param {number} value
|
|
*/
|
|
function setMiredsValue(elem, value) {
|
|
elem.value = value.toString();
|
|
textForNextSibling(elem, elem.value);
|
|
}
|
|
|
|
/** @param {number} value */
|
|
function updateMireds(value) {
|
|
withMiredsValue((elem) => {
|
|
setMiredsValue(elem, value);
|
|
});
|
|
}
|
|
|
|
/** @param {function(HTMLElement): void} callback */
|
|
function withLight(callback) {
|
|
callback(/** @type {!HTMLElement} */
|
|
(document.getElementById("light")));
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} value
|
|
* @param {string} className
|
|
*/
|
|
function lightClass(value, className) {
|
|
withLight((elem) => {
|
|
if (value) {
|
|
elem.classList.add(className);
|
|
} else {
|
|
elem.classList.remove(className);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* implies we should hide one or both white channels
|
|
* @param {boolean} value
|
|
*/
|
|
function whiteEnabled(value) {
|
|
lightClass(value, "light-white");
|
|
}
|
|
|
|
/**
|
|
* no need for raw white channel sliders with cct
|
|
* @param {boolean} value
|
|
*/
|
|
function cctEnabled(value) {
|
|
lightClass(value, "light-cct");
|
|
}
|
|
|
|
/** @param {{cold: number, warm: number}} value */
|
|
function cctInit(value) {
|
|
const control = loadTemplate("mireds-control");
|
|
|
|
withMiredsValue((elem) => {
|
|
elem.setAttribute("min", value.cold.toString());
|
|
elem.setAttribute("max", value.warm.toString());
|
|
elem.addEventListener("change", () => {
|
|
textForNextSibling(elem, elem.value);
|
|
sendAction("light", {mireds: elem.value});
|
|
});
|
|
|
|
});
|
|
|
|
const [datalist] = control.querySelectorAll("datalist");
|
|
datalist.innerHTML = `
|
|
<option value="${value.cold}">Cold</option>
|
|
<option value="${value.warm}">Warm</option>`;
|
|
|
|
mergeTemplate(
|
|
/** @type {HTMLElement} */
|
|
(document.getElementById("light-cct")), control);
|
|
}
|
|
|
|
/** @param {{[k: string]: any}} data */
|
|
function updateLight(data) {
|
|
for (const [key, value] of Object.entries(data)) {
|
|
switch (key) {
|
|
case "state":
|
|
updateLightState(value);
|
|
break;
|
|
|
|
case "state_relay_id":
|
|
lightStateHideRelay(value);
|
|
break;
|
|
|
|
case "channels":
|
|
initLightState();
|
|
initBrightness();
|
|
initChannels(value);
|
|
break;
|
|
|
|
case "cct":
|
|
cctInit(value);
|
|
break;
|
|
|
|
case "brightness":
|
|
updateBrightness(value);
|
|
break;
|
|
|
|
case "values":
|
|
updateChannels(value);
|
|
break;
|
|
|
|
case "rgb":
|
|
case "hsv":
|
|
colorUpdate(key, value);
|
|
break;
|
|
|
|
case "mireds":
|
|
updateMireds(value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param {Event} event */
|
|
function onChannelSliderChange(event) {
|
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
|
|
const target = event.target;
|
|
textForNextSibling(target, target.value);
|
|
|
|
const id = target.dataset["id"];
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
sendAction("light", {
|
|
channel: {
|
|
[id]: target.value
|
|
}
|
|
});
|
|
}
|
|
|
|
/** @param {Event} event */
|
|
function onBrightnessSliderChange(event) {
|
|
if (!(event.target instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
|
|
textForNextSibling(event.target, event.target.value);
|
|
sendAction("light", {brightness: event.target.value});
|
|
}
|
|
|
|
function initBrightness() {
|
|
const template = loadTemplate("brightness-control");
|
|
|
|
const elem = template.getElementById("brightness-value");
|
|
elem?.addEventListener("change", onBrightnessSliderChange);
|
|
|
|
mergeTemplate(
|
|
/** @type {!HTMLElement} */
|
|
(document.getElementById("light-brightness")), template);
|
|
}
|
|
|
|
/** @param {number} value */
|
|
function updateBrightness(value) {
|
|
const elem = document.getElementById("brightness-value");
|
|
if (elem instanceof HTMLInputElement) {
|
|
elem.value = value.toString();
|
|
textForNextSibling(elem, elem.value);
|
|
}
|
|
}
|
|
|
|
/** @param {string[]} channels */
|
|
function initChannels(channels) {
|
|
const container = document.getElementById("light-channels");
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
/** @type {import('./settings.mjs').EnumerableEntry[]} */
|
|
const enumerables = [];
|
|
|
|
channels.forEach((tag, channel) => {
|
|
const line = loadTemplate("channel-control");
|
|
|
|
const [root] = line.querySelectorAll("div");
|
|
root.setAttribute("id", `light-channel-${tag.toLowerCase()}`);
|
|
|
|
const name =
|
|
`Channel #${channel} (${tag.toUpperCase()})`;
|
|
|
|
const [label] = line.querySelectorAll("label");
|
|
label.textContent = name;
|
|
|
|
enumerables.push({"id": channel, "name": name});
|
|
|
|
const [span] = line.querySelectorAll("span");
|
|
span.dataset["id"] = channel.toString();
|
|
|
|
const [slider] = line.querySelectorAll("input");
|
|
slider.dataset["id"] = channel.toString();
|
|
slider.addEventListener("change", onChannelSliderChange);
|
|
|
|
mergeTemplate(container, line);
|
|
});
|
|
|
|
addEnumerables("Channels", enumerables);
|
|
}
|
|
|
|
/** @param {number[]} values */
|
|
function updateChannels(values) {
|
|
const container = document.getElementById("light");
|
|
if (!container) {
|
|
return;
|
|
}
|
|
|
|
values.forEach((value, channel) => {
|
|
const slider = container.querySelector(`input.slider[data-id='${channel}']`);
|
|
if (!(slider instanceof HTMLInputElement)) {
|
|
return;
|
|
}
|
|
|
|
slider.value = value.toString();
|
|
textForNextSibling(slider, slider.value);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @returns {import('./settings.mjs').KeyValueListeners}
|
|
*/
|
|
function listeners() {
|
|
return {
|
|
"light": (_, value) => {
|
|
updateLight(value);
|
|
},
|
|
"useWhite": (_, value) => {
|
|
whiteEnabled(value);
|
|
},
|
|
"useCCT": (_, value) => {
|
|
cctEnabled(value);
|
|
},
|
|
"useColor": (_, value) => {
|
|
colorEnabled(value);
|
|
},
|
|
"useRGB": (_, value) => {
|
|
colorInit(value);
|
|
},
|
|
};
|
|
}
|
|
|
|
export function init() {
|
|
variableListeners(listeners());
|
|
}
|