@iobroker/adapter-react-v5
Version:
React components to develop ioBroker interfaces with react.
1,278 lines • 66.6 kB
JavaScript
/**
* Copyright 2018-2024 Denis Haev <dogafox@gmail.com>
*
* MIT License
*
*/
import React from 'react';
import { copy } from './CopyToClipboard';
import { I18n } from '../i18n';
const NAMESPACE = 'material';
const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const QUALITY_BITS = {
0x00: '0x00 - good',
0x01: '0x01 - general problem',
0x02: '0x02 - no connection problem',
0x10: '0x10 - substitute value from controller',
0x20: '0x20 - substitute initial value',
0x40: '0x40 - substitute value from device or instance',
0x80: '0x80 - substitute value from sensor',
0x11: '0x11 - general problem by instance',
0x41: '0x41 - general problem by device',
0x81: '0x81 - general problem by sensor',
0x12: '0x12 - instance not connected',
0x42: '0x42 - device not connected',
0x82: '0x82 - sensor not connected',
0x44: '0x44 - device reports error',
0x84: '0x84 - sensor reports error',
};
const SIGNATURES = {
JVBERi0: 'pdf',
R0lGODdh: 'gif',
R0lGODlh: 'gif',
iVBORw0KGgo: 'png',
'/9j/': 'jpg',
PHN2Zw: 'svg',
Qk1: 'bmp',
AAABAA: 'ico', // 00 00 01 00 according to https://en.wikipedia.org/wiki/List_of_file_signatures
};
export class Utils {
static namespace = NAMESPACE;
static INSTANCES = 'instances';
static dateFormat = ['DD', 'MM'];
static FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu;
/**
* Capitalize words.
*/
static CapitalWords(name) {
return (name || '')
.split(/[\s_]/)
.filter(item => item)
.map(word => (word ? word[0].toUpperCase() + word.substring(1).toLowerCase() : ''))
.join(' ');
}
static formatSeconds(seconds) {
const days_ = Math.floor(seconds / (3600 * 24));
seconds %= 3600 * 24;
const hours = Math.floor(seconds / 3600)
.toString()
.padStart(2, '0');
seconds %= 3600;
const minutes = Math.floor(seconds / 60)
.toString()
.padStart(2, '0');
seconds %= 60;
const secondsStr = Math.floor(seconds).toString().padStart(2, '0');
let text = '';
if (days_) {
text += `${days_} ${I18n.t('ra_daysShortText')} `;
}
text += `${hours}:${minutes}:${secondsStr}`;
return text;
}
/**
* Get the name of the object by id from the name or description.
*/
static getObjectName(objects, id, settings, options,
/** Set to true to get the description. */
isDesc) {
const item = objects[id];
let text;
if (typeof settings === 'string' && !options) {
options = { language: settings };
settings = null;
}
options = options || {};
if (!options.language) {
options.language =
(objects['system.config'] &&
objects['system.config'].common &&
objects['system.config'].common.language) ||
window.sysLang ||
'en';
}
if (settings?.name) {
const textObj = settings.name;
if (typeof textObj === 'object') {
text = (options.language && textObj[options.language]) || textObj.en;
}
else {
text = textObj;
}
}
else if (isDesc && item?.common?.desc) {
const textObj = item.common.desc;
if (typeof textObj === 'object') {
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
}
else {
text = textObj;
}
text = (text || '').toString().replace(/[_.]/g, ' ');
if (text === text.toUpperCase()) {
text = text[0] + text.substring(1).toLowerCase();
}
}
else if (!isDesc && item?.common) {
const textObj = item.common.name || item.common.desc;
if (textObj && typeof textObj === 'object') {
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
}
else {
text = textObj;
}
text = (text || '').toString().replace(/[_.]/g, ' ');
if (text === text.toUpperCase()) {
text = text[0] + text.substring(1).toLowerCase();
}
}
else {
const pos = id.lastIndexOf('.');
text = id.substring(pos + 1).replace(/[_.]/g, ' ');
text = Utils.CapitalWords(text);
}
return text?.trim() || '';
}
/**
* Get the name of the object from the name or description.
*/
static getObjectNameFromObj(obj,
/** settings or language */
settings, options,
/** Set to true to get the description. */
isDesc,
/** Allow using spaces in name (by edit) */
noTrim) {
const item = obj;
let text = obj?._id || '';
if (typeof settings === 'string' && !options) {
options = { language: settings };
settings = null;
}
options = options || {};
if (settings?.name) {
const name = settings.name;
if (typeof name === 'object') {
text = (options.language && name[options.language]) || name.en;
}
else {
text = name;
}
}
else if (isDesc && item?.common?.desc) {
const desc = item.common.desc;
if (typeof desc === 'object') {
text = (options.language && desc[options.language]) || desc.en;
}
else {
text = desc;
}
text = (text || '').toString().replace(/[_.]/g, ' ');
if (text === text.toUpperCase()) {
text = text[0] + text.substring(1).toLowerCase();
}
}
else if (!isDesc && item?.common?.name) {
let name = item.common.name;
if (!name && item.common.desc) {
name = item.common.desc;
}
if (typeof name === 'object') {
text = (options.language && name[options.language]) || name.en;
}
else {
text = name;
}
text = (text || '').toString().replace(/[_.]/g, ' ');
if (text === text.toUpperCase()) {
text = text[0] + text.substring(1).toLowerCase();
}
}
return noTrim ? text : text.trim();
}
/**
* Extracts from the object material settings, depends on username
*/
static getSettingsOrder(obj, forEnumId, options) {
let common;
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
common = obj.common;
}
else {
common = obj;
}
let settings;
if (common?.custom) {
settings = common.custom[NAMESPACE];
const user = options.user || 'admin';
if (settings && settings[user]) {
if (forEnumId) {
if (settings[user].subOrder && settings[user].subOrder[forEnumId]) {
return JSON.parse(JSON.stringify(settings[user].subOrder[forEnumId]));
}
}
else if (settings[user].order) {
return JSON.parse(JSON.stringify(settings[user].order));
}
}
}
return null;
}
/**
Used in material
*/
static getSettingsCustomURLs(obj, forEnumId, options) {
let common;
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
common = obj.common;
}
else {
common = obj;
}
let settings;
if (common?.custom) {
settings = common.custom[NAMESPACE];
const user = options.user || 'admin';
if (settings && settings[user]) {
if (forEnumId) {
if (settings[user].subURLs && settings[user].subURLs[forEnumId]) {
return JSON.parse(JSON.stringify(settings[user].subURLs[forEnumId]));
}
}
else if (settings[user].URLs) {
return JSON.parse(JSON.stringify(settings[user].URLs));
}
}
}
return null;
}
/**
* Reorder the array items in list between source and dest.
*/
static reorder(list, source, dest) {
const result = Array.from(list);
const [removed] = result.splice(source, 1);
result.splice(dest, 0, removed);
return result;
}
/**
Get smart name settings for the given object.
*/
static getSettings(obj, options, defaultEnabling) {
let settings;
const id = obj?._id || options?.id;
let common;
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
common = obj.common;
}
else {
common = obj;
}
if (common?.custom) {
settings = common.custom;
settings =
settings[NAMESPACE] && settings[NAMESPACE][options.user || 'admin']
? JSON.parse(JSON.stringify(settings[NAMESPACE][options.user || 'admin']))
: { enabled: true };
}
else {
settings = { enabled: defaultEnabling === undefined ? true : defaultEnabling, useCustom: false };
}
if (!Object.prototype.hasOwnProperty.call(settings, 'enabled')) {
settings.enabled = defaultEnabling === undefined ? true : defaultEnabling;
}
if (options) {
if (!settings.name && options.name) {
settings.name = options.name;
}
if (!settings.icon && options.icon) {
settings.icon = options.icon;
}
if (!settings.color && options.color) {
settings.color = options.color;
}
}
if (common) {
if (!settings.color && common.color) {
settings.color = common.color;
}
if (!settings.icon && common.icon) {
settings.icon = common.icon;
}
if (!settings.name && common.name) {
settings.name = common.name;
}
}
if (typeof settings.name === 'object') {
settings.name = (options.language && settings.name[options.language]) || settings.name.en;
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
if (settings.name === settings.name.toUpperCase()) {
settings.name = settings.name[0] + settings.name.substring(1).toLowerCase();
}
}
if (!settings.name && id) {
const pos = id.lastIndexOf('.');
settings.name = id.substring(pos + 1).replace(/[_.]/g, ' ');
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
settings.name = Utils.CapitalWords(settings.name);
}
return settings;
}
/**
Sets smartName settings for the given object.
*/
static setSettings(obj, settings, options) {
if (obj) {
obj.common = obj.common || {};
obj.common.custom = obj.common.custom || {};
obj.common.custom[NAMESPACE] = obj.common.custom[NAMESPACE] || {};
obj.common.custom[NAMESPACE][options.user || 'admin'] = settings;
const s = obj.common.custom[NAMESPACE][options.user || 'admin'];
if (s.useCommon) {
if (s.color !== undefined) {
obj.common.color = s.color;
delete s.color;
}
if (s.icon !== undefined) {
obj.common.icon = s.icon;
delete s.icon;
}
if (s.name !== undefined) {
if (typeof obj.common.name !== 'object' && options.language) {
obj.common.name = { [options.language]: s.name };
}
else if (typeof obj.common.name === 'object' && options.language) {
obj.common.name[options.language] = s.name;
}
delete s.name;
}
}
return true;
}
return false;
}
/**
* Get the icon for the given settings.
*/
static getIcon(settings, style) {
if (settings?.icon) {
// If UTF-8 icon
if (settings.icon.length <= 2) {
return React.createElement("span", { style: style || {} }, settings.icon);
}
if (settings.icon.startsWith('data:image')) {
return (React.createElement("img", { alt: settings.name, src: settings.icon, style: style || {} }));
}
// maybe later some changes for a second type
return (React.createElement("img", { alt: settings.name, src: (settings.prefix || '') + settings.icon, style: style }));
}
return null;
}
/**
* Get the icon for the given object.
*/
static getObjectIcon(id, obj) {
// If id is Object
if (typeof id === 'object') {
obj = id;
id = obj?._id;
}
if (obj?.common?.icon) {
let icon = obj.common.icon;
// If UTF-8 icon
if (typeof icon === 'string' && icon.length <= 2) {
return icon;
}
if (icon.startsWith('data:image')) {
return icon;
}
const parts = id.split('.');
if (parts[0] === 'system') {
icon = `adapter/${parts[2]}${icon.startsWith('/') ? '' : '/'}${icon}`;
}
else {
icon = `adapter/${parts[0]}${icon.startsWith('/') ? '' : '/'}${icon}`;
}
if (window.location.pathname.match(/adapter\/[^/]+\/[^/]+\.html/)) {
icon = `../../${icon}`;
}
else if (window.location.pathname.match(/material\/[.\d]+/)) {
icon = `../../${icon}`;
}
else if (window.location.pathname.match(/material\//)) {
icon = `../${icon}`;
}
return icon;
}
return null;
}
/**
* Converts word1_word2 to word1Word2.
*/
static splitCamelCase(text) {
// if (false && text !== text.toUpperCase()) {
// const words = text.split(/\s+/);
// for (let i = 0; i < words.length; i++) {
// const word = words[i];
// if (word.toLowerCase() !== word && word.toUpperCase() !== word) {
// let z = 0;
// const ww = [];
// let start = 0;
// while (z < word.length) {
// if (word[z].match(/[A-ZÜÄÖА-Я]/)) {
// ww.push(word.substring(start, z));
// start = z;
// }
// z++;
// }
// if (start !== z) {
// ww.push(word.substring(start, z));
// }
// for (let k = 0; k < ww.length; k++) {
// words.splice(i + k, 0, ww[k]);
// }
// i += ww.length;
// }
// }
//
// return words.map(w => {
// w = w.trim();
// if (w) {
// return w[0].toUpperCase() + w.substring(1).toLowerCase();
// }
// return '';
// }).join(' ');
// }
return text ? Utils.CapitalWords(text) : '';
}
/**
* Check if the given color is bright.
* https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
*/
static isUseBright(color, defaultValue) {
if (!color) {
return defaultValue === undefined ? true : defaultValue;
}
color = color.toString();
if (color.startsWith('#')) {
color = color.slice(1);
}
let r;
let g;
let b;
const rgb = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
if (rgb && rgb.length === 4) {
r = parseInt(rgb[1], 10);
g = parseInt(rgb[2], 10);
b = parseInt(rgb[3], 10);
}
else {
// convert 3-digit hex to 6-digits.
if (color.length === 3) {
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
}
// remove alfa channel
if (color.length === 8) {
color = color.substring(0, 6);
}
else if (color.length !== 6) {
return false;
}
r = parseInt(color.slice(0, 2), 16);
g = parseInt(color.slice(2, 4), 16);
b = parseInt(color.slice(4, 6), 16);
}
// http://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
}
/**
* Get the time string in the format 00:00.
*/
static getTimeString(seconds) {
seconds = parseFloat(seconds);
if (Number.isNaN(seconds)) {
return '--:--';
}
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60)
.toString()
.padStart(2, '0');
const secs = (seconds % 60).toString().padStart(2, '0');
if (hours) {
return `${hours}:${minutes}:${secs}`;
}
return `${minutes}:${secs}`;
}
/**
* Gets the wind direction with the given angle (degrees).
*/
static getWindDirection(
/** angle in degrees from 0° to 360° */
angle) {
if (angle >= 0 && angle < 11.25) {
return 'N';
}
if (angle >= 11.25 && angle < 33.75) {
return 'NNE';
}
if (angle >= 33.75 && angle < 56.25) {
return 'NE';
}
if (angle >= 56.25 && angle < 78.75) {
return 'ENE';
}
if (angle >= 78.75 && angle < 101.25) {
return 'E';
}
if (angle >= 101.25 && angle < 123.75) {
return 'ESE';
}
if (angle >= 123.75 && angle < 146.25) {
return 'SE';
}
if (angle >= 146.25 && angle < 168.75) {
return 'SSE';
}
if (angle >= 168.75 && angle < 191.25) {
return 'S';
}
if (angle >= 191.25 && angle < 213.75) {
return 'SSW';
}
if (angle >= 213.75 && angle < 236.25) {
return 'SW';
}
if (angle >= 236.25 && angle < 258.75) {
return 'WSW';
}
if (angle >= 258.75 && angle < 281.25) {
return 'W';
}
if (angle >= 281.25 && angle < 303.75) {
return 'WNW';
}
if (angle >= 303.75 && angle < 326.25) {
return 'NW';
}
if (angle >= 326.25 && angle < 348.75) {
return 'NNW';
}
// if (angle >= 348.75) {
return 'N';
}
/**
* Pad the given number with a zero if it's not two digits long.
*/
static padding(num) {
if (typeof num === 'string') {
if (num.length < 2) {
return `0${num}`;
}
return num;
}
if (num < 10) {
return `0${num}`;
}
return num.toString();
}
/**
* Sets the date format.
*/
static setDataFormat(format) {
if (format) {
Utils.dateFormat = format.toUpperCase().split(/[.-/]/);
Utils.dateFormat.splice(Utils.dateFormat.indexOf('YYYY'), 1);
}
}
/**
* Converts the date to a string.
*/
static date2string(now) {
if (typeof now === 'string') {
now = now.trim();
if (!now) {
return '';
}
// only letters
if (now.match(/^[\w\s]+$/)) {
// Day of the week
return now;
}
const m = now.match(/(\d{1,4})[-./](\d{1,2})[-./](\d{1,4})/);
if (m) {
const a = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
// We now have 3 numbers. Let's try to detect where is year, where is day and where is month
const year = a.find(y => y > 31);
if (year !== undefined) {
a.splice(a.indexOf(year), 1);
const day = a.find(mm => mm > 12);
if (day) {
a.splice(a.indexOf(day), 1);
now = new Date(year, a[0] - 1, day);
}
else if (Utils.dateFormat[0][0] === 'M' && Utils.dateFormat[1][0] === 'D') {
// MM DD
now = new Date(year, a[0] - 1, a[1]);
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
now = new Date(year, a[1] - 1, a[0]);
}
}
else if (Utils.dateFormat[0][0] === 'D' && Utils.dateFormat[1][0] === 'M') {
// DD MM
now = new Date(year, a[1] - 1, a[0]);
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
now = new Date(year, a[0] - 1, a[1]);
}
}
else {
now = new Date(now);
}
}
else {
now = new Date(now);
}
}
else {
now = new Date(now);
}
}
else {
now = new Date(now);
}
let date = I18n.t(`ra_dow_${days[now.getDay()]}`).replace('ra_dow_', '');
date += `. ${now.getDate()} ${I18n.t(`ra_month_${months[now.getMonth()]}`).replace('ra_month_', '')}`;
return date;
}
/**
* Render a text as a link.
*/
static renderTextWithA(text) {
let m = text.match(/<a [^<]+<\/a>|<br\s?\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/);
if (m) {
const result = [];
let key = 1;
do {
const start = text.substring(0, m.index);
text = text.substring((m.index || 0) + m[0].length);
start && result.push(React.createElement("span", { key: `a${key++}` }, start));
if (m[0].startsWith('<b>')) {
result.push(React.createElement("b", { key: `a${key++}` }, m[0].substring(3, m[0].length - 4)));
}
else if (m[0].startsWith('<i>')) {
result.push(React.createElement("i", { key: `a${key++}` }, m[0].substring(3, m[0].length - 4)));
}
else if (m[0].startsWith('<br')) {
result.push(React.createElement("br", { key: `a${key++}` }));
}
else {
const href = m[0].match(/href="([^"]+)"/) || m[0].match(/href='([^']+)'/);
const target = m[0].match(/target="([^"]+)"/) || m[0].match(/target='([^']+)'/);
const rel = m[0].match(/rel="([^"]+)"/) || m[0].match(/rel='([^']+)'/);
const title = m[0].match(/>([^<]*)</);
result.push(
// eslint-disable-next-line react/jsx-no-target-blank
React.createElement("a", { key: `a${key++}`, href: href ? href[1] : '', target: target ? target[1] : '_blank', rel: rel ? rel[1] : 'noreferrer', style: { color: 'inherit' } }, title ? title[1] : ''));
}
m = text ? text.match(/<a [^<]+<\/a>|<br\s?\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/) : null;
if (!m && text) {
// put the rest text
result.push(React.createElement("span", { key: `a${key++}` }, text));
}
} while (m);
return result;
}
return text;
}
/**
* Get the smart name of the given state.
*/
static getSmartName(states, id, instanceId, noCommon) {
if (!id) {
if (!noCommon) {
if (!states.common) {
return states.smartName;
}
if (states && !states.common) {
return states.smartName;
}
return states.common.smartName;
}
if (states && !states.common) {
return states.smartName;
}
const obj = states;
return obj?.common?.custom && obj.common.custom[instanceId]
? obj.common.custom[instanceId].smartName
: undefined;
}
if (!noCommon) {
return states[id].common.smartName;
}
const obj = states[id];
return obj?.common?.custom && obj.common.custom[instanceId]
? obj.common.custom[instanceId].smartName || null
: null;
}
/**
* Get the smart name from a state.
*/
static getSmartNameFromObj(obj, instanceId, noCommon) {
if (!noCommon) {
if (!obj.common) {
return obj.smartName;
}
if (obj && !obj.common) {
return obj.smartName;
}
return obj.common.smartName;
}
if (obj && !obj.common) {
return obj.smartName;
}
const custom = obj?.common?.custom?.[instanceId];
return custom ? custom.smartName : undefined;
}
/**
* Enable smart name for a state.
*/
static enableSmartName(obj, instanceId, noCommon) {
// Typing must be fixed in js-controller
const sureStateObject = obj;
if (noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[instanceId] ||= {};
sureStateObject.common.custom[instanceId].smartName = {};
}
else {
sureStateObject.common.smartName = {};
}
}
/**
* Completely remove smart name from a state.
*/
static removeSmartName(obj, instanceId, noCommon) {
// Typing must be fixed in js-controller
const sureStateObject = obj;
if (noCommon) {
if (sureStateObject?.common?.custom?.[instanceId]) {
sureStateObject.common.custom[instanceId] = null;
}
}
else {
sureStateObject.common.smartName = null;
}
}
/**
* Update the smart name of a state.
*
* @deprecated Use updateSmartNameEx instead
*/
static updateSmartName(obj, newSmartName, byON, smartType, instanceId, noCommon) {
const language = I18n.getLanguage();
// Typing must be fixed in js-controller
const sureStateObject = obj;
// convert the old format
if (typeof sureStateObject.common.smartName === 'string') {
const nnn = sureStateObject.common.smartName;
sureStateObject.common.smartName = {};
sureStateObject.common.smartName[language] = nnn;
}
// convert the old settings
if (sureStateObject.native?.byON) {
delete sureStateObject.native.byON;
let _smartName = sureStateObject.common.smartName;
if (_smartName && typeof _smartName !== 'object') {
_smartName = {
en: _smartName,
[language]: _smartName,
};
}
sureStateObject.common.smartName = _smartName;
}
if (smartType !== undefined) {
if (noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[instanceId] ||= {};
sureStateObject.common.custom[instanceId].smartName ||= {};
if (!smartType) {
delete sureStateObject.common.custom[instanceId].smartName.smartType;
}
else {
sureStateObject.common.custom[instanceId].smartName.smartType = smartType;
}
}
else {
sureStateObject.common.smartName ||= {};
if (!smartType) {
delete sureStateObject.common.smartName.smartType;
}
else {
sureStateObject.common.smartName.smartType = smartType;
}
}
}
if (byON !== undefined) {
if (noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[instanceId] ||= {};
sureStateObject.common.custom[instanceId].smartName ||= {};
sureStateObject.common.custom[instanceId].smartName.byON = byON;
}
else {
sureStateObject.common.smartName ||= {};
sureStateObject.common.smartName.byON = byON;
}
}
if (newSmartName !== undefined) {
let smartName;
if (noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[instanceId] ||= {};
sureStateObject.common.custom[instanceId].smartName ||= {};
smartName = sureStateObject.common.custom[instanceId].smartName;
}
else {
sureStateObject.common.smartName ||= {};
smartName = sureStateObject.common.smartName;
}
smartName[language] = newSmartName;
// If smart name deleted
if (smartName &&
(!smartName[language] ||
(smartName[language] === sureStateObject.common.name && !sureStateObject.common.role))) {
delete smartName[language];
let empty = true;
// Check if the structure has any definitions
for (const key in smartName) {
if (Object.prototype.hasOwnProperty.call(smartName, key)) {
empty = false;
break;
}
}
// If empty => delete smartName completely
if (empty) {
if (noCommon && sureStateObject.common.custom?.[instanceId]) {
if (sureStateObject.common.custom[instanceId].smartName.byON === undefined) {
delete sureStateObject.common.custom[instanceId];
}
else {
delete sureStateObject.common.custom[instanceId].en;
delete sureStateObject.common.custom[instanceId].de;
delete sureStateObject.common.custom[instanceId].ru;
delete sureStateObject.common.custom[instanceId].nl;
delete sureStateObject.common.custom[instanceId].pl;
delete sureStateObject.common.custom[instanceId].it;
delete sureStateObject.common.custom[instanceId].fr;
delete sureStateObject.common.custom[instanceId].pt;
delete sureStateObject.common.custom[instanceId].es;
delete sureStateObject.common.custom[instanceId].uk;
delete sureStateObject.common.custom[instanceId]['zh-cn'];
}
}
else if (sureStateObject.common.smartName &&
sureStateObject.common.smartName.byON !== undefined) {
const _smartName = sureStateObject.common
.smartName;
delete _smartName.en;
delete _smartName.de;
delete _smartName.ru;
delete _smartName.nl;
delete _smartName.pl;
delete _smartName.it;
delete _smartName.fr;
delete _smartName.pt;
delete _smartName.es;
delete _smartName.uk;
delete _smartName['zh-cn'];
}
else {
sureStateObject.common.smartName = null;
}
}
}
}
}
/**
* Update the smart name of a state.
*/
static updateSmartNameEx(obj, options) {
const language = I18n.getLanguage();
// Typing must be fixed in js-controller
const sureStateObject = obj;
// convert the old format
if (typeof sureStateObject.common.smartName === 'string') {
const nnn = sureStateObject.common.smartName;
sureStateObject.common.smartName = {};
sureStateObject.common.smartName[language] = nnn;
}
// convert the old settings
if (sureStateObject.native?.byON) {
delete sureStateObject.native.byON;
let _smartName = sureStateObject.common.smartName;
if (_smartName && typeof _smartName !== 'object') {
_smartName = {
en: _smartName,
[language]: _smartName,
};
}
sureStateObject.common.smartName = _smartName;
}
if (options.smartType !== undefined) {
if (options.noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[options.instanceId] ||= {};
sureStateObject.common.custom[options.instanceId].smartName ||= {};
if (!options.smartType) {
delete sureStateObject.common.custom[options.instanceId].smartName.smartType;
}
else {
sureStateObject.common.custom[options.instanceId].smartName.smartType = options.smartType;
}
}
else {
sureStateObject.common.smartName ||= {};
if (!options.smartType) {
delete sureStateObject.common.smartName.smartType;
}
else {
sureStateObject.common.smartName.smartType = options.smartType;
}
}
}
if (options.byON !== undefined) {
if (options.noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[options.instanceId] ||= {};
sureStateObject.common.custom[options.instanceId].smartName ||= {};
sureStateObject.common.custom[options.instanceId].smartName.byON = options.byON;
}
else {
sureStateObject.common.smartName ||= {};
sureStateObject.common.smartName.byON = options.byON;
}
}
if (options.noAutoDetect !== undefined) {
if (options.noCommon) {
if (options.noAutoDetect) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[options.instanceId] ||= {};
sureStateObject.common.custom[options.instanceId].smartName ||= {};
sureStateObject.common.custom[options.instanceId].smartName.noAutoDetect = options.noAutoDetect;
}
else if (sureStateObject.common.custom?.[options.instanceId]?.smartName) {
delete sureStateObject.common.custom[options.instanceId].smartName.noAutoDetect;
}
}
else {
if (!options.noAutoDetect && sureStateObject.common.smartName) {
// @ts-expect-error must be fixed in js-controller
delete sureStateObject.common.smartName.noAutoDetect;
}
else {
sureStateObject.common.smartName ||= {};
// @ts-expect-error must be fixed in js-controller
sureStateObject.common.smartName.noAutoDetect = options.noAutoDetect;
}
}
}
if (options.smartName !== undefined) {
let smartName;
if (options.noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[options.instanceId] ||= {};
sureStateObject.common.custom[options.instanceId].smartName ||= {};
smartName = sureStateObject.common.custom[options.instanceId].smartName;
}
else {
sureStateObject.common.smartName ||= {};
smartName = sureStateObject.common.smartName;
}
smartName[language] = options.smartName;
// If smart name deleted
if (smartName &&
(!smartName[language] ||
(smartName[language] === sureStateObject.common.name && !sureStateObject.common.role))) {
delete smartName[language];
let empty = true;
// Check if the structure has any definitions
for (const key in smartName) {
if (Object.prototype.hasOwnProperty.call(smartName, key)) {
empty = false;
break;
}
}
// If empty => delete smartName completely
if (empty) {
if (options.noCommon && sureStateObject.common.custom?.[options.instanceId]) {
if (sureStateObject.common.custom[options.instanceId].smartName.byON === undefined) {
delete sureStateObject.common.custom[options.instanceId];
}
else {
delete sureStateObject.common.custom[options.instanceId].en;
delete sureStateObject.common.custom[options.instanceId].de;
delete sureStateObject.common.custom[options.instanceId].ru;
delete sureStateObject.common.custom[options.instanceId].nl;
delete sureStateObject.common.custom[options.instanceId].pl;
delete sureStateObject.common.custom[options.instanceId].it;
delete sureStateObject.common.custom[options.instanceId].fr;
delete sureStateObject.common.custom[options.instanceId].pt;
delete sureStateObject.common.custom[options.instanceId].es;
delete sureStateObject.common.custom[options.instanceId].uk;
delete sureStateObject.common.custom[options.instanceId]['zh-cn'];
}
}
else if (sureStateObject.common.smartName &&
sureStateObject.common.smartName.byON !== undefined) {
const _smartName = sureStateObject.common
.smartName;
delete _smartName.en;
delete _smartName.de;
delete _smartName.ru;
delete _smartName.nl;
delete _smartName.pl;
delete _smartName.it;
delete _smartName.fr;
delete _smartName.pt;
delete _smartName.es;
delete _smartName.uk;
delete _smartName['zh-cn'];
}
else {
sureStateObject.common.smartName = null;
}
}
}
}
}
/**
* Disable the smart name of a state.
*/
static disableSmartName(obj, instanceId, noCommon) {
// Typing must be fixed in js-controller
const sureStateObject = obj;
if (noCommon) {
sureStateObject.common.custom ||= {};
sureStateObject.common.custom[instanceId] ||= {};
sureStateObject.common.custom[instanceId].smartName = false;
}
else {
sureStateObject.common.smartName = false;
}
}
/**
* Copy text to the clipboard.
*/
static copyToClipboard(text, e) {
if (e) {
e.stopPropagation();
e.preventDefault();
}
return copy(text);
}
/**
* Gets the extension of a file name.
*
* @param fileName the file name.
* @returns The extension in lower case.
*/
static getFileExtension(fileName) {
const pos = (fileName || '').lastIndexOf('.');
if (pos !== -1) {
return fileName.substring(pos + 1).toLowerCase();
}
return null;
}
/**
* Format number of bytes as a string with B, KB, MB or GB.
* The base for all calculations is 1024.
*
* @param bytes The number of bytes.
* @returns The formatted string (e.g. '723.5 KB')
*/
static formatBytes(bytes) {
if (Math.abs(bytes) < 1024) {
return `${bytes} B`;
}
const units = ['KB', 'MB', 'GB'];
// const units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
let u = -1;
do {
bytes /= 1024;
++u;
} while (Math.abs(bytes) >= 1024 && u < units.length - 1);
return `${bytes.toFixed(1)} ${units[u]}`;
}
/**
* Invert the given color according to a theme type to get the inverted text color for background
*
* @param color Color in the format '#rrggbb' or '#rgb' (or without a hash)
* @param themeType 'light' or 'dark'
* @param invert If true, the dark theme has a light color in the control, or the dark theme has a light color in the control
*/
static getInvertedColor(color, themeType, invert) {
if (!color) {
return undefined;
}
const invertedColor = Utils.invertColor(color, true);
if (invertedColor === '#FFFFFF' && (themeType === 'dark' || (invert && themeType === 'light'))) {
return '#DDD';
}
if (invertedColor === '#000000' && (themeType === 'light' || (invert && themeType === 'dark'))) {
return '#222';
}
return undefined;
}
// Big thanks to: https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
/**
* Invert the given color
*
* @param hex Color in the format '#rrggbb' or '#rgb' (or without a hash)
* @param bw Set to black or white.
*/
static invertColor(hex, bw) {
if (!hex || typeof hex !== 'string') {
return '';
}
if (hex.startsWith('rgba')) {
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
if (m) {
hex =
parseInt(m[1], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0');
}
}
else if (hex.startsWith('rgb')) {
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m) {
hex =
parseInt(m[1], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0');
}
}
else if (hex.startsWith('#')) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
let alfa = null;
if (hex.length === 8) {
alfa = hex.substring(6, 8);
hex = hex.substring(0, 6);
}
else if (hex.length !== 6) {
console.warn(`Cannot invert color: ${hex}`);
return hex;
}
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
if (bw) {
// http://stackoverflow.com/a/3943023/112731
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? `#000000${alfa || ''}` : `#FFFFFF${alfa || ''}`;
}
// invert color components
const rs = (255 - r).toString(16);
const gs = (255 - g).toString(16);
const bd = (255 - b).toString(16);
// pad each with zeros and return
return `#${rs.padStart(2, '0')}${gs.padStart(2, '0')}${bd.padStart(2, '0')}${alfa || ''}`;
}
/**
* Convert RGB to array [r, g, b]
*
* @param hex Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
* @returns Array with 3 elements [r, g, b]
*/
static color2rgb(hex) {
if (hex === undefined || hex === null || hex === '' || typeof hex !== 'string') {
return false;
}
if (hex.startsWith('rgba')) {
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
if (m) {
hex =
parseInt(m[1], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0');
}
}
else if (hex.startsWith('rgb')) {
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
if (m) {
hex =
parseInt(m[1], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0') +
parseInt(m[2], 10).toString(16).padStart(2, '0');
}
}
else if (hex.startsWith('#')) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6 && hex.length !== 8) {
console.warn(`Cannot invert color: ${hex}`);
return false;
}
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
}
// Big thanks to: https://github.com/antimatter15/rgb-lab
/**
* Convert RGB to LAB
*
* @param rgb color in format [r,g,b]
* @returns lab color in format [l,a,b]
*/
static rgb2lab(rgb) {
let r = rgb[0] / 255;
let g = rgb[1] / 255;
let b = rgb[2] / 255;
r = r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
g = g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
b = b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;
let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
let y = r * 0.2126 + g * 0.7152 + b * 0.0722; /* / 1.00000; */
let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
x = x > 0.008856 ? x ** 0.33333333 : 7.787 * x + 0.137931; // 16 / 116;
y = y > 0.008856 ? y ** 0.33333333 : 7.787 * y + 0.137931; // 16 / 116;
z = z > 0.008856 ? z ** 0.33333333 : 7.787 * z + 0.137931; // 16 / 116;
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
}
/**
* Calculate the distance between two colors in LAB color space in the range 0-100^2
* If the distance is less than 1000, the colors are similar
*
* @param color1 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
* @param color2 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
* @returns distance in the range 0-100^2
*/
static colorDistance(color1, color2) {
const rgb1 = Utils.color2rgb(color1);
const rgb2 = Utils.color2rgb(color2);
if (!rgb1 || !rgb2) {
return 0;
}
const lab1 = Utils.rgb2lab(rgb1);
const lab2 = Utils.rgb2lab(rgb2);
const dltL = lab1[0] - lab2[0];
const dltA = lab1[1] - lab2[1];
const dltB = lab1[2] - lab2[2];
const c1 = Math.sqrt(lab1[1] * lab1[1] + lab1[2] * lab1[2]);
const c2 = Math.sqrt(lab2[1] * lab2[1] + lab2[2] * lab2[2]);
const dltC = c1 - c2;
let dltH = dltA * dltA + dltB * dltB - dltC * dltC;
dltH = dltH < 0 ? 0 : Math.sqrt(dltH);
const sc = 1.0 + 0.045 * c1;
const sh = 1.0 + 0.015 * c1;
const dltLKlsl = dltL;
const dltCkcsc = dltC / sc;
const dltHkhsh = dltH / sh;
const i = dltLKlsl * dltLKlsl + dltCkcsc * dltCkcsc + dltHkhsh * dltHkhsh;
return i < 0 ?