UNPKG

@iobroker/adapter-react-v5

Version:

React components to develop ioBroker interfaces with react.

1,278 lines 66.6 kB
/** * 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 ?