@iobroker/adapter-react-v5 6.1.10 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Components/404.js +13 -13
- package/Components/Loader.js +223 -223
- package/Components/Loaders/PT.css +108 -108
- package/Components/Loaders/PT.js +103 -103
- package/Components/Loaders/Vendor.css +13 -13
- package/Components/Loaders/Vendor.js +7 -7
- package/Components/ObjectBrowser.js +29 -29
- package/Components/UploadImage.js +305 -305
- package/Components/loader.css +221 -221
- package/Components/types.d.ts +82 -82
- package/GenericApp.js +49 -49
- package/LICENSE +22 -22
- package/Prompt.js +7 -7
- package/README.md +1004 -1005
- package/Theme.js +8 -7
- package/assets/devices/Alarm Systems.svg +18 -18
- package/assets/devices/Amplifier.svg +21 -21
- package/assets/devices/Awnings.svg +4 -4
- package/assets/devices/Battery Status.svg +4 -4
- package/assets/devices/Ceiling Spotlights.svg +15 -15
- package/assets/devices/Chandelier.svg +6 -6
- package/assets/devices/Climate.svg +11 -11
- package/assets/devices/Coffee Makers.svg +5 -5
- package/assets/devices/Cold Water.svg +31 -31
- package/assets/devices/Computer.svg +21 -21
- package/assets/devices/Consumption.svg +7 -7
- package/assets/devices/Curtains.svg +43 -43
- package/assets/devices/Dishwashers.svg +11 -11
- package/assets/devices/Doors.svg +5 -5
- package/assets/devices/Doorstep.svg +35 -35
- package/assets/devices/Dryer.svg +13 -13
- package/assets/devices/Fan.svg +20 -20
- package/assets/devices/Floor Lamps.svg +4 -4
- package/assets/devices/Garage Doors.svg +9 -9
- package/assets/devices/Gates.svg +32 -32
- package/assets/devices/Hairdryer.svg +23 -23
- package/assets/devices/Handle.svg +6 -6
- package/assets/devices/Hanging Lamps.svg +8 -8
- package/assets/devices/Heater.svg +44 -44
- package/assets/devices/Hoods.svg +11 -11
- package/assets/devices/Hot Water.svg +9 -9
- package/assets/devices/Humidity.svg +41 -41
- package/assets/devices/Iron.svg +4 -4
- package/assets/devices/Irrigation.svg +22 -22
- package/assets/devices/Led Strip.svg +30 -30
- package/assets/devices/Light.svg +29 -29
- package/assets/devices/Lightings.svg +46 -46
- package/assets/devices/Lock.svg +19 -19
- package/assets/devices/Louvre.svg +6 -6
- package/assets/devices/Mowing Machine.svg +8 -8
- package/assets/devices/Music.svg +12 -12
- package/assets/devices/Outdoor Blinds.svg +6 -6
- package/assets/devices/People.svg +19 -19
- package/assets/devices/Pool.svg +7 -7
- package/assets/devices/Power Consumption.svg +12 -12
- package/assets/devices/Printer.svg +9 -9
- package/assets/devices/Pump.svg +9 -9
- package/assets/devices/Receiver.svg +18 -18
- package/assets/devices/Sconces.svg +9 -9
- package/assets/devices/Security.svg +34 -34
- package/assets/devices/Shading.svg +4 -4
- package/assets/devices/Shutters.svg +10 -10
- package/assets/devices/SmokeDetector.svg +12 -12
- package/assets/devices/Sockets.svg +13 -13
- package/assets/devices/Speaker.svg +35 -35
- package/assets/devices/Stove.svg +11 -11
- package/assets/devices/Table Lamps.svg +11 -11
- package/assets/devices/Temperature Sensors.svg +28 -28
- package/assets/devices/Tv.svg +7 -7
- package/assets/devices/Vacuum Cleaner.svg +15 -15
- package/assets/devices/Ventilation.svg +12 -12
- package/assets/devices/Washing Machines.svg +15 -15
- package/assets/devices/Water Consumption.svg +5 -5
- package/assets/devices/Water Heater.svg +8 -8
- package/assets/devices/Water.svg +40 -40
- package/assets/devices/Weather.svg +28 -28
- package/assets/devices/Window.svg +7 -7
- package/assets/lamp_ceiling.svg +8 -8
- package/assets/lamp_table.svg +7 -7
- package/assets/no_icon.svg +9 -9
- package/assets/rooms/Anteroom.svg +52 -52
- package/assets/rooms/Attic.svg +21 -21
- package/assets/rooms/Balcony.svg +12 -12
- package/assets/rooms/Barn.svg +5 -5
- package/assets/rooms/Basement.svg +4 -4
- package/assets/rooms/Bathroom.svg +38 -38
- package/assets/rooms/Bedroom.svg +5 -5
- package/assets/rooms/Boiler Room.svg +12 -12
- package/assets/rooms/Carport.svg +17 -17
- package/assets/rooms/Cellar.svg +89 -89
- package/assets/rooms/Chamber.svg +9 -9
- package/assets/rooms/Corridor.svg +52 -52
- package/assets/rooms/Dining Area.svg +37 -37
- package/assets/rooms/Dining Room.svg +37 -37
- package/assets/rooms/Dining.svg +37 -37
- package/assets/rooms/Dressing Room.svg +4 -4
- package/assets/rooms/Driveway.svg +14 -14
- package/assets/rooms/Entrance.svg +44 -44
- package/assets/rooms/Equipment Room.svg +14 -14
- package/assets/rooms/Front Yard.svg +64 -64
- package/assets/rooms/Gallery.svg +13 -13
- package/assets/rooms/Garage.svg +20 -20
- package/assets/rooms/Garden.svg +12 -12
- package/assets/rooms/Ground Floor.svg +95 -95
- package/assets/rooms/Guest Bathroom.svg +32 -32
- package/assets/rooms/Guest Room.svg +5 -5
- package/assets/rooms/Gym.svg +4 -4
- package/assets/rooms/Hall.svg +19 -19
- package/assets/rooms/Home Theater.svg +7 -7
- package/assets/rooms/Kitchen.svg +17 -17
- package/assets/rooms/Laundry Room.svg +11 -11
- package/assets/rooms/Living Area.svg +10 -10
- package/assets/rooms/Living Room.svg +10 -10
- package/assets/rooms/Locker Room.svg +16 -16
- package/assets/rooms/Nursery.svg +4 -4
- package/assets/rooms/Office.svg +8 -8
- package/assets/rooms/Outdoors.svg +7 -7
- package/assets/rooms/Playroom.svg +5 -5
- package/assets/rooms/Pool.svg +7 -7
- package/assets/rooms/Rear Wall.svg +30 -30
- package/assets/rooms/Second Floor.svg +95 -95
- package/assets/rooms/Shed.svg +16 -16
- package/assets/rooms/Sleeping Area.svg +22 -22
- package/assets/rooms/Stairway.svg +4 -4
- package/assets/rooms/Stairwell.svg +15 -15
- package/assets/rooms/Storeroom.svg +4 -4
- package/assets/rooms/Summer House.svg +27 -27
- package/assets/rooms/Swimming Pool.svg +21 -21
- package/assets/rooms/Terrace.svg +6 -6
- package/assets/rooms/Toilet.svg +10 -10
- package/assets/rooms/Upstairs.svg +5 -5
- package/assets/rooms/Wardrobe.svg +60 -60
- package/assets/rooms/Washroom.svg +19 -19
- package/assets/rooms/Wc.svg +10 -10
- package/assets/rooms/Windscreen.svg +60 -60
- package/assets/rooms/Workshop.svg +22 -22
- package/assets/rooms/Workspace.svg +8 -8
- package/craco-module-federation.js +71 -71
- package/icons/IconFx.js +1 -1
- package/icons/IconLogout.js +1 -1
- package/index.css +54 -54
- package/modulefederation.admin.config.js +31 -31
- package/package.json +4 -4
- package/src/AdminConnection.tsx +3 -3
- package/src/Components/404.tsx +121 -121
- package/src/Components/ColorPicker.tsx +315 -315
- package/src/Components/ComplexCron.tsx +507 -507
- package/src/Components/CopyToClipboard.tsx +165 -165
- package/src/Components/CustomModal.tsx +163 -163
- package/src/Components/FileBrowser.tsx +2414 -2414
- package/src/Components/FileViewer.tsx +393 -393
- package/src/Components/Icon.tsx +210 -210
- package/src/Components/IconPicker.tsx +149 -149
- package/src/Components/IconSelector.tsx +2202 -2202
- package/src/Components/Image.tsx +176 -176
- package/src/Components/Loader.tsx +304 -304
- package/src/Components/Logo.tsx +166 -166
- package/src/Components/MDUtils.tsx +100 -100
- package/src/Components/ObjectBrowser.tsx +8032 -8032
- package/src/Components/Router.tsx +90 -90
- package/src/Components/SaveCloseButtons.tsx +113 -113
- package/src/Components/Schedule.tsx +1724 -1724
- package/src/Components/SelectWithIcon.tsx +197 -197
- package/src/Components/TabContainer.tsx +55 -55
- package/src/Components/TabContent.tsx +37 -37
- package/src/Components/TabHeader.tsx +19 -19
- package/src/Components/TableResize.tsx +259 -259
- package/src/Components/TextWithIcon.tsx +148 -148
- package/src/Components/ToggleThemeMenu.tsx +34 -34
- package/src/Components/TreeTable.tsx +919 -919
- package/src/Components/UploadImage.tsx +599 -599
- package/src/Components/Utils.tsx +1794 -1794
- package/src/Components/loader.css +221 -221
- package/src/Components/withWidth.tsx +21 -21
- package/src/Connection.tsx +7 -7
- package/src/Dialogs/ComplexCron.tsx +129 -129
- package/src/Dialogs/Confirm.tsx +162 -162
- package/src/Dialogs/Cron.tsx +182 -182
- package/src/Dialogs/Error.tsx +72 -72
- package/src/Dialogs/Message.tsx +71 -71
- package/src/Dialogs/SelectFile.tsx +270 -270
- package/src/Dialogs/SelectID.tsx +298 -298
- package/src/Dialogs/SimpleCron.tsx +100 -100
- package/src/Dialogs/TextInput.tsx +107 -107
- package/src/GenericApp.tsx +976 -976
- package/src/LegacyConnection.tsx +3589 -3589
- package/src/Prompt.tsx +20 -20
- package/src/Theme.tsx +479 -479
- package/src/icons/IconAdapter.tsx +20 -20
- package/src/icons/IconAlias.tsx +20 -20
- package/src/icons/IconChannel.tsx +21 -21
- package/src/icons/IconClearFilter.tsx +22 -22
- package/src/icons/IconClosed.tsx +17 -17
- package/src/icons/IconCopy.tsx +16 -16
- package/src/icons/IconDevice.tsx +27 -27
- package/src/icons/IconDocument.tsx +17 -17
- package/src/icons/IconDocumentReadOnly.tsx +18 -18
- package/src/icons/IconExpert.tsx +18 -18
- package/src/icons/IconFx.tsx +36 -36
- package/src/icons/IconInstance.tsx +20 -20
- package/src/icons/IconLogout.tsx +30 -30
- package/src/icons/IconNoIcon.tsx +19 -19
- package/src/icons/IconOpen.tsx +17 -17
- package/src/icons/IconProps.tsx +15 -15
- package/src/icons/IconState.tsx +17 -17
- package/src/index.css +54 -54
- package/types.d.ts +134 -134
package/src/Components/Utils.tsx
CHANGED
|
@@ -1,1794 +1,1794 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright 2018-2024 Denis Haev <dogafox@gmail.com>
|
|
3
|
-
*
|
|
4
|
-
* MIT License
|
|
5
|
-
*
|
|
6
|
-
* */
|
|
7
|
-
import React from 'react';
|
|
8
|
-
import copy from './CopyToClipboard';
|
|
9
|
-
import I18n from '../i18n';
|
|
10
|
-
import { IobTheme, ThemeName, ThemeType } from '../types';
|
|
11
|
-
|
|
12
|
-
const NAMESPACE = 'material';
|
|
13
|
-
const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
14
|
-
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
15
|
-
const QUALITY_BITS: Record<ioBroker.STATE_QUALITY[keyof ioBroker.STATE_QUALITY], string> = {
|
|
16
|
-
0x00: '0x00 - good',
|
|
17
|
-
|
|
18
|
-
0x01: '0x01 - general problem',
|
|
19
|
-
0x02: '0x02 - no connection problem',
|
|
20
|
-
|
|
21
|
-
0x10: '0x10 - substitute value from controller',
|
|
22
|
-
0x20: '0x20 - substitute initial value',
|
|
23
|
-
0x40: '0x40 - substitute value from device or instance',
|
|
24
|
-
0x80: '0x80 - substitute value from sensor',
|
|
25
|
-
|
|
26
|
-
0x11: '0x11 - general problem by instance',
|
|
27
|
-
0x41: '0x41 - general problem by device',
|
|
28
|
-
0x81: '0x81 - general problem by sensor',
|
|
29
|
-
|
|
30
|
-
0x12: '0x12 - instance not connected',
|
|
31
|
-
0x42: '0x42 - device not connected',
|
|
32
|
-
0x82: '0x82 - sensor not connected',
|
|
33
|
-
|
|
34
|
-
0x44: '0x44 - device reports error',
|
|
35
|
-
0x84: '0x84 - sensor reports error',
|
|
36
|
-
};
|
|
37
|
-
const SIGNATURES: Record<string, string> = {
|
|
38
|
-
JVBERi0: 'pdf',
|
|
39
|
-
R0lGODdh: 'gif',
|
|
40
|
-
R0lGODlh: 'gif',
|
|
41
|
-
iVBORw0KGgo: 'png',
|
|
42
|
-
'/9j/': 'jpg',
|
|
43
|
-
PHN2Zw: 'svg',
|
|
44
|
-
Qk1: 'bmp',
|
|
45
|
-
AAABAA: 'ico', // 00 00 01 00 according to https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
type SmartName = null
|
|
49
|
-
| false
|
|
50
|
-
| string
|
|
51
|
-
| ({ [lang in ioBroker.Languages]?: string } & {
|
|
52
|
-
/** Which kind of device it is */
|
|
53
|
-
smartType?: string | null;
|
|
54
|
-
/** Which value to set when the ON command is issued */
|
|
55
|
-
byON?: string | null;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
type ClassDictionary = Record<string, any>;
|
|
59
|
-
// eslint-disable-next-line no-use-before-define
|
|
60
|
-
type ClassValue = ClassArray | ClassDictionary | string | number | null | boolean | undefined;
|
|
61
|
-
type ClassArray = ClassValue[];
|
|
62
|
-
|
|
63
|
-
class Utils {
|
|
64
|
-
static namespace = NAMESPACE;
|
|
65
|
-
|
|
66
|
-
static INSTANCES = 'instances';
|
|
67
|
-
|
|
68
|
-
static dateFormat = ['DD', 'MM'];
|
|
69
|
-
|
|
70
|
-
static FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Capitalize words.
|
|
74
|
-
*/
|
|
75
|
-
static CapitalWords(name: string | null | undefined): string {
|
|
76
|
-
return (name || '')
|
|
77
|
-
.split(/[\s_]/)
|
|
78
|
-
.filter(item => item)
|
|
79
|
-
.map(word => (word ? word[0].toUpperCase() + word.substring(1).toLowerCase() : ''))
|
|
80
|
-
.join(' ');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
static formatSeconds(seconds: number): string {
|
|
84
|
-
const days_ = Math.floor(seconds / (3600 * 24));
|
|
85
|
-
seconds %= 3600 * 24;
|
|
86
|
-
|
|
87
|
-
const hours = Math.floor(seconds / 3600)
|
|
88
|
-
.toString()
|
|
89
|
-
.padStart(2, '0');
|
|
90
|
-
seconds %= 3600;
|
|
91
|
-
|
|
92
|
-
const minutes = Math.floor(seconds / 60)
|
|
93
|
-
.toString()
|
|
94
|
-
.padStart(2, '0');
|
|
95
|
-
seconds %= 60;
|
|
96
|
-
|
|
97
|
-
const secondsStr = Math.floor(seconds).toString().padStart(2, '0');
|
|
98
|
-
|
|
99
|
-
let text = '';
|
|
100
|
-
if (days_) {
|
|
101
|
-
text += `${days_} ${I18n.t('ra_daysShortText')} `;
|
|
102
|
-
}
|
|
103
|
-
text += `${hours}:${minutes}:${secondsStr}`;
|
|
104
|
-
|
|
105
|
-
return text;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Get the name of the object by id from the name or description.
|
|
110
|
-
*/
|
|
111
|
-
static getObjectName(
|
|
112
|
-
objects: Record<string, ioBroker.Object>,
|
|
113
|
-
id: string,
|
|
114
|
-
settings?: { name: ioBroker.StringOrTranslated } | ioBroker.Languages | null,
|
|
115
|
-
options?: { language?: ioBroker.Languages },
|
|
116
|
-
/** Set to true to get the description. */
|
|
117
|
-
isDesc?: boolean,
|
|
118
|
-
): string {
|
|
119
|
-
const item = objects[id];
|
|
120
|
-
let text: string | undefined;
|
|
121
|
-
|
|
122
|
-
if (typeof settings === 'string' && !options) {
|
|
123
|
-
options = { language: settings };
|
|
124
|
-
settings = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
options = options || {};
|
|
128
|
-
if (!options.language) {
|
|
129
|
-
options.language =
|
|
130
|
-
(objects['system.config'] &&
|
|
131
|
-
objects['system.config'].common &&
|
|
132
|
-
objects['system.config'].common.language) ||
|
|
133
|
-
window.sysLang ||
|
|
134
|
-
'en';
|
|
135
|
-
}
|
|
136
|
-
if ((settings as { name: ioBroker.StringOrTranslated })?.name) {
|
|
137
|
-
const textObj = (settings as { name: ioBroker.StringOrTranslated }).name;
|
|
138
|
-
if (typeof textObj === 'object') {
|
|
139
|
-
text = (options.language && textObj[options.language]) || textObj.en;
|
|
140
|
-
} else {
|
|
141
|
-
text = textObj as string;
|
|
142
|
-
}
|
|
143
|
-
} else if (isDesc && item?.common?.desc) {
|
|
144
|
-
const textObj = item.common.desc;
|
|
145
|
-
if (typeof textObj === 'object') {
|
|
146
|
-
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
|
|
147
|
-
} else {
|
|
148
|
-
text = textObj as string;
|
|
149
|
-
}
|
|
150
|
-
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
151
|
-
|
|
152
|
-
if (text === text.toUpperCase()) {
|
|
153
|
-
text = text[0] + text.substring(1).toLowerCase();
|
|
154
|
-
}
|
|
155
|
-
} else if (!isDesc && item?.common) {
|
|
156
|
-
const textObj = item.common.name || item.common.desc;
|
|
157
|
-
if (textObj && typeof textObj === 'object') {
|
|
158
|
-
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
|
|
159
|
-
} else {
|
|
160
|
-
text = textObj as string;
|
|
161
|
-
}
|
|
162
|
-
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
163
|
-
|
|
164
|
-
if (text === text.toUpperCase()) {
|
|
165
|
-
text = text[0] + text.substring(1).toLowerCase();
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
const pos = id.lastIndexOf('.');
|
|
169
|
-
text = id.substring(pos + 1).replace(/[_.]/g, ' ');
|
|
170
|
-
text = Utils.CapitalWords(text);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return text?.trim() || '';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Get the name of the object from the name or description.
|
|
178
|
-
*/
|
|
179
|
-
static getObjectNameFromObj(
|
|
180
|
-
obj: ioBroker.PartialObject,
|
|
181
|
-
/** settings or language */
|
|
182
|
-
settings: { name: ioBroker.StringOrTranslated } | ioBroker.Languages | null,
|
|
183
|
-
options?: { language?: ioBroker.Languages },
|
|
184
|
-
/** Set to true to get the description. */
|
|
185
|
-
isDesc?: boolean,
|
|
186
|
-
/** Allow using spaces in name (by edit) */
|
|
187
|
-
noTrim?: boolean,
|
|
188
|
-
): string {
|
|
189
|
-
const item = obj;
|
|
190
|
-
let text = (obj && obj._id) || '';
|
|
191
|
-
|
|
192
|
-
if (typeof settings === 'string' && !options) {
|
|
193
|
-
options = { language: settings };
|
|
194
|
-
settings = null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
options = options || {};
|
|
198
|
-
|
|
199
|
-
if ((settings as { name: ioBroker.StringOrTranslated })?.name) {
|
|
200
|
-
const name = (settings as { name: ioBroker.StringOrTranslated }).name;
|
|
201
|
-
if (typeof name === 'object') {
|
|
202
|
-
text = (options.language && name[options.language]) || name.en;
|
|
203
|
-
} else {
|
|
204
|
-
text = name;
|
|
205
|
-
}
|
|
206
|
-
} else if (isDesc && item?.common?.desc) {
|
|
207
|
-
const desc: ioBroker.StringOrTranslated = item.common.desc;
|
|
208
|
-
if (typeof desc === 'object') {
|
|
209
|
-
text = (options.language && desc[options.language]) || desc.en;
|
|
210
|
-
} else {
|
|
211
|
-
text = desc;
|
|
212
|
-
}
|
|
213
|
-
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
214
|
-
|
|
215
|
-
if (text === text.toUpperCase()) {
|
|
216
|
-
text = text[0] + text.substring(1).toLowerCase();
|
|
217
|
-
}
|
|
218
|
-
} else if (!isDesc && item?.common?.name) {
|
|
219
|
-
let name = item.common.name;
|
|
220
|
-
if (!name && item.common.desc) {
|
|
221
|
-
name = item.common.desc;
|
|
222
|
-
}
|
|
223
|
-
if (typeof name === 'object') {
|
|
224
|
-
text = (options.language && name[options.language]) || name.en;
|
|
225
|
-
} else {
|
|
226
|
-
text = name;
|
|
227
|
-
}
|
|
228
|
-
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
229
|
-
|
|
230
|
-
if (text === text.toUpperCase()) {
|
|
231
|
-
text = text[0] + text.substring(1).toLowerCase();
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return noTrim ? text : text.trim();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Extracts from the object material settings, depends on username
|
|
239
|
-
*/
|
|
240
|
-
static getSettingsOrder(
|
|
241
|
-
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
242
|
-
forEnumId: string,
|
|
243
|
-
options: { user?: string },
|
|
244
|
-
): string | null {
|
|
245
|
-
let common: ioBroker.StateCommon | undefined;
|
|
246
|
-
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
247
|
-
common = (obj as ioBroker.StateObject).common;
|
|
248
|
-
} else {
|
|
249
|
-
common = obj as any as ioBroker.StateCommon;
|
|
250
|
-
}
|
|
251
|
-
let settings;
|
|
252
|
-
if (common?.custom) {
|
|
253
|
-
settings = common.custom[NAMESPACE];
|
|
254
|
-
const user = options.user || 'admin';
|
|
255
|
-
if (settings && settings[user]) {
|
|
256
|
-
if (forEnumId) {
|
|
257
|
-
if (settings[user].subOrder && settings[user].subOrder[forEnumId]) {
|
|
258
|
-
return JSON.parse(JSON.stringify(settings[user].subOrder[forEnumId]));
|
|
259
|
-
}
|
|
260
|
-
} else if (settings[user].order) {
|
|
261
|
-
return JSON.parse(JSON.stringify(settings[user].order));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
Used in material
|
|
270
|
-
*/
|
|
271
|
-
static getSettingsCustomURLs(
|
|
272
|
-
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
273
|
-
forEnumId: string,
|
|
274
|
-
options: { user?: string },
|
|
275
|
-
): string | null {
|
|
276
|
-
let common: ioBroker.StateCommon | undefined;
|
|
277
|
-
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
278
|
-
common = (obj as ioBroker.StateObject).common;
|
|
279
|
-
} else {
|
|
280
|
-
common = obj as any as ioBroker.StateCommon;
|
|
281
|
-
}
|
|
282
|
-
let settings;
|
|
283
|
-
if (common?.custom) {
|
|
284
|
-
settings = common.custom[NAMESPACE];
|
|
285
|
-
const user = options.user || 'admin';
|
|
286
|
-
if (settings && settings[user]) {
|
|
287
|
-
if (forEnumId) {
|
|
288
|
-
if (settings[user].subURLs && settings[user].subURLs[forEnumId]) {
|
|
289
|
-
return JSON.parse(JSON.stringify(settings[user].subURLs[forEnumId]));
|
|
290
|
-
}
|
|
291
|
-
} else if (settings[user].URLs) {
|
|
292
|
-
return JSON.parse(JSON.stringify(settings[user].URLs));
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Reorder the array items in list between source and dest.
|
|
301
|
-
*/
|
|
302
|
-
static reorder(
|
|
303
|
-
list: Iterable<any> | ArrayLike<any>,
|
|
304
|
-
source: number,
|
|
305
|
-
dest: number,
|
|
306
|
-
): Iterable<any> | ArrayLike<any> {
|
|
307
|
-
const result = Array.from(list);
|
|
308
|
-
const [removed] = result.splice(source, 1);
|
|
309
|
-
result.splice(dest, 0, removed);
|
|
310
|
-
return result;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
Get smart name settings for the given object.
|
|
315
|
-
*/
|
|
316
|
-
static getSettings(
|
|
317
|
-
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
318
|
-
options: {
|
|
319
|
-
id?: string;
|
|
320
|
-
user?: string;
|
|
321
|
-
name?: ioBroker.StringOrTranslated;
|
|
322
|
-
icon?: string;
|
|
323
|
-
color?: string;
|
|
324
|
-
language?: ioBroker.Languages;
|
|
325
|
-
},
|
|
326
|
-
defaultEnabling?: boolean,
|
|
327
|
-
) {
|
|
328
|
-
let settings;
|
|
329
|
-
const id = (obj as ioBroker.StateObject)?._id || options?.id;
|
|
330
|
-
let common: ioBroker.StateCommon | undefined;
|
|
331
|
-
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
332
|
-
common = (obj as ioBroker.StateObject).common;
|
|
333
|
-
} else {
|
|
334
|
-
common = obj as ioBroker.StateCommon;
|
|
335
|
-
}
|
|
336
|
-
if (common?.custom) {
|
|
337
|
-
settings = common.custom;
|
|
338
|
-
settings =
|
|
339
|
-
settings[NAMESPACE] && settings[NAMESPACE][options.user || 'admin']
|
|
340
|
-
? JSON.parse(JSON.stringify(settings[NAMESPACE][options.user || 'admin']))
|
|
341
|
-
: { enabled: true };
|
|
342
|
-
} else {
|
|
343
|
-
settings = { enabled: defaultEnabling === undefined ? true : defaultEnabling, useCustom: false };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!Object.prototype.hasOwnProperty.call(settings, 'enabled')) {
|
|
347
|
-
settings.enabled = defaultEnabling === undefined ? true : defaultEnabling;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (options) {
|
|
351
|
-
if (!settings.name && options.name) {
|
|
352
|
-
settings.name = options.name;
|
|
353
|
-
}
|
|
354
|
-
if (!settings.icon && options.icon) {
|
|
355
|
-
settings.icon = options.icon;
|
|
356
|
-
}
|
|
357
|
-
if (!settings.color && options.color) {
|
|
358
|
-
settings.color = options.color;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (common) {
|
|
363
|
-
if (!settings.color && common.color) {
|
|
364
|
-
settings.color = common.color;
|
|
365
|
-
}
|
|
366
|
-
if (!settings.icon && common.icon) {
|
|
367
|
-
settings.icon = common.icon;
|
|
368
|
-
}
|
|
369
|
-
if (!settings.name && common.name) {
|
|
370
|
-
settings.name = common.name;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (typeof settings.name === 'object') {
|
|
375
|
-
settings.name = (options.language && settings.name[options.language]) || settings.name.en;
|
|
376
|
-
|
|
377
|
-
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
|
|
378
|
-
|
|
379
|
-
if (settings.name === settings.name.toUpperCase()) {
|
|
380
|
-
settings.name = settings.name[0] + settings.name.substring(1).toLowerCase();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (!settings.name && id) {
|
|
384
|
-
const pos = id.lastIndexOf('.');
|
|
385
|
-
settings.name = id.substring(pos + 1).replace(/[_.]/g, ' ');
|
|
386
|
-
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
|
|
387
|
-
settings.name = Utils.CapitalWords(settings.name);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return settings;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
Sets smartName settings for the given object.
|
|
395
|
-
*/
|
|
396
|
-
static setSettings(
|
|
397
|
-
obj: Partial<ioBroker.Object>,
|
|
398
|
-
settings: Record<string, any>,
|
|
399
|
-
options: { user?: string; language?: ioBroker.Languages },
|
|
400
|
-
): boolean {
|
|
401
|
-
if (obj) {
|
|
402
|
-
obj.common = obj.common || ({} as ioBroker.StateCommon);
|
|
403
|
-
obj.common.custom = obj.common.custom || {};
|
|
404
|
-
obj.common.custom[NAMESPACE] = obj.common.custom[NAMESPACE] || {};
|
|
405
|
-
obj.common.custom[NAMESPACE][options.user || 'admin'] = settings;
|
|
406
|
-
const s = obj.common.custom[NAMESPACE][options.user || 'admin'];
|
|
407
|
-
if (s.useCommon) {
|
|
408
|
-
if (s.color !== undefined) {
|
|
409
|
-
obj.common.color = s.color;
|
|
410
|
-
delete s.color;
|
|
411
|
-
}
|
|
412
|
-
if (s.icon !== undefined) {
|
|
413
|
-
obj.common.icon = s.icon;
|
|
414
|
-
delete s.icon;
|
|
415
|
-
}
|
|
416
|
-
if (s.name !== undefined) {
|
|
417
|
-
if (typeof obj.common.name !== 'object' && options.language) {
|
|
418
|
-
obj.common.name = { [options.language]: s.name } as ioBroker.StringOrTranslated;
|
|
419
|
-
} else if (typeof obj.common.name === 'object' && options.language) {
|
|
420
|
-
obj.common.name[options.language] = s.name;
|
|
421
|
-
}
|
|
422
|
-
delete s.name;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return true;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
return false;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Get the icon for the given settings.
|
|
434
|
-
*/
|
|
435
|
-
static getIcon(
|
|
436
|
-
settings: { icon?: string; name?: string; prefix?: string },
|
|
437
|
-
style?: React.CSSProperties,
|
|
438
|
-
): React.JSX.Element | null {
|
|
439
|
-
if (settings?.icon) {
|
|
440
|
-
// If UTF-8 icon
|
|
441
|
-
if (settings.icon.length <= 2) {
|
|
442
|
-
return <span style={style || {}}>{settings.icon}</span>;
|
|
443
|
-
}
|
|
444
|
-
if (settings.icon.startsWith('data:image')) {
|
|
445
|
-
return <img alt={settings.name} src={settings.icon} style={style || {}} />;
|
|
446
|
-
}
|
|
447
|
-
// maybe later some changes for a second type
|
|
448
|
-
return <img alt={settings.name} src={(settings.prefix || '') + settings.icon} style={style} />;
|
|
449
|
-
}
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Get the icon for the given object.
|
|
455
|
-
*/
|
|
456
|
-
static getObjectIcon(id: string | ioBroker.PartialObject, obj?: ioBroker.PartialObject): string | null {
|
|
457
|
-
// If id is Object
|
|
458
|
-
if (typeof id === 'object') {
|
|
459
|
-
obj = id as ioBroker.PartialObject;
|
|
460
|
-
id = obj?._id as string;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (obj?.common?.icon) {
|
|
464
|
-
let icon = obj.common.icon;
|
|
465
|
-
// If UTF-8 icon
|
|
466
|
-
if (typeof icon === 'string' && icon.length <= 2) {
|
|
467
|
-
return icon;
|
|
468
|
-
}
|
|
469
|
-
if (icon.startsWith('data:image')) {
|
|
470
|
-
return icon;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const parts = id.split('.');
|
|
474
|
-
if (parts[0] === 'system') {
|
|
475
|
-
icon = `adapter/${parts[2]}${icon.startsWith('/') ? '' : '/'}${icon}`;
|
|
476
|
-
} else {
|
|
477
|
-
icon = `adapter/${parts[0]}${icon.startsWith('/') ? '' : '/'}${icon}`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (window.location.pathname.match(/adapter\/[^/]+\/[^/]+\.html/)) {
|
|
481
|
-
icon = `../../${icon}`;
|
|
482
|
-
} else if (window.location.pathname.match(/material\/[.\d]+/)) {
|
|
483
|
-
icon = `../../${icon}`;
|
|
484
|
-
} else if (window.location.pathname.match(/material\//)) {
|
|
485
|
-
icon = `../${icon}`;
|
|
486
|
-
}
|
|
487
|
-
return icon;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Converts word1_word2 to word1Word2.
|
|
495
|
-
*/
|
|
496
|
-
static splitCamelCase(text: string | null | undefined): string {
|
|
497
|
-
// if (false && text !== text.toUpperCase()) {
|
|
498
|
-
// const words = text.split(/\s+/);
|
|
499
|
-
// for (let i = 0; i < words.length; i++) {
|
|
500
|
-
// const word = words[i];
|
|
501
|
-
// if (word.toLowerCase() !== word && word.toUpperCase() !== word) {
|
|
502
|
-
// let z = 0;
|
|
503
|
-
// const ww = [];
|
|
504
|
-
// let start = 0;
|
|
505
|
-
// while (z < word.length) {
|
|
506
|
-
// if (word[z].match(/[A-ZÜÄÖА-Я]/)) {
|
|
507
|
-
// ww.push(word.substring(start, z));
|
|
508
|
-
// start = z;
|
|
509
|
-
// }
|
|
510
|
-
// z++;
|
|
511
|
-
// }
|
|
512
|
-
// if (start !== z) {
|
|
513
|
-
// ww.push(word.substring(start, z));
|
|
514
|
-
// }
|
|
515
|
-
// for (let k = 0; k < ww.length; k++) {
|
|
516
|
-
// words.splice(i + k, 0, ww[k]);
|
|
517
|
-
// }
|
|
518
|
-
// i += ww.length;
|
|
519
|
-
// }
|
|
520
|
-
// }
|
|
521
|
-
//
|
|
522
|
-
// return words.map(w => {
|
|
523
|
-
// w = w.trim();
|
|
524
|
-
// if (w) {
|
|
525
|
-
// return w[0].toUpperCase() + w.substring(1).toLowerCase();
|
|
526
|
-
// }
|
|
527
|
-
// return '';
|
|
528
|
-
// }).join(' ');
|
|
529
|
-
// }
|
|
530
|
-
return text ? Utils.CapitalWords(text) : '';
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Check if the given color is bright.
|
|
535
|
-
* https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
|
|
536
|
-
*/
|
|
537
|
-
static isUseBright(color: string | null | undefined, defaultValue?: boolean): boolean {
|
|
538
|
-
if (!color) {
|
|
539
|
-
return defaultValue === undefined ? true : defaultValue;
|
|
540
|
-
}
|
|
541
|
-
color = color.toString();
|
|
542
|
-
if (color.startsWith('#')) {
|
|
543
|
-
color = color.slice(1);
|
|
544
|
-
}
|
|
545
|
-
let r;
|
|
546
|
-
let g;
|
|
547
|
-
let b;
|
|
548
|
-
|
|
549
|
-
const rgb = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
|
550
|
-
if (rgb && rgb.length === 4) {
|
|
551
|
-
r = parseInt(rgb[1], 10);
|
|
552
|
-
g = parseInt(rgb[2], 10);
|
|
553
|
-
b = parseInt(rgb[3], 10);
|
|
554
|
-
} else {
|
|
555
|
-
// convert 3-digit hex to 6-digits.
|
|
556
|
-
if (color.length === 3) {
|
|
557
|
-
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
|
|
558
|
-
}
|
|
559
|
-
// remove alfa channel
|
|
560
|
-
if (color.length === 8) {
|
|
561
|
-
color = color.substring(0, 6);
|
|
562
|
-
} else if (color.length !== 6) {
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
r = parseInt(color.slice(0, 2), 16);
|
|
567
|
-
g = parseInt(color.slice(2, 4), 16);
|
|
568
|
-
b = parseInt(color.slice(4, 6), 16);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// http://stackoverflow.com/a/3943023/112731
|
|
572
|
-
return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Get the time string in the format 00:00.
|
|
577
|
-
*/
|
|
578
|
-
static getTimeString(seconds: string | number): string {
|
|
579
|
-
seconds = parseFloat(seconds as string);
|
|
580
|
-
if (Number.isNaN(seconds)) {
|
|
581
|
-
return '--:--';
|
|
582
|
-
}
|
|
583
|
-
const hours = Math.floor(seconds / 3600);
|
|
584
|
-
const minutes = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
|
|
585
|
-
const secs = (seconds % 60).toString().padStart(2, '0');
|
|
586
|
-
if (hours) {
|
|
587
|
-
return `${hours}:${minutes}:${secs}`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return `${minutes}:${secs}`;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* Gets the wind direction with the given angle (degrees).
|
|
595
|
-
*/
|
|
596
|
-
static getWindDirection(
|
|
597
|
-
/** angle in degrees from 0° to 360° */
|
|
598
|
-
angle: number,
|
|
599
|
-
): string {
|
|
600
|
-
if (angle >= 0 && angle < 11.25) {
|
|
601
|
-
return 'N';
|
|
602
|
-
}
|
|
603
|
-
if (angle >= 11.25 && angle < 33.75) {
|
|
604
|
-
return 'NNE';
|
|
605
|
-
}
|
|
606
|
-
if (angle >= 33.75 && angle < 56.25) {
|
|
607
|
-
return 'NE';
|
|
608
|
-
}
|
|
609
|
-
if (angle >= 56.25 && angle < 78.75) {
|
|
610
|
-
return 'ENE';
|
|
611
|
-
}
|
|
612
|
-
if (angle >= 78.75 && angle < 101.25) {
|
|
613
|
-
return 'E';
|
|
614
|
-
}
|
|
615
|
-
if (angle >= 101.25 && angle < 123.75) {
|
|
616
|
-
return 'ESE';
|
|
617
|
-
}
|
|
618
|
-
if (angle >= 123.75 && angle < 146.25) {
|
|
619
|
-
return 'SE';
|
|
620
|
-
}
|
|
621
|
-
if (angle >= 146.25 && angle < 168.75) {
|
|
622
|
-
return 'SSE';
|
|
623
|
-
}
|
|
624
|
-
if (angle >= 168.75 && angle < 191.25) {
|
|
625
|
-
return 'S';
|
|
626
|
-
}
|
|
627
|
-
if (angle >= 191.25 && angle < 213.75) {
|
|
628
|
-
return 'SSW';
|
|
629
|
-
}
|
|
630
|
-
if (angle >= 213.75 && angle < 236.25) {
|
|
631
|
-
return 'SW';
|
|
632
|
-
}
|
|
633
|
-
if (angle >= 236.25 && angle < 258.75) {
|
|
634
|
-
return 'WSW';
|
|
635
|
-
}
|
|
636
|
-
if (angle >= 258.75 && angle < 281.25) {
|
|
637
|
-
return 'W';
|
|
638
|
-
}
|
|
639
|
-
if (angle >= 281.25 && angle < 303.75) {
|
|
640
|
-
return 'WNW';
|
|
641
|
-
}
|
|
642
|
-
if (angle >= 303.75 && angle < 326.25) {
|
|
643
|
-
return 'NW';
|
|
644
|
-
}
|
|
645
|
-
if (angle >= 326.25 && angle < 348.75) {
|
|
646
|
-
return 'NNW';
|
|
647
|
-
}
|
|
648
|
-
// if (angle >= 348.75) {
|
|
649
|
-
return 'N';
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Pad the given number with a zero if it's not two digits long.
|
|
654
|
-
*/
|
|
655
|
-
static padding(num: string | number): string {
|
|
656
|
-
if (typeof num === 'string') {
|
|
657
|
-
if (num.length < 2) {
|
|
658
|
-
return `0${num}`;
|
|
659
|
-
}
|
|
660
|
-
return num;
|
|
661
|
-
}
|
|
662
|
-
if (num < 10) {
|
|
663
|
-
return `0${num}`;
|
|
664
|
-
}
|
|
665
|
-
return num.toString();
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Sets the date format.
|
|
670
|
-
*/
|
|
671
|
-
static setDataFormat(format: string): void {
|
|
672
|
-
if (format) {
|
|
673
|
-
Utils.dateFormat = format.toUpperCase().split(/[.-/]/);
|
|
674
|
-
Utils.dateFormat.splice(Utils.dateFormat.indexOf('YYYY'), 1);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Converts the date to a string.
|
|
680
|
-
*/
|
|
681
|
-
static date2string(now: string | number | Date): string {
|
|
682
|
-
if (typeof now === 'string') {
|
|
683
|
-
now = now.trim();
|
|
684
|
-
if (!now) {
|
|
685
|
-
return '';
|
|
686
|
-
}
|
|
687
|
-
// only letters
|
|
688
|
-
if (now.match(/^[\w\s]+$/)) {
|
|
689
|
-
// Day of the week
|
|
690
|
-
return now;
|
|
691
|
-
}
|
|
692
|
-
const m = now.match(/(\d{1,4})[-./](\d{1,2})[-./](\d{1,4})/);
|
|
693
|
-
if (m) {
|
|
694
|
-
const a = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
695
|
-
// We now have 3 numbers. Let's try to detect where is year, where is day and where is month
|
|
696
|
-
const year = a.find(y => y > 31);
|
|
697
|
-
if (year !== undefined) {
|
|
698
|
-
a.splice(a.indexOf(year), 1);
|
|
699
|
-
|
|
700
|
-
const day = a.find(mm => mm > 12);
|
|
701
|
-
if (day) {
|
|
702
|
-
a.splice(a.indexOf(day), 1);
|
|
703
|
-
now = new Date(year, a[0] - 1, day);
|
|
704
|
-
} else if (Utils.dateFormat[0][0] === 'M' && Utils.dateFormat[1][0] === 'D') {
|
|
705
|
-
// MM DD
|
|
706
|
-
now = new Date(year, a[0] - 1, a[1]);
|
|
707
|
-
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
|
|
708
|
-
now = new Date(year, a[1] - 1, a[0]);
|
|
709
|
-
}
|
|
710
|
-
} else if (Utils.dateFormat[0][0] === 'D' && Utils.dateFormat[1][0] === 'M') {
|
|
711
|
-
// DD MM
|
|
712
|
-
now = new Date(year, a[1] - 1, a[0]);
|
|
713
|
-
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
|
|
714
|
-
now = new Date(year, a[0] - 1, a[1]);
|
|
715
|
-
}
|
|
716
|
-
} else {
|
|
717
|
-
now = new Date(now);
|
|
718
|
-
}
|
|
719
|
-
} else {
|
|
720
|
-
now = new Date(now);
|
|
721
|
-
}
|
|
722
|
-
} else {
|
|
723
|
-
now = new Date(now);
|
|
724
|
-
}
|
|
725
|
-
} else {
|
|
726
|
-
now = new Date(now);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
let date = I18n.t(`ra_dow_${days[now.getDay()]}`).replace('ra_dow_', '');
|
|
730
|
-
date += `. ${now.getDate()} ${I18n.t(`ra_month_${months[now.getMonth()]}`).replace('ra_month_', '')}`;
|
|
731
|
-
return date;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Render a text as a link.
|
|
736
|
-
*/
|
|
737
|
-
static renderTextWithA(text: string): React.JSX.Element[] | string {
|
|
738
|
-
let m: RegExpMatchArray | null = text.match(/<a [^<]+<\/a>|<br\s?\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/);
|
|
739
|
-
if (m) {
|
|
740
|
-
const result: React.JSX.Element[] = [];
|
|
741
|
-
let key = 1;
|
|
742
|
-
do {
|
|
743
|
-
const start = text.substring(0, m.index);
|
|
744
|
-
text = text.substring((m.index || 0) + m[0].length);
|
|
745
|
-
start && result.push(<span key={`a${key++}`}>{start}</span>);
|
|
746
|
-
|
|
747
|
-
if (m[0].startsWith('<b>')) {
|
|
748
|
-
result.push(<b key={`a${key++}`}>{m[0].substring(3, m[0].length - 4)}</b>);
|
|
749
|
-
} else if (m[0].startsWith('<i>')) {
|
|
750
|
-
result.push(<i key={`a${key++}`}>{m[0].substring(3, m[0].length - 4)}</i>);
|
|
751
|
-
} else if (m[0].startsWith('<br')) {
|
|
752
|
-
result.push(<br key={`a${key++}`} />);
|
|
753
|
-
} else {
|
|
754
|
-
const href = m[0].match(/href="([^"]+)"/) || m[0].match(/href='([^']+)'/);
|
|
755
|
-
const target = m[0].match(/target="([^"]+)"/) || m[0].match(/target='([^']+)'/);
|
|
756
|
-
const rel = m[0].match(/rel="([^"]+)"/) || m[0].match(/rel='([^']+)'/);
|
|
757
|
-
const title = m[0].match(/>([^<]*)</);
|
|
758
|
-
|
|
759
|
-
// eslint-disable-next-line
|
|
760
|
-
result.push(<a
|
|
761
|
-
key={`a${key++}`}
|
|
762
|
-
href={href ? href[1] : ''}
|
|
763
|
-
target={target ? target[1] : '_blank'}
|
|
764
|
-
rel={rel ? rel[1] : ''}
|
|
765
|
-
style={{ color: 'inherit' }}
|
|
766
|
-
>
|
|
767
|
-
{title ? title[1] : ''}
|
|
768
|
-
</a>);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
m = text ? text.match(/<a [^<]+<\/a>|<br\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/) : null;
|
|
772
|
-
if (!m) {
|
|
773
|
-
text && result.push(<span key={`a${key++}`}>{text}</span>);
|
|
774
|
-
}
|
|
775
|
-
} while (m);
|
|
776
|
-
|
|
777
|
-
return result;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
return text;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Get the smart name of the given state.
|
|
785
|
-
*/
|
|
786
|
-
static getSmartName(
|
|
787
|
-
states: Record<string, ioBroker.StateObject> | ioBroker.StateObject | ioBroker.StateCommon,
|
|
788
|
-
id: string,
|
|
789
|
-
instanceId: string,
|
|
790
|
-
noCommon?: boolean,
|
|
791
|
-
): SmartName | undefined {
|
|
792
|
-
if (!id) {
|
|
793
|
-
if (!noCommon) {
|
|
794
|
-
if (!(states as ioBroker.StateObject).common) {
|
|
795
|
-
return (states as ioBroker.StateCommon).smartName;
|
|
796
|
-
}
|
|
797
|
-
if (states && !(states as ioBroker.StateObject).common) {
|
|
798
|
-
return (states as ioBroker.StateCommon).smartName;
|
|
799
|
-
}
|
|
800
|
-
return (states as ioBroker.StateObject).common.smartName;
|
|
801
|
-
}
|
|
802
|
-
if (states && !(states as ioBroker.StateObject).common) {
|
|
803
|
-
return (states as ioBroker.StateCommon).smartName;
|
|
804
|
-
}
|
|
805
|
-
const obj = states as ioBroker.StateObject;
|
|
806
|
-
return obj?.common?.custom && obj.common.custom[instanceId] ?
|
|
807
|
-
obj.common.custom[instanceId].smartName : undefined;
|
|
808
|
-
}
|
|
809
|
-
if (!noCommon) {
|
|
810
|
-
return (states as Record<string, ioBroker.StateObject>)[id].common.smartName;
|
|
811
|
-
}
|
|
812
|
-
const obj = (states as Record<string, ioBroker.StateObject>)[id];
|
|
813
|
-
|
|
814
|
-
return obj?.common?.custom && obj.common.custom[instanceId] ?
|
|
815
|
-
obj.common.custom[instanceId].smartName || null : null;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Get the smart name from a state.
|
|
820
|
-
*/
|
|
821
|
-
static getSmartNameFromObj(
|
|
822
|
-
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
823
|
-
instanceId: string,
|
|
824
|
-
noCommon?: boolean,
|
|
825
|
-
): SmartName | undefined {
|
|
826
|
-
if (!noCommon) {
|
|
827
|
-
if (!(obj as ioBroker.StateObject).common) {
|
|
828
|
-
return (obj as ioBroker.StateCommon).smartName;
|
|
829
|
-
}
|
|
830
|
-
if (obj && !(obj as ioBroker.StateObject).common) {
|
|
831
|
-
return (obj as ioBroker.StateCommon).smartName;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
return (obj as ioBroker.StateObject).common.smartName;
|
|
835
|
-
}
|
|
836
|
-
if (obj && !(obj as ioBroker.StateObject).common) {
|
|
837
|
-
return (obj as ioBroker.StateCommon).smartName;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
const custom: Record<string, string> | undefined | null = (obj as ioBroker.StateObject)?.common?.custom?.[instanceId];
|
|
841
|
-
|
|
842
|
-
return custom ? custom.smartName : undefined;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/**
|
|
846
|
-
* Enable smart name for a state.
|
|
847
|
-
*/
|
|
848
|
-
static enableSmartName(
|
|
849
|
-
obj: ioBroker.StateObject,
|
|
850
|
-
instanceId: string,
|
|
851
|
-
noCommon?: boolean,
|
|
852
|
-
): void {
|
|
853
|
-
if (noCommon) {
|
|
854
|
-
obj.common.custom = obj.common.custom || {};
|
|
855
|
-
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
856
|
-
obj.common.custom[instanceId].smartName = {};
|
|
857
|
-
} else {
|
|
858
|
-
obj.common.smartName = {};
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
/**
|
|
863
|
-
* Completely remove smart name from a state.
|
|
864
|
-
*/
|
|
865
|
-
static removeSmartName(
|
|
866
|
-
obj: ioBroker.StateObject,
|
|
867
|
-
instanceId: string,
|
|
868
|
-
noCommon?: boolean,
|
|
869
|
-
) {
|
|
870
|
-
if (noCommon) {
|
|
871
|
-
if (obj.common && obj.common.custom && obj.common.custom[instanceId]) {
|
|
872
|
-
obj.common.custom[instanceId] = null;
|
|
873
|
-
}
|
|
874
|
-
} else {
|
|
875
|
-
obj.common.smartName = null;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* Update the smart name of a state.
|
|
881
|
-
*/
|
|
882
|
-
static updateSmartName(
|
|
883
|
-
obj: ioBroker.StateObject,
|
|
884
|
-
newSmartName: ioBroker.StringOrTranslated,
|
|
885
|
-
byON: string | null,
|
|
886
|
-
smartType: string | null,
|
|
887
|
-
instanceId: string,
|
|
888
|
-
noCommon?: boolean,
|
|
889
|
-
) {
|
|
890
|
-
const language = I18n.getLanguage();
|
|
891
|
-
|
|
892
|
-
// convert the old format
|
|
893
|
-
if (typeof obj.common.smartName === 'string') {
|
|
894
|
-
const nnn = obj.common.smartName;
|
|
895
|
-
obj.common.smartName = {};
|
|
896
|
-
obj.common.smartName[language] = nnn;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// convert the old settings
|
|
900
|
-
if (obj.native && obj.native.byON) {
|
|
901
|
-
delete obj.native.byON;
|
|
902
|
-
let _smartName: SmartName = obj.common.smartName as SmartName;
|
|
903
|
-
|
|
904
|
-
if (_smartName && typeof _smartName !== 'object') {
|
|
905
|
-
_smartName = {
|
|
906
|
-
en: _smartName as string,
|
|
907
|
-
[language]: _smartName as string,
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
obj.common.smartName = _smartName;
|
|
911
|
-
}
|
|
912
|
-
if (smartType !== undefined) {
|
|
913
|
-
if (noCommon) {
|
|
914
|
-
obj.common.custom = obj.common.custom || {};
|
|
915
|
-
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
916
|
-
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
917
|
-
if (!smartType) {
|
|
918
|
-
delete obj.common.custom[instanceId].smartName.smartType;
|
|
919
|
-
} else {
|
|
920
|
-
obj.common.custom[instanceId].smartName.smartType = smartType;
|
|
921
|
-
}
|
|
922
|
-
} else {
|
|
923
|
-
obj.common.smartName = obj.common.smartName || {};
|
|
924
|
-
if (!smartType) {
|
|
925
|
-
// @ts-expect-error fixed in js-controller
|
|
926
|
-
delete obj.common.smartName.smartType;
|
|
927
|
-
} else {
|
|
928
|
-
// @ts-expect-error fixed in js-controller
|
|
929
|
-
obj.common.smartName.smartType = smartType;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
if (byON !== undefined) {
|
|
935
|
-
if (noCommon) {
|
|
936
|
-
obj.common.custom = obj.common.custom || {};
|
|
937
|
-
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
938
|
-
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
939
|
-
obj.common.custom[instanceId].smartName.byON = byON;
|
|
940
|
-
} else {
|
|
941
|
-
obj.common.smartName = obj.common.smartName || {};
|
|
942
|
-
// @ts-expect-error fixed in js-controller
|
|
943
|
-
obj.common.smartName.byON = byON;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
if (newSmartName !== undefined) {
|
|
948
|
-
let smartName;
|
|
949
|
-
if (noCommon) {
|
|
950
|
-
obj.common.custom = obj.common.custom || {};
|
|
951
|
-
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
952
|
-
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
953
|
-
smartName = obj.common.custom[instanceId].smartName;
|
|
954
|
-
} else {
|
|
955
|
-
obj.common.smartName = obj.common.smartName || {};
|
|
956
|
-
smartName = obj.common.smartName;
|
|
957
|
-
}
|
|
958
|
-
smartName[language] = newSmartName;
|
|
959
|
-
|
|
960
|
-
// If smart name deleted
|
|
961
|
-
if (
|
|
962
|
-
smartName &&
|
|
963
|
-
(!smartName[language] ||
|
|
964
|
-
(smartName[language] === obj.common.name &&
|
|
965
|
-
(!obj.common.role || obj.common.role.includes('button'))))
|
|
966
|
-
) {
|
|
967
|
-
delete smartName[language];
|
|
968
|
-
let empty = true;
|
|
969
|
-
// Check if the structure has any definitions
|
|
970
|
-
for (const key in smartName) {
|
|
971
|
-
if (Object.prototype.hasOwnProperty.call(smartName, key)) {
|
|
972
|
-
empty = false;
|
|
973
|
-
break;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
// If empty => delete smartName completely
|
|
977
|
-
if (empty) {
|
|
978
|
-
if (noCommon && obj.common.custom && obj.common.custom[instanceId]) {
|
|
979
|
-
if (obj.common.custom[instanceId].smartName.byON === undefined) {
|
|
980
|
-
delete obj.common.custom[instanceId];
|
|
981
|
-
} else {
|
|
982
|
-
delete obj.common.custom[instanceId].en;
|
|
983
|
-
delete obj.common.custom[instanceId].de;
|
|
984
|
-
delete obj.common.custom[instanceId].ru;
|
|
985
|
-
delete obj.common.custom[instanceId].nl;
|
|
986
|
-
delete obj.common.custom[instanceId].pl;
|
|
987
|
-
delete obj.common.custom[instanceId].it;
|
|
988
|
-
delete obj.common.custom[instanceId].fr;
|
|
989
|
-
delete obj.common.custom[instanceId].pt;
|
|
990
|
-
delete obj.common.custom[instanceId].es;
|
|
991
|
-
delete obj.common.custom[instanceId].uk;
|
|
992
|
-
delete obj.common.custom[instanceId]['zh-cn'];
|
|
993
|
-
}
|
|
994
|
-
// @ts-expect-error fixed in js-controller
|
|
995
|
-
} else if (obj.common.smartName && (obj.common.smartName as SmartName).byON !== undefined) {
|
|
996
|
-
const _smartName: { [lang in ioBroker.Languages]?: string } = obj.common.smartName as { [lang in ioBroker.Languages]?: string };
|
|
997
|
-
delete _smartName.en;
|
|
998
|
-
delete _smartName.de;
|
|
999
|
-
delete _smartName.ru;
|
|
1000
|
-
delete _smartName.nl;
|
|
1001
|
-
delete _smartName.pl;
|
|
1002
|
-
delete _smartName.it;
|
|
1003
|
-
delete _smartName.fr;
|
|
1004
|
-
delete _smartName.pt;
|
|
1005
|
-
delete _smartName.es;
|
|
1006
|
-
delete _smartName.uk;
|
|
1007
|
-
delete _smartName['zh-cn'];
|
|
1008
|
-
} else {
|
|
1009
|
-
obj.common.smartName = null;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
/**
|
|
1017
|
-
* Disable the smart name of a state.
|
|
1018
|
-
*/
|
|
1019
|
-
static disableSmartName(
|
|
1020
|
-
obj: ioBroker.StateObject,
|
|
1021
|
-
instanceId: string,
|
|
1022
|
-
noCommon?: boolean,
|
|
1023
|
-
): void {
|
|
1024
|
-
if (noCommon) {
|
|
1025
|
-
obj.common.custom = obj.common.custom || {};
|
|
1026
|
-
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
1027
|
-
obj.common.custom[instanceId].smartName = false;
|
|
1028
|
-
} else {
|
|
1029
|
-
obj.common.smartName = false;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
/**
|
|
1034
|
-
* Copy text to the clipboard.
|
|
1035
|
-
*/
|
|
1036
|
-
static copyToClipboard(
|
|
1037
|
-
text: string,
|
|
1038
|
-
e?: Event,
|
|
1039
|
-
): boolean {
|
|
1040
|
-
if (e) {
|
|
1041
|
-
e.stopPropagation();
|
|
1042
|
-
e.preventDefault();
|
|
1043
|
-
}
|
|
1044
|
-
return copy(text);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
/**
|
|
1048
|
-
* Gets the extension of a file name.
|
|
1049
|
-
* @param fileName the file name.
|
|
1050
|
-
* @returns The extension in lower case.
|
|
1051
|
-
*/
|
|
1052
|
-
static getFileExtension(fileName: string): string | null {
|
|
1053
|
-
const pos = (fileName || '').lastIndexOf('.');
|
|
1054
|
-
if (pos !== -1) {
|
|
1055
|
-
return fileName.substring(pos + 1).toLowerCase();
|
|
1056
|
-
}
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Format number of bytes as a string with B, KB, MB or GB.
|
|
1062
|
-
* The base for all calculations is 1024.
|
|
1063
|
-
* @returns The formatted string (e.g. '723.5 KB')
|
|
1064
|
-
*/
|
|
1065
|
-
static formatBytes(
|
|
1066
|
-
/** The number of bytes. */
|
|
1067
|
-
bytes: number,
|
|
1068
|
-
): string {
|
|
1069
|
-
if (Math.abs(bytes) < 1024) {
|
|
1070
|
-
return `${bytes} B`;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
const units = ['KB', 'MB', 'GB'];
|
|
1074
|
-
// const units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
|
|
1075
|
-
let u = -1;
|
|
1076
|
-
|
|
1077
|
-
do {
|
|
1078
|
-
bytes /= 1024;
|
|
1079
|
-
++u;
|
|
1080
|
-
} while (Math.abs(bytes) >= 1024 && u < units.length - 1);
|
|
1081
|
-
|
|
1082
|
-
return `${bytes.toFixed(1)} ${units[u]}`;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
/**
|
|
1086
|
-
* Invert the given color according to a theme type to get the inverted text color for background
|
|
1087
|
-
*/
|
|
1088
|
-
static getInvertedColor(
|
|
1089
|
-
/** Color in the format '#rrggbb' or '#rgb' (or without a hash) */
|
|
1090
|
-
color: string,
|
|
1091
|
-
themeType: ThemeType,
|
|
1092
|
-
/** dark theme has light color in control, or light theme has light color in control */
|
|
1093
|
-
invert?: boolean,
|
|
1094
|
-
): string | undefined {
|
|
1095
|
-
if (!color) {
|
|
1096
|
-
return undefined;
|
|
1097
|
-
}
|
|
1098
|
-
const invertedColor = Utils.invertColor(color, true);
|
|
1099
|
-
if (invertedColor === '#FFFFFF' && (themeType === 'dark' || (invert && themeType === 'light'))) {
|
|
1100
|
-
return '#DDD';
|
|
1101
|
-
}
|
|
1102
|
-
if (invertedColor === '#000000' && (themeType === 'light' || (invert && themeType === 'dark'))) {
|
|
1103
|
-
return '#222';
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
return undefined;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
// Big thanks to: https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
|
|
1110
|
-
/**
|
|
1111
|
-
* Invert the given color
|
|
1112
|
-
* @param hex Color in the format '#rrggbb' or '#rgb' (or without hash)
|
|
1113
|
-
* @param bw Set to black or white.
|
|
1114
|
-
*/
|
|
1115
|
-
static invertColor(hex: string, bw?: boolean): string {
|
|
1116
|
-
if (hex === undefined || hex === null || hex === '' || typeof hex !== 'string') {
|
|
1117
|
-
return '';
|
|
1118
|
-
}
|
|
1119
|
-
if (hex.startsWith('rgba')) {
|
|
1120
|
-
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
|
|
1121
|
-
if (m) {
|
|
1122
|
-
hex =
|
|
1123
|
-
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1124
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1125
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1126
|
-
}
|
|
1127
|
-
} else if (hex.startsWith('rgb')) {
|
|
1128
|
-
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
1129
|
-
if (m) {
|
|
1130
|
-
hex =
|
|
1131
|
-
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1132
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1133
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1134
|
-
}
|
|
1135
|
-
} else if (hex.startsWith('#')) {
|
|
1136
|
-
hex = hex.slice(1);
|
|
1137
|
-
}
|
|
1138
|
-
// convert 3-digit hex to 6-digits.
|
|
1139
|
-
if (hex.length === 3) {
|
|
1140
|
-
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1141
|
-
}
|
|
1142
|
-
let alfa = null;
|
|
1143
|
-
if (hex.length === 8) {
|
|
1144
|
-
alfa = hex.substring(6, 8);
|
|
1145
|
-
hex = hex.substring(0, 6);
|
|
1146
|
-
} else if (hex.length !== 6) {
|
|
1147
|
-
console.warn(`Cannot invert color: ${hex}`);
|
|
1148
|
-
return hex;
|
|
1149
|
-
}
|
|
1150
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
1151
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
1152
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
1153
|
-
|
|
1154
|
-
if (bw) {
|
|
1155
|
-
// http://stackoverflow.com/a/3943023/112731
|
|
1156
|
-
return r * 0.299 + g * 0.587 + b * 0.114 > 186
|
|
1157
|
-
? `#000000${alfa || ''}`
|
|
1158
|
-
: `#FFFFFF${alfa || ''}`;
|
|
1159
|
-
}
|
|
1160
|
-
// invert color components
|
|
1161
|
-
const rs = (255 - r).toString(16);
|
|
1162
|
-
const gs = (255 - g).toString(16);
|
|
1163
|
-
const bd = (255 - b).toString(16);
|
|
1164
|
-
// pad each with zeros and return
|
|
1165
|
-
return `#${rs.padStart(2, '0')}${gs.padStart(2, '0')}${bd.padStart(2, '0')}${alfa || ''}`;
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/**
|
|
1169
|
-
* Convert RGB to array [r, g, b]
|
|
1170
|
-
* @param hex Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1171
|
-
* @returns Array with 3 elements [r, g, b]
|
|
1172
|
-
*/
|
|
1173
|
-
static color2rgb(hex: string): false | [number, number, number] | '' {
|
|
1174
|
-
if (hex === undefined || hex === null || hex === '' || typeof hex !== 'string') {
|
|
1175
|
-
return false;
|
|
1176
|
-
}
|
|
1177
|
-
if (hex.startsWith('rgba')) {
|
|
1178
|
-
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
|
|
1179
|
-
if (m) {
|
|
1180
|
-
hex =
|
|
1181
|
-
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1182
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1183
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1184
|
-
}
|
|
1185
|
-
} else if (hex.startsWith('rgb')) {
|
|
1186
|
-
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
1187
|
-
if (m) {
|
|
1188
|
-
hex =
|
|
1189
|
-
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1190
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1191
|
-
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1192
|
-
}
|
|
1193
|
-
} else if (hex.startsWith('#')) {
|
|
1194
|
-
hex = hex.slice(1);
|
|
1195
|
-
}
|
|
1196
|
-
// convert 3-digit hex to 6-digits.
|
|
1197
|
-
if (hex.length === 3) {
|
|
1198
|
-
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1199
|
-
}
|
|
1200
|
-
if (hex.length !== 6 && hex.length !== 8) {
|
|
1201
|
-
console.warn(`Cannot invert color: ${hex}`);
|
|
1202
|
-
return false;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
return [
|
|
1206
|
-
parseInt(hex.slice(0, 2), 16),
|
|
1207
|
-
parseInt(hex.slice(2, 4), 16),
|
|
1208
|
-
parseInt(hex.slice(4, 6), 16),
|
|
1209
|
-
];
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// Big thanks to: https://github.com/antimatter15/rgb-lab
|
|
1213
|
-
/**
|
|
1214
|
-
* Convert RGB to LAB
|
|
1215
|
-
* @param {Array<number>} rgb color in format [r,g,b]
|
|
1216
|
-
* @returns {Array<number>} lab color in format [l,a,b]
|
|
1217
|
-
*/
|
|
1218
|
-
static rgb2lab(rgb: [number, number, number]): [number, number, number] {
|
|
1219
|
-
let r = rgb[0] / 255;
|
|
1220
|
-
let g = rgb[1] / 255;
|
|
1221
|
-
let b = rgb[2] / 255;
|
|
1222
|
-
|
|
1223
|
-
r = r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
|
|
1224
|
-
g = g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
|
|
1225
|
-
b = b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;
|
|
1226
|
-
|
|
1227
|
-
let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
|
1228
|
-
let y = r * 0.2126 + g * 0.7152 + b * 0.0722; /* / 1.00000; */
|
|
1229
|
-
let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
|
1230
|
-
|
|
1231
|
-
x = x > 0.008856 ? x ** 0.33333333 : 7.787 * x + 0.137931; // 16 / 116;
|
|
1232
|
-
y = y > 0.008856 ? y ** 0.33333333 : 7.787 * y + 0.137931; // 16 / 116;
|
|
1233
|
-
z = z > 0.008856 ? z ** 0.33333333 : 7.787 * z + 0.137931; // 16 / 116;
|
|
1234
|
-
|
|
1235
|
-
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* Calculate the distance between two colors in LAB color space in the range 0-100^2
|
|
1240
|
-
* If distance is less than 1000, the colors are similar
|
|
1241
|
-
* @param color1 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1242
|
-
* @param color2 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1243
|
-
* @returns distance in the range 0-100^2
|
|
1244
|
-
*/
|
|
1245
|
-
static colorDistance(color1: string, color2: string): number {
|
|
1246
|
-
const rgb1 = Utils.color2rgb(color1);
|
|
1247
|
-
const rgb2 = Utils.color2rgb(color2);
|
|
1248
|
-
if (!rgb1 || !rgb2) {
|
|
1249
|
-
return 0;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
const lab1 = Utils.rgb2lab(rgb1);
|
|
1253
|
-
const lab2 = Utils.rgb2lab(rgb2);
|
|
1254
|
-
const dltL = lab1[0] - lab2[0];
|
|
1255
|
-
const dltA = lab1[1] - lab2[1];
|
|
1256
|
-
const dltB = lab1[2] - lab2[2];
|
|
1257
|
-
const c1 = Math.sqrt(lab1[1] * lab1[1] + lab1[2] * lab1[2]);
|
|
1258
|
-
const c2 = Math.sqrt(lab2[1] * lab2[1] + lab2[2] * lab2[2]);
|
|
1259
|
-
const dltC = c1 - c2;
|
|
1260
|
-
let dltH = dltA * dltA + dltB * dltB - dltC * dltC;
|
|
1261
|
-
dltH = dltH < 0 ? 0 : Math.sqrt(dltH);
|
|
1262
|
-
const sc = 1.0 + 0.045 * c1;
|
|
1263
|
-
const sh = 1.0 + 0.015 * c1;
|
|
1264
|
-
const dltLKlsl = dltL;
|
|
1265
|
-
const dltCkcsc = dltC / sc;
|
|
1266
|
-
const dltHkhsh = dltH / sh;
|
|
1267
|
-
const i = dltLKlsl * dltLKlsl + dltCkcsc * dltCkcsc + dltHkhsh * dltHkhsh;
|
|
1268
|
-
return i < 0 ? 0 : i;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
// https://github.com/lukeed/clsx/blob/master/src/index.js
|
|
1272
|
-
// License
|
|
1273
|
-
// MIT © Luke Edwards
|
|
1274
|
-
/**
|
|
1275
|
-
* @private
|
|
1276
|
-
*/
|
|
1277
|
-
static _toVal(mix: ClassValue): string {
|
|
1278
|
-
let y;
|
|
1279
|
-
let str = '';
|
|
1280
|
-
|
|
1281
|
-
if (typeof mix === 'string' || typeof mix === 'number') {
|
|
1282
|
-
str += mix;
|
|
1283
|
-
} else if (typeof mix === 'object') {
|
|
1284
|
-
if (Array.isArray(mix)) {
|
|
1285
|
-
for (let k = 0; k < mix.length; k++) {
|
|
1286
|
-
if (mix[k]) {
|
|
1287
|
-
y = Utils._toVal(mix[k]);
|
|
1288
|
-
if (y) {
|
|
1289
|
-
str && (str += ' ');
|
|
1290
|
-
str += y;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
} else {
|
|
1295
|
-
for (const k in mix) {
|
|
1296
|
-
if (mix[k]) {
|
|
1297
|
-
str && (str += ' ');
|
|
1298
|
-
str += k;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
return str;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
// https://github.com/lukeed/clsx/blob/master/src/index.js
|
|
1308
|
-
// License
|
|
1309
|
-
// MIT © Luke Edwards
|
|
1310
|
-
/**
|
|
1311
|
-
* Convert any object to a string with its values.
|
|
1312
|
-
* @returns {string}
|
|
1313
|
-
*/
|
|
1314
|
-
static clsx(...inputs: ClassValue[]): string {
|
|
1315
|
-
let i = 0;
|
|
1316
|
-
let tmp;
|
|
1317
|
-
let x;
|
|
1318
|
-
let str = '';
|
|
1319
|
-
while (i < inputs.length) {
|
|
1320
|
-
// eslint-disable-next-line prefer-rest-params
|
|
1321
|
-
tmp = inputs[i++];
|
|
1322
|
-
if (tmp) {
|
|
1323
|
-
x = Utils._toVal(tmp);
|
|
1324
|
-
if (x) {
|
|
1325
|
-
str && (str += ' ');
|
|
1326
|
-
str += x;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
return str;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
/**
|
|
1334
|
-
* Get the current theme name (either from local storage or the browser settings).
|
|
1335
|
-
*/
|
|
1336
|
-
static getThemeName(themeName?: ThemeName | null): ThemeName {
|
|
1337
|
-
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1338
|
-
return (window as any).vendorPrefix;
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
themeName = ((window as any)._localStorage || window.localStorage).getItem('App.themeName');
|
|
1342
|
-
if (themeName) {
|
|
1343
|
-
return themeName;
|
|
1344
|
-
}
|
|
1345
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'colored';
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
/**
|
|
1349
|
-
* Get the type of theme.
|
|
1350
|
-
*/
|
|
1351
|
-
static getThemeType(themeName?: ThemeName): ThemeType {
|
|
1352
|
-
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@') {
|
|
1353
|
-
return 'light';
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
themeName = themeName || Utils.getThemeName();
|
|
1357
|
-
return themeName === 'dark' || themeName === 'blue' ? 'dark' : 'light';
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* Set the theme name and theme type.
|
|
1362
|
-
*/
|
|
1363
|
-
static setThemeName(themeName: ThemeName): void {
|
|
1364
|
-
const vendorPrefix = (window as any).vendorPrefix;
|
|
1365
|
-
if (vendorPrefix && vendorPrefix !== '@@vendorPrefix@@' && vendorPrefix !== 'MV') {
|
|
1366
|
-
return; // ignore
|
|
1367
|
-
}
|
|
1368
|
-
((window as any)._localStorage || window.localStorage).setItem('App.themeName', themeName);
|
|
1369
|
-
((window as any)._localStorage || window.localStorage).setItem(
|
|
1370
|
-
'App.theme',
|
|
1371
|
-
themeName === 'dark' || themeName === 'blue' ? 'dark' : 'light',
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Toggle the theme name between 'dark' and 'colored'.
|
|
1377
|
-
* @returns the new theme name.
|
|
1378
|
-
*/
|
|
1379
|
-
static toggleTheme(themeName?: ThemeName | null): ThemeName {
|
|
1380
|
-
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1381
|
-
return (window as any).vendorPrefix as ThemeName;
|
|
1382
|
-
}
|
|
1383
|
-
themeName = themeName || ((window as any)._localStorage || window.localStorage).getItem('App.themeName') || 'light';
|
|
1384
|
-
|
|
1385
|
-
// dark => blue => colored => light => dark
|
|
1386
|
-
const themes = Utils.getThemeNames();
|
|
1387
|
-
const pos = themeName ? themes.indexOf(themeName) : -1;
|
|
1388
|
-
let newTheme: ThemeName;
|
|
1389
|
-
if (pos !== -1) {
|
|
1390
|
-
newTheme = themes[(pos + 1) % themes.length];
|
|
1391
|
-
} else {
|
|
1392
|
-
newTheme = themes[0];
|
|
1393
|
-
}
|
|
1394
|
-
Utils.setThemeName(newTheme);
|
|
1395
|
-
|
|
1396
|
-
return newTheme;
|
|
1397
|
-
}
|
|
1398
|
-
|
|
1399
|
-
/**
|
|
1400
|
-
* Get the list of themes
|
|
1401
|
-
* @returns list of possible themes
|
|
1402
|
-
*/
|
|
1403
|
-
static getThemeNames(): ThemeName[] {
|
|
1404
|
-
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1405
|
-
return [(window as any).vendorPrefix as ThemeName];
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
return ['light', 'dark', 'blue', 'colored'];
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
/**
|
|
1412
|
-
* Parse a query string into its parts.
|
|
1413
|
-
*/
|
|
1414
|
-
static parseQuery(query: string): Record<string, string | number | boolean> {
|
|
1415
|
-
query = (query || '').toString().replace(/^\?/, '');
|
|
1416
|
-
const result: Record<string, string | number | boolean> = {};
|
|
1417
|
-
query.split('&').forEach(part => {
|
|
1418
|
-
part = part.trim();
|
|
1419
|
-
if (part) {
|
|
1420
|
-
const parts = part.split('=');
|
|
1421
|
-
const attr = decodeURIComponent(parts[0]).trim();
|
|
1422
|
-
if (parts.length > 1) {
|
|
1423
|
-
const value = decodeURIComponent(parts[1]);
|
|
1424
|
-
if (value === 'true') {
|
|
1425
|
-
result[attr] = true;
|
|
1426
|
-
} else if (value === 'false') {
|
|
1427
|
-
result[attr] = false;
|
|
1428
|
-
} else {
|
|
1429
|
-
const f = parseFloat(value);
|
|
1430
|
-
if (f.toString() === value) {
|
|
1431
|
-
result[attr] = f;
|
|
1432
|
-
} else {
|
|
1433
|
-
result[attr] = value;
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
} else {
|
|
1437
|
-
result[attr] = true;
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
});
|
|
1441
|
-
return result;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
/**
|
|
1445
|
-
* Returns parent ID.
|
|
1446
|
-
* @returns parent ID or null if no parent
|
|
1447
|
-
*/
|
|
1448
|
-
static getParentId(id: string): string | null {
|
|
1449
|
-
const p = (id || '').toString().split('.');
|
|
1450
|
-
if (p.length > 1) {
|
|
1451
|
-
p.pop();
|
|
1452
|
-
return p.join('.');
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
return null;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
static formatDate(dateObj: Date, dateFormat: string): string {
|
|
1459
|
-
// format could be DD.MM.YYYY, YYYY.MM.DD or MM/DD/YYYY
|
|
1460
|
-
|
|
1461
|
-
if (!dateObj) {
|
|
1462
|
-
return '';
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
let text;
|
|
1466
|
-
const mm = (dateObj.getMonth() + 1).toString().padStart(2, '0');
|
|
1467
|
-
const dd = dateObj.getDate().toString().padStart(2, '0');
|
|
1468
|
-
|
|
1469
|
-
if (dateFormat === 'MM/DD/YYYY') {
|
|
1470
|
-
text = `${mm}/${dd}/${dateObj.getFullYear()}`;
|
|
1471
|
-
} else {
|
|
1472
|
-
text = `${dateObj.getFullYear()}-${mm}-${dd}`;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// time
|
|
1476
|
-
text += ` ${dateObj.getHours().toString().padStart(2, '0')}:${dateObj.getMinutes().toString().padStart(2, '0')}:${dateObj.getSeconds().toString().padStart(2, '0')}.${dateObj.getMilliseconds().toString().padStart(3, '0')}`;
|
|
1477
|
-
|
|
1478
|
-
return text;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
/*
|
|
1482
|
-
Format seconds to string like 'h:mm:ss' or 'd.hh:mm:ss'
|
|
1483
|
-
*/
|
|
1484
|
-
static formatTime(seconds: number): string {
|
|
1485
|
-
if (seconds) {
|
|
1486
|
-
seconds = Math.round(seconds);
|
|
1487
|
-
const d = Math.floor(seconds / (3600 * 24));
|
|
1488
|
-
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
|
1489
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
1490
|
-
const s = seconds % 60;
|
|
1491
|
-
if (d) {
|
|
1492
|
-
return `${d}.${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1493
|
-
}
|
|
1494
|
-
if (h) {
|
|
1495
|
-
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
return `0:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1499
|
-
}
|
|
1500
|
-
return '0:00:00';
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
static MDtext2link(text: string): string {
|
|
1504
|
-
const m = text.match(/\d+\.\)\s/);
|
|
1505
|
-
if (m) {
|
|
1506
|
-
text = text.replace(m[0], m[0].replace(/\s/, ' '));
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
return text
|
|
1510
|
-
.replace(/[^a-zA-Zа-яА-Я0-9]/g, '')
|
|
1511
|
-
.trim()
|
|
1512
|
-
.replace(/\s/g, '')
|
|
1513
|
-
.toLowerCase();
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
/*
|
|
1517
|
-
Open url link in the new target window
|
|
1518
|
-
*/
|
|
1519
|
-
static openLink(url: string, target?: string): void {
|
|
1520
|
-
// replace IPv6 Address with [ipv6]:port
|
|
1521
|
-
url = url.replace(/\/\/([0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*)(:\d+)?\//i, '//[$1]$2/');
|
|
1522
|
-
|
|
1523
|
-
if (target === 'this') {
|
|
1524
|
-
window.location.href = url;
|
|
1525
|
-
} else {
|
|
1526
|
-
window.open(url, target || '_blank');
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
static MDgetTitle(text: string): string {
|
|
1531
|
-
const result = Utils.MDextractHeader(text);
|
|
1532
|
-
const header = result.header;
|
|
1533
|
-
let body = result.body;
|
|
1534
|
-
if (!header.title) {
|
|
1535
|
-
// remove {docsify-bla}
|
|
1536
|
-
body = body.replace(/{[^}]*}/g, '');
|
|
1537
|
-
body = body.trim();
|
|
1538
|
-
const lines = body.replace(/\r/g, '').split('\n');
|
|
1539
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1540
|
-
if (lines[i].startsWith('# ')) {
|
|
1541
|
-
return lines[i].substring(2).trim();
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
return '';
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
return header.title?.toString() || '';
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
static MDextractHeader(text: string): { header: Record<string, string | boolean | number>; body: string } {
|
|
1551
|
-
const attrs: Record<string, string | boolean | number> = {};
|
|
1552
|
-
if (text.substring(0, 3) === '---') {
|
|
1553
|
-
const pos = text.substring(3).indexOf('\n---');
|
|
1554
|
-
if (pos !== -1) {
|
|
1555
|
-
const _header = text.substring(3, pos + 3);
|
|
1556
|
-
const lines = _header.replace(/\r/g, '').split('\n');
|
|
1557
|
-
lines.forEach(line => {
|
|
1558
|
-
if (!line.trim()) {
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
const pos_ = line.indexOf(':');
|
|
1562
|
-
if (pos_ !== -1) {
|
|
1563
|
-
const attr = line.substring(0, pos_).trim();
|
|
1564
|
-
let value = line.substring(pos_ + 1).trim();
|
|
1565
|
-
value = value.replace(/^['"]|['"]$/g, '');
|
|
1566
|
-
if (value === 'true') {
|
|
1567
|
-
attrs[attr] = true;
|
|
1568
|
-
} else if (value === 'false') {
|
|
1569
|
-
attrs[attr] = false;
|
|
1570
|
-
} else if (parseFloat(value).toString() === attrs[attr]) {
|
|
1571
|
-
attrs[attr] = parseFloat(value);
|
|
1572
|
-
} else {
|
|
1573
|
-
attrs[attr] = value;
|
|
1574
|
-
}
|
|
1575
|
-
} else {
|
|
1576
|
-
attrs[line.trim()] = true;
|
|
1577
|
-
}
|
|
1578
|
-
});
|
|
1579
|
-
text = text.substring(pos + 7);
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
return { header: attrs, body: text };
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
static MDremoveDocsify(text: string): string {
|
|
1586
|
-
const m = text.match(/{docsify-[^}]*}/g);
|
|
1587
|
-
if (m) {
|
|
1588
|
-
m.forEach(doc => (text = text.replace(doc, '')));
|
|
1589
|
-
}
|
|
1590
|
-
return text;
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
/**
|
|
1594
|
-
* Generate the file for download from JSON object.
|
|
1595
|
-
*/
|
|
1596
|
-
static generateFile(
|
|
1597
|
-
fileName: string,
|
|
1598
|
-
/** json file data */
|
|
1599
|
-
json: Record<string, any>,
|
|
1600
|
-
): void {
|
|
1601
|
-
const el = document.createElement('a');
|
|
1602
|
-
el.setAttribute(
|
|
1603
|
-
'href',
|
|
1604
|
-
`data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(json, null, 2))}`,
|
|
1605
|
-
);
|
|
1606
|
-
el.setAttribute('download', fileName);
|
|
1607
|
-
|
|
1608
|
-
el.style.display = 'none';
|
|
1609
|
-
document.body.appendChild(el);
|
|
1610
|
-
|
|
1611
|
-
el.click();
|
|
1612
|
-
|
|
1613
|
-
document.body.removeChild(el);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
/**
|
|
1617
|
-
* Convert quality code into text
|
|
1618
|
-
* @returns lines that decode quality
|
|
1619
|
-
*/
|
|
1620
|
-
static quality2text(quality: ioBroker.STATE_QUALITY[keyof ioBroker.STATE_QUALITY]): string[] {
|
|
1621
|
-
// eslint-disable-next-line no-bitwise
|
|
1622
|
-
const custom = quality & 0xFFFF0000;
|
|
1623
|
-
const text: string = QUALITY_BITS[quality];
|
|
1624
|
-
let result;
|
|
1625
|
-
if (text) {
|
|
1626
|
-
result = [text];
|
|
1627
|
-
// eslint-disable-next-line no-bitwise
|
|
1628
|
-
} else if (quality & 0x01) {
|
|
1629
|
-
// eslint-disable-next-line no-bitwise
|
|
1630
|
-
result = [QUALITY_BITS[0x01], `0x${(quality & (0xFFFF & ~1)).toString(16)}`];
|
|
1631
|
-
// eslint-disable-next-line no-bitwise
|
|
1632
|
-
} else if (quality & 0x02) {
|
|
1633
|
-
// eslint-disable-next-line no-bitwise
|
|
1634
|
-
result = [QUALITY_BITS[0x02], `0x${(quality & (0xFFFF & ~2)).toString(16)}`];
|
|
1635
|
-
} else {
|
|
1636
|
-
result = [`0x${quality.toString(16)}`];
|
|
1637
|
-
}
|
|
1638
|
-
if (custom) {
|
|
1639
|
-
// eslint-disable-next-line no-bitwise
|
|
1640
|
-
result.push(`0x${(custom >> 16).toString(16).toUpperCase()}`);
|
|
1641
|
-
}
|
|
1642
|
-
return result;
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
/**
|
|
1646
|
-
* Deep copy object
|
|
1647
|
-
*/
|
|
1648
|
-
static clone(object: Record<string, any>): Record<string, any> {
|
|
1649
|
-
return JSON.parse(JSON.stringify(object));
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
/**
|
|
1653
|
-
* Get states of object
|
|
1654
|
-
* @returns states as an object in form {"value1": "label1", "value2": "label2"} or null
|
|
1655
|
-
*/
|
|
1656
|
-
static getStates(obj: ioBroker.StateObject | null | undefined): Record<string, string> | null {
|
|
1657
|
-
const states: Record<string, string> | string[] | string | undefined | null = obj?.common?.states;
|
|
1658
|
-
let result: Record<string, string> | null | undefined;
|
|
1659
|
-
if (states) {
|
|
1660
|
-
if (typeof states === 'string' && states[0] === '{') {
|
|
1661
|
-
try {
|
|
1662
|
-
result = JSON.parse(states) as Record<string, string>;
|
|
1663
|
-
} catch (ex) {
|
|
1664
|
-
console.error(`Cannot parse states: ${states}`);
|
|
1665
|
-
result = null;
|
|
1666
|
-
}
|
|
1667
|
-
} else if (typeof states === 'string') {
|
|
1668
|
-
// if old format val1:text1;val2:text2
|
|
1669
|
-
const parts = states.split(';');
|
|
1670
|
-
result = {};
|
|
1671
|
-
for (let p = 0; p < parts.length; p++) {
|
|
1672
|
-
const s = parts[p].split(':');
|
|
1673
|
-
result[s[0]] = s[1];
|
|
1674
|
-
}
|
|
1675
|
-
} else if (Array.isArray(states)) {
|
|
1676
|
-
result = {};
|
|
1677
|
-
if (obj?.common.type === 'number') {
|
|
1678
|
-
states.forEach((value, key) => ((result as Record<string, string>)[key] = value));
|
|
1679
|
-
} else if (obj?.common.type === 'string') {
|
|
1680
|
-
states.forEach(value => ((result as Record<string, string>)[value] = value));
|
|
1681
|
-
} else if (obj?.common.type === 'boolean') {
|
|
1682
|
-
result.false = states[0];
|
|
1683
|
-
result.true = states[1];
|
|
1684
|
-
}
|
|
1685
|
-
} else if (typeof states === 'object') {
|
|
1686
|
-
result = states as Record<string, string>;
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
return result || null;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
/**
|
|
1694
|
-
* Get svg file as text
|
|
1695
|
-
* @param url URL of SVG file
|
|
1696
|
-
* @returns Promise with "data:image..."
|
|
1697
|
-
*/
|
|
1698
|
-
static async getSvg(url: string): Promise<string> {
|
|
1699
|
-
const response = await fetch(url);
|
|
1700
|
-
const blob = await response.blob();
|
|
1701
|
-
return new Promise(resolve => {
|
|
1702
|
-
const reader = new FileReader();
|
|
1703
|
-
// eslint-disable-next-line func-names
|
|
1704
|
-
reader.onload = function () {
|
|
1705
|
-
resolve(this.result?.toString() || '');
|
|
1706
|
-
};
|
|
1707
|
-
reader.readAsDataURL(blob);
|
|
1708
|
-
});
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
/**
|
|
1712
|
-
* Detect file extension by its content
|
|
1713
|
-
* @returns Detected extension, like 'jpg'
|
|
1714
|
-
*/
|
|
1715
|
-
static detectMimeType(
|
|
1716
|
-
/** Base64 encoded binary file */
|
|
1717
|
-
base64: string,
|
|
1718
|
-
): string | null {
|
|
1719
|
-
const signature = Object.keys(SIGNATURES).find(s => base64.startsWith(s));
|
|
1720
|
-
return signature ? SIGNATURES[signature] : null;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
/**
|
|
1724
|
-
* Check if configured repository is the stable repository
|
|
1725
|
-
*/
|
|
1726
|
-
static isStableRepository(
|
|
1727
|
-
/** current configured repository or multi repository */
|
|
1728
|
-
activeRepo: string | string[],
|
|
1729
|
-
): boolean {
|
|
1730
|
-
return !!((
|
|
1731
|
-
typeof activeRepo === 'string' &&
|
|
1732
|
-
activeRepo.toLowerCase().startsWith('stable')
|
|
1733
|
-
)
|
|
1734
|
-
||
|
|
1735
|
-
(
|
|
1736
|
-
activeRepo &&
|
|
1737
|
-
typeof activeRepo !== 'string' &&
|
|
1738
|
-
activeRepo.find(r => r.toLowerCase().startsWith('stable'))
|
|
1739
|
-
));
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
/**
|
|
1743
|
-
* Check if a given string is an integer
|
|
1744
|
-
*/
|
|
1745
|
-
static isStringInteger(str: string | number): boolean {
|
|
1746
|
-
if (typeof str === 'number') {
|
|
1747
|
-
return Math.round(str) === str;
|
|
1748
|
-
}
|
|
1749
|
-
return parseInt(str, 10).toString() === str;
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* Check if the date is valid
|
|
1754
|
-
*/
|
|
1755
|
-
static isValidDate(date: any): boolean {
|
|
1756
|
-
// eslint-disable-next-line no-restricted-globals
|
|
1757
|
-
return date instanceof Date && !isNaN(date as any as number);
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
static getStyle(
|
|
1761
|
-
theme: IobTheme,
|
|
1762
|
-
...args: (((_theme: IobTheme) => Record<string, any>) | undefined | Record<string, any>)[]
|
|
1763
|
-
): Record<string, any> {
|
|
1764
|
-
const result: Record<string, any> = {};
|
|
1765
|
-
|
|
1766
|
-
for (let a = 0; a < args.length; a++) {
|
|
1767
|
-
if (typeof args[a] === 'function') {
|
|
1768
|
-
Object.assign(result, (args[a] as (_theme: IobTheme) => Record<string, any>)(theme));
|
|
1769
|
-
} else if (args[a] && typeof args[a] === 'object') {
|
|
1770
|
-
Object.keys(args[a] as Record<string, any>).forEach((attr: string) => {
|
|
1771
|
-
if (typeof (args[a] as Record<string, any>)[attr] === 'function') {
|
|
1772
|
-
result[attr] = ((args[a] as Record<string, any>)[attr] as (_theme: IobTheme) => Record<string, any>)(theme);
|
|
1773
|
-
} else if (typeof (args[a] as Record<string, any>)[attr] === 'object') {
|
|
1774
|
-
const obj = (args[a] as Record<string, any>)[attr];
|
|
1775
|
-
result[attr] = {};
|
|
1776
|
-
Object.keys(obj).forEach((attr1: string) => {
|
|
1777
|
-
if (typeof obj[attr1] === 'function') {
|
|
1778
|
-
result[attr][attr1] = obj(theme);
|
|
1779
|
-
} else if (obj[attr1] || obj[attr1] === 0) {
|
|
1780
|
-
result[attr][attr1] = obj[attr1];
|
|
1781
|
-
}
|
|
1782
|
-
});
|
|
1783
|
-
} else if ((args[a] as Record<string, any>)[attr] || (args[a] as Record<string, any>)[attr] === 0) {
|
|
1784
|
-
result[attr] = (args[a] as Record<string, any>)[attr];
|
|
1785
|
-
}
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
return result;
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
export default Utils;
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2018-2024 Denis Haev <dogafox@gmail.com>
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* */
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import copy from './CopyToClipboard';
|
|
9
|
+
import I18n from '../i18n';
|
|
10
|
+
import { IobTheme, ThemeName, ThemeType } from '../types';
|
|
11
|
+
|
|
12
|
+
const NAMESPACE = 'material';
|
|
13
|
+
const days = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
|
14
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
15
|
+
const QUALITY_BITS: Record<ioBroker.STATE_QUALITY[keyof ioBroker.STATE_QUALITY], string> = {
|
|
16
|
+
0x00: '0x00 - good',
|
|
17
|
+
|
|
18
|
+
0x01: '0x01 - general problem',
|
|
19
|
+
0x02: '0x02 - no connection problem',
|
|
20
|
+
|
|
21
|
+
0x10: '0x10 - substitute value from controller',
|
|
22
|
+
0x20: '0x20 - substitute initial value',
|
|
23
|
+
0x40: '0x40 - substitute value from device or instance',
|
|
24
|
+
0x80: '0x80 - substitute value from sensor',
|
|
25
|
+
|
|
26
|
+
0x11: '0x11 - general problem by instance',
|
|
27
|
+
0x41: '0x41 - general problem by device',
|
|
28
|
+
0x81: '0x81 - general problem by sensor',
|
|
29
|
+
|
|
30
|
+
0x12: '0x12 - instance not connected',
|
|
31
|
+
0x42: '0x42 - device not connected',
|
|
32
|
+
0x82: '0x82 - sensor not connected',
|
|
33
|
+
|
|
34
|
+
0x44: '0x44 - device reports error',
|
|
35
|
+
0x84: '0x84 - sensor reports error',
|
|
36
|
+
};
|
|
37
|
+
const SIGNATURES: Record<string, string> = {
|
|
38
|
+
JVBERi0: 'pdf',
|
|
39
|
+
R0lGODdh: 'gif',
|
|
40
|
+
R0lGODlh: 'gif',
|
|
41
|
+
iVBORw0KGgo: 'png',
|
|
42
|
+
'/9j/': 'jpg',
|
|
43
|
+
PHN2Zw: 'svg',
|
|
44
|
+
Qk1: 'bmp',
|
|
45
|
+
AAABAA: 'ico', // 00 00 01 00 according to https://en.wikipedia.org/wiki/List_of_file_signatures
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type SmartName = null
|
|
49
|
+
| false
|
|
50
|
+
| string
|
|
51
|
+
| ({ [lang in ioBroker.Languages]?: string } & {
|
|
52
|
+
/** Which kind of device it is */
|
|
53
|
+
smartType?: string | null;
|
|
54
|
+
/** Which value to set when the ON command is issued */
|
|
55
|
+
byON?: string | null;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
type ClassDictionary = Record<string, any>;
|
|
59
|
+
// eslint-disable-next-line no-use-before-define
|
|
60
|
+
type ClassValue = ClassArray | ClassDictionary | string | number | null | boolean | undefined;
|
|
61
|
+
type ClassArray = ClassValue[];
|
|
62
|
+
|
|
63
|
+
class Utils {
|
|
64
|
+
static namespace = NAMESPACE;
|
|
65
|
+
|
|
66
|
+
static INSTANCES = 'instances';
|
|
67
|
+
|
|
68
|
+
static dateFormat = ['DD', 'MM'];
|
|
69
|
+
|
|
70
|
+
static FORBIDDEN_CHARS = /[^._\-/ :!#$%&()+=@^{}|~\p{Ll}\p{Lu}\p{Nd}]+/gu;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Capitalize words.
|
|
74
|
+
*/
|
|
75
|
+
static CapitalWords(name: string | null | undefined): string {
|
|
76
|
+
return (name || '')
|
|
77
|
+
.split(/[\s_]/)
|
|
78
|
+
.filter(item => item)
|
|
79
|
+
.map(word => (word ? word[0].toUpperCase() + word.substring(1).toLowerCase() : ''))
|
|
80
|
+
.join(' ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static formatSeconds(seconds: number): string {
|
|
84
|
+
const days_ = Math.floor(seconds / (3600 * 24));
|
|
85
|
+
seconds %= 3600 * 24;
|
|
86
|
+
|
|
87
|
+
const hours = Math.floor(seconds / 3600)
|
|
88
|
+
.toString()
|
|
89
|
+
.padStart(2, '0');
|
|
90
|
+
seconds %= 3600;
|
|
91
|
+
|
|
92
|
+
const minutes = Math.floor(seconds / 60)
|
|
93
|
+
.toString()
|
|
94
|
+
.padStart(2, '0');
|
|
95
|
+
seconds %= 60;
|
|
96
|
+
|
|
97
|
+
const secondsStr = Math.floor(seconds).toString().padStart(2, '0');
|
|
98
|
+
|
|
99
|
+
let text = '';
|
|
100
|
+
if (days_) {
|
|
101
|
+
text += `${days_} ${I18n.t('ra_daysShortText')} `;
|
|
102
|
+
}
|
|
103
|
+
text += `${hours}:${minutes}:${secondsStr}`;
|
|
104
|
+
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the name of the object by id from the name or description.
|
|
110
|
+
*/
|
|
111
|
+
static getObjectName(
|
|
112
|
+
objects: Record<string, ioBroker.Object>,
|
|
113
|
+
id: string,
|
|
114
|
+
settings?: { name: ioBroker.StringOrTranslated } | ioBroker.Languages | null,
|
|
115
|
+
options?: { language?: ioBroker.Languages },
|
|
116
|
+
/** Set to true to get the description. */
|
|
117
|
+
isDesc?: boolean,
|
|
118
|
+
): string {
|
|
119
|
+
const item = objects[id];
|
|
120
|
+
let text: string | undefined;
|
|
121
|
+
|
|
122
|
+
if (typeof settings === 'string' && !options) {
|
|
123
|
+
options = { language: settings };
|
|
124
|
+
settings = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
options = options || {};
|
|
128
|
+
if (!options.language) {
|
|
129
|
+
options.language =
|
|
130
|
+
(objects['system.config'] &&
|
|
131
|
+
objects['system.config'].common &&
|
|
132
|
+
objects['system.config'].common.language) ||
|
|
133
|
+
window.sysLang ||
|
|
134
|
+
'en';
|
|
135
|
+
}
|
|
136
|
+
if ((settings as { name: ioBroker.StringOrTranslated })?.name) {
|
|
137
|
+
const textObj = (settings as { name: ioBroker.StringOrTranslated }).name;
|
|
138
|
+
if (typeof textObj === 'object') {
|
|
139
|
+
text = (options.language && textObj[options.language]) || textObj.en;
|
|
140
|
+
} else {
|
|
141
|
+
text = textObj as string;
|
|
142
|
+
}
|
|
143
|
+
} else if (isDesc && item?.common?.desc) {
|
|
144
|
+
const textObj = item.common.desc;
|
|
145
|
+
if (typeof textObj === 'object') {
|
|
146
|
+
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
|
|
147
|
+
} else {
|
|
148
|
+
text = textObj as string;
|
|
149
|
+
}
|
|
150
|
+
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
151
|
+
|
|
152
|
+
if (text === text.toUpperCase()) {
|
|
153
|
+
text = text[0] + text.substring(1).toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
} else if (!isDesc && item?.common) {
|
|
156
|
+
const textObj = item.common.name || item.common.desc;
|
|
157
|
+
if (textObj && typeof textObj === 'object') {
|
|
158
|
+
text = (options.language && textObj[options.language]) || textObj.en || textObj.de || textObj.ru || '';
|
|
159
|
+
} else {
|
|
160
|
+
text = textObj as string;
|
|
161
|
+
}
|
|
162
|
+
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
163
|
+
|
|
164
|
+
if (text === text.toUpperCase()) {
|
|
165
|
+
text = text[0] + text.substring(1).toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
const pos = id.lastIndexOf('.');
|
|
169
|
+
text = id.substring(pos + 1).replace(/[_.]/g, ' ');
|
|
170
|
+
text = Utils.CapitalWords(text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return text?.trim() || '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the name of the object from the name or description.
|
|
178
|
+
*/
|
|
179
|
+
static getObjectNameFromObj(
|
|
180
|
+
obj: ioBroker.PartialObject,
|
|
181
|
+
/** settings or language */
|
|
182
|
+
settings: { name: ioBroker.StringOrTranslated } | ioBroker.Languages | null,
|
|
183
|
+
options?: { language?: ioBroker.Languages },
|
|
184
|
+
/** Set to true to get the description. */
|
|
185
|
+
isDesc?: boolean,
|
|
186
|
+
/** Allow using spaces in name (by edit) */
|
|
187
|
+
noTrim?: boolean,
|
|
188
|
+
): string {
|
|
189
|
+
const item = obj;
|
|
190
|
+
let text = (obj && obj._id) || '';
|
|
191
|
+
|
|
192
|
+
if (typeof settings === 'string' && !options) {
|
|
193
|
+
options = { language: settings };
|
|
194
|
+
settings = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
options = options || {};
|
|
198
|
+
|
|
199
|
+
if ((settings as { name: ioBroker.StringOrTranslated })?.name) {
|
|
200
|
+
const name = (settings as { name: ioBroker.StringOrTranslated }).name;
|
|
201
|
+
if (typeof name === 'object') {
|
|
202
|
+
text = (options.language && name[options.language]) || name.en;
|
|
203
|
+
} else {
|
|
204
|
+
text = name;
|
|
205
|
+
}
|
|
206
|
+
} else if (isDesc && item?.common?.desc) {
|
|
207
|
+
const desc: ioBroker.StringOrTranslated = item.common.desc;
|
|
208
|
+
if (typeof desc === 'object') {
|
|
209
|
+
text = (options.language && desc[options.language]) || desc.en;
|
|
210
|
+
} else {
|
|
211
|
+
text = desc;
|
|
212
|
+
}
|
|
213
|
+
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
214
|
+
|
|
215
|
+
if (text === text.toUpperCase()) {
|
|
216
|
+
text = text[0] + text.substring(1).toLowerCase();
|
|
217
|
+
}
|
|
218
|
+
} else if (!isDesc && item?.common?.name) {
|
|
219
|
+
let name = item.common.name;
|
|
220
|
+
if (!name && item.common.desc) {
|
|
221
|
+
name = item.common.desc;
|
|
222
|
+
}
|
|
223
|
+
if (typeof name === 'object') {
|
|
224
|
+
text = (options.language && name[options.language]) || name.en;
|
|
225
|
+
} else {
|
|
226
|
+
text = name;
|
|
227
|
+
}
|
|
228
|
+
text = (text || '').toString().replace(/[_.]/g, ' ');
|
|
229
|
+
|
|
230
|
+
if (text === text.toUpperCase()) {
|
|
231
|
+
text = text[0] + text.substring(1).toLowerCase();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return noTrim ? text : text.trim();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extracts from the object material settings, depends on username
|
|
239
|
+
*/
|
|
240
|
+
static getSettingsOrder(
|
|
241
|
+
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
242
|
+
forEnumId: string,
|
|
243
|
+
options: { user?: string },
|
|
244
|
+
): string | null {
|
|
245
|
+
let common: ioBroker.StateCommon | undefined;
|
|
246
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
247
|
+
common = (obj as ioBroker.StateObject).common;
|
|
248
|
+
} else {
|
|
249
|
+
common = obj as any as ioBroker.StateCommon;
|
|
250
|
+
}
|
|
251
|
+
let settings;
|
|
252
|
+
if (common?.custom) {
|
|
253
|
+
settings = common.custom[NAMESPACE];
|
|
254
|
+
const user = options.user || 'admin';
|
|
255
|
+
if (settings && settings[user]) {
|
|
256
|
+
if (forEnumId) {
|
|
257
|
+
if (settings[user].subOrder && settings[user].subOrder[forEnumId]) {
|
|
258
|
+
return JSON.parse(JSON.stringify(settings[user].subOrder[forEnumId]));
|
|
259
|
+
}
|
|
260
|
+
} else if (settings[user].order) {
|
|
261
|
+
return JSON.parse(JSON.stringify(settings[user].order));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
Used in material
|
|
270
|
+
*/
|
|
271
|
+
static getSettingsCustomURLs(
|
|
272
|
+
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
273
|
+
forEnumId: string,
|
|
274
|
+
options: { user?: string },
|
|
275
|
+
): string | null {
|
|
276
|
+
let common: ioBroker.StateCommon | undefined;
|
|
277
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
278
|
+
common = (obj as ioBroker.StateObject).common;
|
|
279
|
+
} else {
|
|
280
|
+
common = obj as any as ioBroker.StateCommon;
|
|
281
|
+
}
|
|
282
|
+
let settings;
|
|
283
|
+
if (common?.custom) {
|
|
284
|
+
settings = common.custom[NAMESPACE];
|
|
285
|
+
const user = options.user || 'admin';
|
|
286
|
+
if (settings && settings[user]) {
|
|
287
|
+
if (forEnumId) {
|
|
288
|
+
if (settings[user].subURLs && settings[user].subURLs[forEnumId]) {
|
|
289
|
+
return JSON.parse(JSON.stringify(settings[user].subURLs[forEnumId]));
|
|
290
|
+
}
|
|
291
|
+
} else if (settings[user].URLs) {
|
|
292
|
+
return JSON.parse(JSON.stringify(settings[user].URLs));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Reorder the array items in list between source and dest.
|
|
301
|
+
*/
|
|
302
|
+
static reorder(
|
|
303
|
+
list: Iterable<any> | ArrayLike<any>,
|
|
304
|
+
source: number,
|
|
305
|
+
dest: number,
|
|
306
|
+
): Iterable<any> | ArrayLike<any> {
|
|
307
|
+
const result = Array.from(list);
|
|
308
|
+
const [removed] = result.splice(source, 1);
|
|
309
|
+
result.splice(dest, 0, removed);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
Get smart name settings for the given object.
|
|
315
|
+
*/
|
|
316
|
+
static getSettings(
|
|
317
|
+
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
318
|
+
options: {
|
|
319
|
+
id?: string;
|
|
320
|
+
user?: string;
|
|
321
|
+
name?: ioBroker.StringOrTranslated;
|
|
322
|
+
icon?: string;
|
|
323
|
+
color?: string;
|
|
324
|
+
language?: ioBroker.Languages;
|
|
325
|
+
},
|
|
326
|
+
defaultEnabling?: boolean,
|
|
327
|
+
) {
|
|
328
|
+
let settings;
|
|
329
|
+
const id = (obj as ioBroker.StateObject)?._id || options?.id;
|
|
330
|
+
let common: ioBroker.StateCommon | undefined;
|
|
331
|
+
if (obj && Object.prototype.hasOwnProperty.call(obj, 'common')) {
|
|
332
|
+
common = (obj as ioBroker.StateObject).common;
|
|
333
|
+
} else {
|
|
334
|
+
common = obj as ioBroker.StateCommon;
|
|
335
|
+
}
|
|
336
|
+
if (common?.custom) {
|
|
337
|
+
settings = common.custom;
|
|
338
|
+
settings =
|
|
339
|
+
settings[NAMESPACE] && settings[NAMESPACE][options.user || 'admin']
|
|
340
|
+
? JSON.parse(JSON.stringify(settings[NAMESPACE][options.user || 'admin']))
|
|
341
|
+
: { enabled: true };
|
|
342
|
+
} else {
|
|
343
|
+
settings = { enabled: defaultEnabling === undefined ? true : defaultEnabling, useCustom: false };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!Object.prototype.hasOwnProperty.call(settings, 'enabled')) {
|
|
347
|
+
settings.enabled = defaultEnabling === undefined ? true : defaultEnabling;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (options) {
|
|
351
|
+
if (!settings.name && options.name) {
|
|
352
|
+
settings.name = options.name;
|
|
353
|
+
}
|
|
354
|
+
if (!settings.icon && options.icon) {
|
|
355
|
+
settings.icon = options.icon;
|
|
356
|
+
}
|
|
357
|
+
if (!settings.color && options.color) {
|
|
358
|
+
settings.color = options.color;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (common) {
|
|
363
|
+
if (!settings.color && common.color) {
|
|
364
|
+
settings.color = common.color;
|
|
365
|
+
}
|
|
366
|
+
if (!settings.icon && common.icon) {
|
|
367
|
+
settings.icon = common.icon;
|
|
368
|
+
}
|
|
369
|
+
if (!settings.name && common.name) {
|
|
370
|
+
settings.name = common.name;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (typeof settings.name === 'object') {
|
|
375
|
+
settings.name = (options.language && settings.name[options.language]) || settings.name.en;
|
|
376
|
+
|
|
377
|
+
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
|
|
378
|
+
|
|
379
|
+
if (settings.name === settings.name.toUpperCase()) {
|
|
380
|
+
settings.name = settings.name[0] + settings.name.substring(1).toLowerCase();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (!settings.name && id) {
|
|
384
|
+
const pos = id.lastIndexOf('.');
|
|
385
|
+
settings.name = id.substring(pos + 1).replace(/[_.]/g, ' ');
|
|
386
|
+
settings.name = (settings.name || '').toString().replace(/_/g, ' ');
|
|
387
|
+
settings.name = Utils.CapitalWords(settings.name);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return settings;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
Sets smartName settings for the given object.
|
|
395
|
+
*/
|
|
396
|
+
static setSettings(
|
|
397
|
+
obj: Partial<ioBroker.Object>,
|
|
398
|
+
settings: Record<string, any>,
|
|
399
|
+
options: { user?: string; language?: ioBroker.Languages },
|
|
400
|
+
): boolean {
|
|
401
|
+
if (obj) {
|
|
402
|
+
obj.common = obj.common || ({} as ioBroker.StateCommon);
|
|
403
|
+
obj.common.custom = obj.common.custom || {};
|
|
404
|
+
obj.common.custom[NAMESPACE] = obj.common.custom[NAMESPACE] || {};
|
|
405
|
+
obj.common.custom[NAMESPACE][options.user || 'admin'] = settings;
|
|
406
|
+
const s = obj.common.custom[NAMESPACE][options.user || 'admin'];
|
|
407
|
+
if (s.useCommon) {
|
|
408
|
+
if (s.color !== undefined) {
|
|
409
|
+
obj.common.color = s.color;
|
|
410
|
+
delete s.color;
|
|
411
|
+
}
|
|
412
|
+
if (s.icon !== undefined) {
|
|
413
|
+
obj.common.icon = s.icon;
|
|
414
|
+
delete s.icon;
|
|
415
|
+
}
|
|
416
|
+
if (s.name !== undefined) {
|
|
417
|
+
if (typeof obj.common.name !== 'object' && options.language) {
|
|
418
|
+
obj.common.name = { [options.language]: s.name } as ioBroker.StringOrTranslated;
|
|
419
|
+
} else if (typeof obj.common.name === 'object' && options.language) {
|
|
420
|
+
obj.common.name[options.language] = s.name;
|
|
421
|
+
}
|
|
422
|
+
delete s.name;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Get the icon for the given settings.
|
|
434
|
+
*/
|
|
435
|
+
static getIcon(
|
|
436
|
+
settings: { icon?: string; name?: string; prefix?: string },
|
|
437
|
+
style?: React.CSSProperties,
|
|
438
|
+
): React.JSX.Element | null {
|
|
439
|
+
if (settings?.icon) {
|
|
440
|
+
// If UTF-8 icon
|
|
441
|
+
if (settings.icon.length <= 2) {
|
|
442
|
+
return <span style={style || {}}>{settings.icon}</span>;
|
|
443
|
+
}
|
|
444
|
+
if (settings.icon.startsWith('data:image')) {
|
|
445
|
+
return <img alt={settings.name} src={settings.icon} style={style || {}} />;
|
|
446
|
+
}
|
|
447
|
+
// maybe later some changes for a second type
|
|
448
|
+
return <img alt={settings.name} src={(settings.prefix || '') + settings.icon} style={style} />;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get the icon for the given object.
|
|
455
|
+
*/
|
|
456
|
+
static getObjectIcon(id: string | ioBroker.PartialObject, obj?: ioBroker.PartialObject): string | null {
|
|
457
|
+
// If id is Object
|
|
458
|
+
if (typeof id === 'object') {
|
|
459
|
+
obj = id as ioBroker.PartialObject;
|
|
460
|
+
id = obj?._id as string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (obj?.common?.icon) {
|
|
464
|
+
let icon = obj.common.icon;
|
|
465
|
+
// If UTF-8 icon
|
|
466
|
+
if (typeof icon === 'string' && icon.length <= 2) {
|
|
467
|
+
return icon;
|
|
468
|
+
}
|
|
469
|
+
if (icon.startsWith('data:image')) {
|
|
470
|
+
return icon;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const parts = id.split('.');
|
|
474
|
+
if (parts[0] === 'system') {
|
|
475
|
+
icon = `adapter/${parts[2]}${icon.startsWith('/') ? '' : '/'}${icon}`;
|
|
476
|
+
} else {
|
|
477
|
+
icon = `adapter/${parts[0]}${icon.startsWith('/') ? '' : '/'}${icon}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (window.location.pathname.match(/adapter\/[^/]+\/[^/]+\.html/)) {
|
|
481
|
+
icon = `../../${icon}`;
|
|
482
|
+
} else if (window.location.pathname.match(/material\/[.\d]+/)) {
|
|
483
|
+
icon = `../../${icon}`;
|
|
484
|
+
} else if (window.location.pathname.match(/material\//)) {
|
|
485
|
+
icon = `../${icon}`;
|
|
486
|
+
}
|
|
487
|
+
return icon;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Converts word1_word2 to word1Word2.
|
|
495
|
+
*/
|
|
496
|
+
static splitCamelCase(text: string | null | undefined): string {
|
|
497
|
+
// if (false && text !== text.toUpperCase()) {
|
|
498
|
+
// const words = text.split(/\s+/);
|
|
499
|
+
// for (let i = 0; i < words.length; i++) {
|
|
500
|
+
// const word = words[i];
|
|
501
|
+
// if (word.toLowerCase() !== word && word.toUpperCase() !== word) {
|
|
502
|
+
// let z = 0;
|
|
503
|
+
// const ww = [];
|
|
504
|
+
// let start = 0;
|
|
505
|
+
// while (z < word.length) {
|
|
506
|
+
// if (word[z].match(/[A-ZÜÄÖА-Я]/)) {
|
|
507
|
+
// ww.push(word.substring(start, z));
|
|
508
|
+
// start = z;
|
|
509
|
+
// }
|
|
510
|
+
// z++;
|
|
511
|
+
// }
|
|
512
|
+
// if (start !== z) {
|
|
513
|
+
// ww.push(word.substring(start, z));
|
|
514
|
+
// }
|
|
515
|
+
// for (let k = 0; k < ww.length; k++) {
|
|
516
|
+
// words.splice(i + k, 0, ww[k]);
|
|
517
|
+
// }
|
|
518
|
+
// i += ww.length;
|
|
519
|
+
// }
|
|
520
|
+
// }
|
|
521
|
+
//
|
|
522
|
+
// return words.map(w => {
|
|
523
|
+
// w = w.trim();
|
|
524
|
+
// if (w) {
|
|
525
|
+
// return w[0].toUpperCase() + w.substring(1).toLowerCase();
|
|
526
|
+
// }
|
|
527
|
+
// return '';
|
|
528
|
+
// }).join(' ');
|
|
529
|
+
// }
|
|
530
|
+
return text ? Utils.CapitalWords(text) : '';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Check if the given color is bright.
|
|
535
|
+
* https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
|
|
536
|
+
*/
|
|
537
|
+
static isUseBright(color: string | null | undefined, defaultValue?: boolean): boolean {
|
|
538
|
+
if (!color) {
|
|
539
|
+
return defaultValue === undefined ? true : defaultValue;
|
|
540
|
+
}
|
|
541
|
+
color = color.toString();
|
|
542
|
+
if (color.startsWith('#')) {
|
|
543
|
+
color = color.slice(1);
|
|
544
|
+
}
|
|
545
|
+
let r;
|
|
546
|
+
let g;
|
|
547
|
+
let b;
|
|
548
|
+
|
|
549
|
+
const rgb = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
|
550
|
+
if (rgb && rgb.length === 4) {
|
|
551
|
+
r = parseInt(rgb[1], 10);
|
|
552
|
+
g = parseInt(rgb[2], 10);
|
|
553
|
+
b = parseInt(rgb[3], 10);
|
|
554
|
+
} else {
|
|
555
|
+
// convert 3-digit hex to 6-digits.
|
|
556
|
+
if (color.length === 3) {
|
|
557
|
+
color = color[0] + color[0] + color[1] + color[1] + color[2] + color[2];
|
|
558
|
+
}
|
|
559
|
+
// remove alfa channel
|
|
560
|
+
if (color.length === 8) {
|
|
561
|
+
color = color.substring(0, 6);
|
|
562
|
+
} else if (color.length !== 6) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
r = parseInt(color.slice(0, 2), 16);
|
|
567
|
+
g = parseInt(color.slice(2, 4), 16);
|
|
568
|
+
b = parseInt(color.slice(4, 6), 16);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// http://stackoverflow.com/a/3943023/112731
|
|
572
|
+
return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Get the time string in the format 00:00.
|
|
577
|
+
*/
|
|
578
|
+
static getTimeString(seconds: string | number): string {
|
|
579
|
+
seconds = parseFloat(seconds as string);
|
|
580
|
+
if (Number.isNaN(seconds)) {
|
|
581
|
+
return '--:--';
|
|
582
|
+
}
|
|
583
|
+
const hours = Math.floor(seconds / 3600);
|
|
584
|
+
const minutes = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
|
|
585
|
+
const secs = (seconds % 60).toString().padStart(2, '0');
|
|
586
|
+
if (hours) {
|
|
587
|
+
return `${hours}:${minutes}:${secs}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return `${minutes}:${secs}`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Gets the wind direction with the given angle (degrees).
|
|
595
|
+
*/
|
|
596
|
+
static getWindDirection(
|
|
597
|
+
/** angle in degrees from 0° to 360° */
|
|
598
|
+
angle: number,
|
|
599
|
+
): string {
|
|
600
|
+
if (angle >= 0 && angle < 11.25) {
|
|
601
|
+
return 'N';
|
|
602
|
+
}
|
|
603
|
+
if (angle >= 11.25 && angle < 33.75) {
|
|
604
|
+
return 'NNE';
|
|
605
|
+
}
|
|
606
|
+
if (angle >= 33.75 && angle < 56.25) {
|
|
607
|
+
return 'NE';
|
|
608
|
+
}
|
|
609
|
+
if (angle >= 56.25 && angle < 78.75) {
|
|
610
|
+
return 'ENE';
|
|
611
|
+
}
|
|
612
|
+
if (angle >= 78.75 && angle < 101.25) {
|
|
613
|
+
return 'E';
|
|
614
|
+
}
|
|
615
|
+
if (angle >= 101.25 && angle < 123.75) {
|
|
616
|
+
return 'ESE';
|
|
617
|
+
}
|
|
618
|
+
if (angle >= 123.75 && angle < 146.25) {
|
|
619
|
+
return 'SE';
|
|
620
|
+
}
|
|
621
|
+
if (angle >= 146.25 && angle < 168.75) {
|
|
622
|
+
return 'SSE';
|
|
623
|
+
}
|
|
624
|
+
if (angle >= 168.75 && angle < 191.25) {
|
|
625
|
+
return 'S';
|
|
626
|
+
}
|
|
627
|
+
if (angle >= 191.25 && angle < 213.75) {
|
|
628
|
+
return 'SSW';
|
|
629
|
+
}
|
|
630
|
+
if (angle >= 213.75 && angle < 236.25) {
|
|
631
|
+
return 'SW';
|
|
632
|
+
}
|
|
633
|
+
if (angle >= 236.25 && angle < 258.75) {
|
|
634
|
+
return 'WSW';
|
|
635
|
+
}
|
|
636
|
+
if (angle >= 258.75 && angle < 281.25) {
|
|
637
|
+
return 'W';
|
|
638
|
+
}
|
|
639
|
+
if (angle >= 281.25 && angle < 303.75) {
|
|
640
|
+
return 'WNW';
|
|
641
|
+
}
|
|
642
|
+
if (angle >= 303.75 && angle < 326.25) {
|
|
643
|
+
return 'NW';
|
|
644
|
+
}
|
|
645
|
+
if (angle >= 326.25 && angle < 348.75) {
|
|
646
|
+
return 'NNW';
|
|
647
|
+
}
|
|
648
|
+
// if (angle >= 348.75) {
|
|
649
|
+
return 'N';
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Pad the given number with a zero if it's not two digits long.
|
|
654
|
+
*/
|
|
655
|
+
static padding(num: string | number): string {
|
|
656
|
+
if (typeof num === 'string') {
|
|
657
|
+
if (num.length < 2) {
|
|
658
|
+
return `0${num}`;
|
|
659
|
+
}
|
|
660
|
+
return num;
|
|
661
|
+
}
|
|
662
|
+
if (num < 10) {
|
|
663
|
+
return `0${num}`;
|
|
664
|
+
}
|
|
665
|
+
return num.toString();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Sets the date format.
|
|
670
|
+
*/
|
|
671
|
+
static setDataFormat(format: string): void {
|
|
672
|
+
if (format) {
|
|
673
|
+
Utils.dateFormat = format.toUpperCase().split(/[.-/]/);
|
|
674
|
+
Utils.dateFormat.splice(Utils.dateFormat.indexOf('YYYY'), 1);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Converts the date to a string.
|
|
680
|
+
*/
|
|
681
|
+
static date2string(now: string | number | Date): string {
|
|
682
|
+
if (typeof now === 'string') {
|
|
683
|
+
now = now.trim();
|
|
684
|
+
if (!now) {
|
|
685
|
+
return '';
|
|
686
|
+
}
|
|
687
|
+
// only letters
|
|
688
|
+
if (now.match(/^[\w\s]+$/)) {
|
|
689
|
+
// Day of the week
|
|
690
|
+
return now;
|
|
691
|
+
}
|
|
692
|
+
const m = now.match(/(\d{1,4})[-./](\d{1,2})[-./](\d{1,4})/);
|
|
693
|
+
if (m) {
|
|
694
|
+
const a = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
695
|
+
// We now have 3 numbers. Let's try to detect where is year, where is day and where is month
|
|
696
|
+
const year = a.find(y => y > 31);
|
|
697
|
+
if (year !== undefined) {
|
|
698
|
+
a.splice(a.indexOf(year), 1);
|
|
699
|
+
|
|
700
|
+
const day = a.find(mm => mm > 12);
|
|
701
|
+
if (day) {
|
|
702
|
+
a.splice(a.indexOf(day), 1);
|
|
703
|
+
now = new Date(year, a[0] - 1, day);
|
|
704
|
+
} else if (Utils.dateFormat[0][0] === 'M' && Utils.dateFormat[1][0] === 'D') {
|
|
705
|
+
// MM DD
|
|
706
|
+
now = new Date(year, a[0] - 1, a[1]);
|
|
707
|
+
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
|
|
708
|
+
now = new Date(year, a[1] - 1, a[0]);
|
|
709
|
+
}
|
|
710
|
+
} else if (Utils.dateFormat[0][0] === 'D' && Utils.dateFormat[1][0] === 'M') {
|
|
711
|
+
// DD MM
|
|
712
|
+
now = new Date(year, a[1] - 1, a[0]);
|
|
713
|
+
if (Math.abs(now.getTime() - Date.now()) > 3600000 * 24 * 10) {
|
|
714
|
+
now = new Date(year, a[0] - 1, a[1]);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
now = new Date(now);
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
now = new Date(now);
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
now = new Date(now);
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
now = new Date(now);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
let date = I18n.t(`ra_dow_${days[now.getDay()]}`).replace('ra_dow_', '');
|
|
730
|
+
date += `. ${now.getDate()} ${I18n.t(`ra_month_${months[now.getMonth()]}`).replace('ra_month_', '')}`;
|
|
731
|
+
return date;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Render a text as a link.
|
|
736
|
+
*/
|
|
737
|
+
static renderTextWithA(text: string): React.JSX.Element[] | string {
|
|
738
|
+
let m: RegExpMatchArray | null = text.match(/<a [^<]+<\/a>|<br\s?\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/);
|
|
739
|
+
if (m) {
|
|
740
|
+
const result: React.JSX.Element[] = [];
|
|
741
|
+
let key = 1;
|
|
742
|
+
do {
|
|
743
|
+
const start = text.substring(0, m.index);
|
|
744
|
+
text = text.substring((m.index || 0) + m[0].length);
|
|
745
|
+
start && result.push(<span key={`a${key++}`}>{start}</span>);
|
|
746
|
+
|
|
747
|
+
if (m[0].startsWith('<b>')) {
|
|
748
|
+
result.push(<b key={`a${key++}`}>{m[0].substring(3, m[0].length - 4)}</b>);
|
|
749
|
+
} else if (m[0].startsWith('<i>')) {
|
|
750
|
+
result.push(<i key={`a${key++}`}>{m[0].substring(3, m[0].length - 4)}</i>);
|
|
751
|
+
} else if (m[0].startsWith('<br')) {
|
|
752
|
+
result.push(<br key={`a${key++}`} />);
|
|
753
|
+
} else {
|
|
754
|
+
const href = m[0].match(/href="([^"]+)"/) || m[0].match(/href='([^']+)'/);
|
|
755
|
+
const target = m[0].match(/target="([^"]+)"/) || m[0].match(/target='([^']+)'/);
|
|
756
|
+
const rel = m[0].match(/rel="([^"]+)"/) || m[0].match(/rel='([^']+)'/);
|
|
757
|
+
const title = m[0].match(/>([^<]*)</);
|
|
758
|
+
|
|
759
|
+
// eslint-disable-next-line
|
|
760
|
+
result.push(<a
|
|
761
|
+
key={`a${key++}`}
|
|
762
|
+
href={href ? href[1] : ''}
|
|
763
|
+
target={target ? target[1] : '_blank'}
|
|
764
|
+
rel={rel ? rel[1] : ''}
|
|
765
|
+
style={{ color: 'inherit' }}
|
|
766
|
+
>
|
|
767
|
+
{title ? title[1] : ''}
|
|
768
|
+
</a>);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
m = text ? text.match(/<a [^<]+<\/a>|<br\/?>|<b>[^<]+<\/b>|<i>[^<]+<\/i>/) : null;
|
|
772
|
+
if (!m) {
|
|
773
|
+
text && result.push(<span key={`a${key++}`}>{text}</span>);
|
|
774
|
+
}
|
|
775
|
+
} while (m);
|
|
776
|
+
|
|
777
|
+
return result;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return text;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Get the smart name of the given state.
|
|
785
|
+
*/
|
|
786
|
+
static getSmartName(
|
|
787
|
+
states: Record<string, ioBroker.StateObject> | ioBroker.StateObject | ioBroker.StateCommon,
|
|
788
|
+
id: string,
|
|
789
|
+
instanceId: string,
|
|
790
|
+
noCommon?: boolean,
|
|
791
|
+
): SmartName | undefined {
|
|
792
|
+
if (!id) {
|
|
793
|
+
if (!noCommon) {
|
|
794
|
+
if (!(states as ioBroker.StateObject).common) {
|
|
795
|
+
return (states as ioBroker.StateCommon).smartName;
|
|
796
|
+
}
|
|
797
|
+
if (states && !(states as ioBroker.StateObject).common) {
|
|
798
|
+
return (states as ioBroker.StateCommon).smartName;
|
|
799
|
+
}
|
|
800
|
+
return (states as ioBroker.StateObject).common.smartName;
|
|
801
|
+
}
|
|
802
|
+
if (states && !(states as ioBroker.StateObject).common) {
|
|
803
|
+
return (states as ioBroker.StateCommon).smartName;
|
|
804
|
+
}
|
|
805
|
+
const obj = states as ioBroker.StateObject;
|
|
806
|
+
return obj?.common?.custom && obj.common.custom[instanceId] ?
|
|
807
|
+
obj.common.custom[instanceId].smartName : undefined;
|
|
808
|
+
}
|
|
809
|
+
if (!noCommon) {
|
|
810
|
+
return (states as Record<string, ioBroker.StateObject>)[id].common.smartName;
|
|
811
|
+
}
|
|
812
|
+
const obj = (states as Record<string, ioBroker.StateObject>)[id];
|
|
813
|
+
|
|
814
|
+
return obj?.common?.custom && obj.common.custom[instanceId] ?
|
|
815
|
+
obj.common.custom[instanceId].smartName || null : null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Get the smart name from a state.
|
|
820
|
+
*/
|
|
821
|
+
static getSmartNameFromObj(
|
|
822
|
+
obj: ioBroker.StateObject | ioBroker.StateCommon,
|
|
823
|
+
instanceId: string,
|
|
824
|
+
noCommon?: boolean,
|
|
825
|
+
): SmartName | undefined {
|
|
826
|
+
if (!noCommon) {
|
|
827
|
+
if (!(obj as ioBroker.StateObject).common) {
|
|
828
|
+
return (obj as ioBroker.StateCommon).smartName;
|
|
829
|
+
}
|
|
830
|
+
if (obj && !(obj as ioBroker.StateObject).common) {
|
|
831
|
+
return (obj as ioBroker.StateCommon).smartName;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return (obj as ioBroker.StateObject).common.smartName;
|
|
835
|
+
}
|
|
836
|
+
if (obj && !(obj as ioBroker.StateObject).common) {
|
|
837
|
+
return (obj as ioBroker.StateCommon).smartName;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const custom: Record<string, string> | undefined | null = (obj as ioBroker.StateObject)?.common?.custom?.[instanceId];
|
|
841
|
+
|
|
842
|
+
return custom ? custom.smartName : undefined;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Enable smart name for a state.
|
|
847
|
+
*/
|
|
848
|
+
static enableSmartName(
|
|
849
|
+
obj: ioBroker.StateObject,
|
|
850
|
+
instanceId: string,
|
|
851
|
+
noCommon?: boolean,
|
|
852
|
+
): void {
|
|
853
|
+
if (noCommon) {
|
|
854
|
+
obj.common.custom = obj.common.custom || {};
|
|
855
|
+
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
856
|
+
obj.common.custom[instanceId].smartName = {};
|
|
857
|
+
} else {
|
|
858
|
+
obj.common.smartName = {};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Completely remove smart name from a state.
|
|
864
|
+
*/
|
|
865
|
+
static removeSmartName(
|
|
866
|
+
obj: ioBroker.StateObject,
|
|
867
|
+
instanceId: string,
|
|
868
|
+
noCommon?: boolean,
|
|
869
|
+
) {
|
|
870
|
+
if (noCommon) {
|
|
871
|
+
if (obj.common && obj.common.custom && obj.common.custom[instanceId]) {
|
|
872
|
+
obj.common.custom[instanceId] = null;
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
obj.common.smartName = null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Update the smart name of a state.
|
|
881
|
+
*/
|
|
882
|
+
static updateSmartName(
|
|
883
|
+
obj: ioBroker.StateObject,
|
|
884
|
+
newSmartName: ioBroker.StringOrTranslated,
|
|
885
|
+
byON: string | null,
|
|
886
|
+
smartType: string | null,
|
|
887
|
+
instanceId: string,
|
|
888
|
+
noCommon?: boolean,
|
|
889
|
+
) {
|
|
890
|
+
const language = I18n.getLanguage();
|
|
891
|
+
|
|
892
|
+
// convert the old format
|
|
893
|
+
if (typeof obj.common.smartName === 'string') {
|
|
894
|
+
const nnn = obj.common.smartName;
|
|
895
|
+
obj.common.smartName = {};
|
|
896
|
+
obj.common.smartName[language] = nnn;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// convert the old settings
|
|
900
|
+
if (obj.native && obj.native.byON) {
|
|
901
|
+
delete obj.native.byON;
|
|
902
|
+
let _smartName: SmartName = obj.common.smartName as SmartName;
|
|
903
|
+
|
|
904
|
+
if (_smartName && typeof _smartName !== 'object') {
|
|
905
|
+
_smartName = {
|
|
906
|
+
en: _smartName as string,
|
|
907
|
+
[language]: _smartName as string,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
obj.common.smartName = _smartName;
|
|
911
|
+
}
|
|
912
|
+
if (smartType !== undefined) {
|
|
913
|
+
if (noCommon) {
|
|
914
|
+
obj.common.custom = obj.common.custom || {};
|
|
915
|
+
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
916
|
+
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
917
|
+
if (!smartType) {
|
|
918
|
+
delete obj.common.custom[instanceId].smartName.smartType;
|
|
919
|
+
} else {
|
|
920
|
+
obj.common.custom[instanceId].smartName.smartType = smartType;
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
obj.common.smartName = obj.common.smartName || {};
|
|
924
|
+
if (!smartType) {
|
|
925
|
+
// @ts-expect-error fixed in js-controller
|
|
926
|
+
delete obj.common.smartName.smartType;
|
|
927
|
+
} else {
|
|
928
|
+
// @ts-expect-error fixed in js-controller
|
|
929
|
+
obj.common.smartName.smartType = smartType;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (byON !== undefined) {
|
|
935
|
+
if (noCommon) {
|
|
936
|
+
obj.common.custom = obj.common.custom || {};
|
|
937
|
+
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
938
|
+
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
939
|
+
obj.common.custom[instanceId].smartName.byON = byON;
|
|
940
|
+
} else {
|
|
941
|
+
obj.common.smartName = obj.common.smartName || {};
|
|
942
|
+
// @ts-expect-error fixed in js-controller
|
|
943
|
+
obj.common.smartName.byON = byON;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (newSmartName !== undefined) {
|
|
948
|
+
let smartName;
|
|
949
|
+
if (noCommon) {
|
|
950
|
+
obj.common.custom = obj.common.custom || {};
|
|
951
|
+
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
952
|
+
obj.common.custom[instanceId].smartName = obj.common.custom[instanceId].smartName || {};
|
|
953
|
+
smartName = obj.common.custom[instanceId].smartName;
|
|
954
|
+
} else {
|
|
955
|
+
obj.common.smartName = obj.common.smartName || {};
|
|
956
|
+
smartName = obj.common.smartName;
|
|
957
|
+
}
|
|
958
|
+
smartName[language] = newSmartName;
|
|
959
|
+
|
|
960
|
+
// If smart name deleted
|
|
961
|
+
if (
|
|
962
|
+
smartName &&
|
|
963
|
+
(!smartName[language] ||
|
|
964
|
+
(smartName[language] === obj.common.name &&
|
|
965
|
+
(!obj.common.role || obj.common.role.includes('button'))))
|
|
966
|
+
) {
|
|
967
|
+
delete smartName[language];
|
|
968
|
+
let empty = true;
|
|
969
|
+
// Check if the structure has any definitions
|
|
970
|
+
for (const key in smartName) {
|
|
971
|
+
if (Object.prototype.hasOwnProperty.call(smartName, key)) {
|
|
972
|
+
empty = false;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// If empty => delete smartName completely
|
|
977
|
+
if (empty) {
|
|
978
|
+
if (noCommon && obj.common.custom && obj.common.custom[instanceId]) {
|
|
979
|
+
if (obj.common.custom[instanceId].smartName.byON === undefined) {
|
|
980
|
+
delete obj.common.custom[instanceId];
|
|
981
|
+
} else {
|
|
982
|
+
delete obj.common.custom[instanceId].en;
|
|
983
|
+
delete obj.common.custom[instanceId].de;
|
|
984
|
+
delete obj.common.custom[instanceId].ru;
|
|
985
|
+
delete obj.common.custom[instanceId].nl;
|
|
986
|
+
delete obj.common.custom[instanceId].pl;
|
|
987
|
+
delete obj.common.custom[instanceId].it;
|
|
988
|
+
delete obj.common.custom[instanceId].fr;
|
|
989
|
+
delete obj.common.custom[instanceId].pt;
|
|
990
|
+
delete obj.common.custom[instanceId].es;
|
|
991
|
+
delete obj.common.custom[instanceId].uk;
|
|
992
|
+
delete obj.common.custom[instanceId]['zh-cn'];
|
|
993
|
+
}
|
|
994
|
+
// @ts-expect-error fixed in js-controller
|
|
995
|
+
} else if (obj.common.smartName && (obj.common.smartName as SmartName).byON !== undefined) {
|
|
996
|
+
const _smartName: { [lang in ioBroker.Languages]?: string } = obj.common.smartName as { [lang in ioBroker.Languages]?: string };
|
|
997
|
+
delete _smartName.en;
|
|
998
|
+
delete _smartName.de;
|
|
999
|
+
delete _smartName.ru;
|
|
1000
|
+
delete _smartName.nl;
|
|
1001
|
+
delete _smartName.pl;
|
|
1002
|
+
delete _smartName.it;
|
|
1003
|
+
delete _smartName.fr;
|
|
1004
|
+
delete _smartName.pt;
|
|
1005
|
+
delete _smartName.es;
|
|
1006
|
+
delete _smartName.uk;
|
|
1007
|
+
delete _smartName['zh-cn'];
|
|
1008
|
+
} else {
|
|
1009
|
+
obj.common.smartName = null;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Disable the smart name of a state.
|
|
1018
|
+
*/
|
|
1019
|
+
static disableSmartName(
|
|
1020
|
+
obj: ioBroker.StateObject,
|
|
1021
|
+
instanceId: string,
|
|
1022
|
+
noCommon?: boolean,
|
|
1023
|
+
): void {
|
|
1024
|
+
if (noCommon) {
|
|
1025
|
+
obj.common.custom = obj.common.custom || {};
|
|
1026
|
+
obj.common.custom[instanceId] = obj.common.custom[instanceId] || {};
|
|
1027
|
+
obj.common.custom[instanceId].smartName = false;
|
|
1028
|
+
} else {
|
|
1029
|
+
obj.common.smartName = false;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Copy text to the clipboard.
|
|
1035
|
+
*/
|
|
1036
|
+
static copyToClipboard(
|
|
1037
|
+
text: string,
|
|
1038
|
+
e?: Event,
|
|
1039
|
+
): boolean {
|
|
1040
|
+
if (e) {
|
|
1041
|
+
e.stopPropagation();
|
|
1042
|
+
e.preventDefault();
|
|
1043
|
+
}
|
|
1044
|
+
return copy(text);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Gets the extension of a file name.
|
|
1049
|
+
* @param fileName the file name.
|
|
1050
|
+
* @returns The extension in lower case.
|
|
1051
|
+
*/
|
|
1052
|
+
static getFileExtension(fileName: string): string | null {
|
|
1053
|
+
const pos = (fileName || '').lastIndexOf('.');
|
|
1054
|
+
if (pos !== -1) {
|
|
1055
|
+
return fileName.substring(pos + 1).toLowerCase();
|
|
1056
|
+
}
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Format number of bytes as a string with B, KB, MB or GB.
|
|
1062
|
+
* The base for all calculations is 1024.
|
|
1063
|
+
* @returns The formatted string (e.g. '723.5 KB')
|
|
1064
|
+
*/
|
|
1065
|
+
static formatBytes(
|
|
1066
|
+
/** The number of bytes. */
|
|
1067
|
+
bytes: number,
|
|
1068
|
+
): string {
|
|
1069
|
+
if (Math.abs(bytes) < 1024) {
|
|
1070
|
+
return `${bytes} B`;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const units = ['KB', 'MB', 'GB'];
|
|
1074
|
+
// const units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
|
|
1075
|
+
let u = -1;
|
|
1076
|
+
|
|
1077
|
+
do {
|
|
1078
|
+
bytes /= 1024;
|
|
1079
|
+
++u;
|
|
1080
|
+
} while (Math.abs(bytes) >= 1024 && u < units.length - 1);
|
|
1081
|
+
|
|
1082
|
+
return `${bytes.toFixed(1)} ${units[u]}`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Invert the given color according to a theme type to get the inverted text color for background
|
|
1087
|
+
*/
|
|
1088
|
+
static getInvertedColor(
|
|
1089
|
+
/** Color in the format '#rrggbb' or '#rgb' (or without a hash) */
|
|
1090
|
+
color: string,
|
|
1091
|
+
themeType: ThemeType,
|
|
1092
|
+
/** dark theme has light color in control, or light theme has light color in control */
|
|
1093
|
+
invert?: boolean,
|
|
1094
|
+
): string | undefined {
|
|
1095
|
+
if (!color) {
|
|
1096
|
+
return undefined;
|
|
1097
|
+
}
|
|
1098
|
+
const invertedColor = Utils.invertColor(color, true);
|
|
1099
|
+
if (invertedColor === '#FFFFFF' && (themeType === 'dark' || (invert && themeType === 'light'))) {
|
|
1100
|
+
return '#DDD';
|
|
1101
|
+
}
|
|
1102
|
+
if (invertedColor === '#000000' && (themeType === 'light' || (invert && themeType === 'dark'))) {
|
|
1103
|
+
return '#222';
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
return undefined;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Big thanks to: https://stackoverflow.com/questions/35969656/how-can-i-generate-the-opposite-color-according-to-current-color
|
|
1110
|
+
/**
|
|
1111
|
+
* Invert the given color
|
|
1112
|
+
* @param hex Color in the format '#rrggbb' or '#rgb' (or without hash)
|
|
1113
|
+
* @param bw Set to black or white.
|
|
1114
|
+
*/
|
|
1115
|
+
static invertColor(hex: string, bw?: boolean): string {
|
|
1116
|
+
if (hex === undefined || hex === null || hex === '' || typeof hex !== 'string') {
|
|
1117
|
+
return '';
|
|
1118
|
+
}
|
|
1119
|
+
if (hex.startsWith('rgba')) {
|
|
1120
|
+
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
|
|
1121
|
+
if (m) {
|
|
1122
|
+
hex =
|
|
1123
|
+
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1124
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1125
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1126
|
+
}
|
|
1127
|
+
} else if (hex.startsWith('rgb')) {
|
|
1128
|
+
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
1129
|
+
if (m) {
|
|
1130
|
+
hex =
|
|
1131
|
+
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1132
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1133
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1134
|
+
}
|
|
1135
|
+
} else if (hex.startsWith('#')) {
|
|
1136
|
+
hex = hex.slice(1);
|
|
1137
|
+
}
|
|
1138
|
+
// convert 3-digit hex to 6-digits.
|
|
1139
|
+
if (hex.length === 3) {
|
|
1140
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1141
|
+
}
|
|
1142
|
+
let alfa = null;
|
|
1143
|
+
if (hex.length === 8) {
|
|
1144
|
+
alfa = hex.substring(6, 8);
|
|
1145
|
+
hex = hex.substring(0, 6);
|
|
1146
|
+
} else if (hex.length !== 6) {
|
|
1147
|
+
console.warn(`Cannot invert color: ${hex}`);
|
|
1148
|
+
return hex;
|
|
1149
|
+
}
|
|
1150
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
1151
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
1152
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
1153
|
+
|
|
1154
|
+
if (bw) {
|
|
1155
|
+
// http://stackoverflow.com/a/3943023/112731
|
|
1156
|
+
return r * 0.299 + g * 0.587 + b * 0.114 > 186
|
|
1157
|
+
? `#000000${alfa || ''}`
|
|
1158
|
+
: `#FFFFFF${alfa || ''}`;
|
|
1159
|
+
}
|
|
1160
|
+
// invert color components
|
|
1161
|
+
const rs = (255 - r).toString(16);
|
|
1162
|
+
const gs = (255 - g).toString(16);
|
|
1163
|
+
const bd = (255 - b).toString(16);
|
|
1164
|
+
// pad each with zeros and return
|
|
1165
|
+
return `#${rs.padStart(2, '0')}${gs.padStart(2, '0')}${bd.padStart(2, '0')}${alfa || ''}`;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Convert RGB to array [r, g, b]
|
|
1170
|
+
* @param hex Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1171
|
+
* @returns Array with 3 elements [r, g, b]
|
|
1172
|
+
*/
|
|
1173
|
+
static color2rgb(hex: string): false | [number, number, number] | '' {
|
|
1174
|
+
if (hex === undefined || hex === null || hex === '' || typeof hex !== 'string') {
|
|
1175
|
+
return false;
|
|
1176
|
+
}
|
|
1177
|
+
if (hex.startsWith('rgba')) {
|
|
1178
|
+
const m = hex.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),\s*([.\d]+)\)/);
|
|
1179
|
+
if (m) {
|
|
1180
|
+
hex =
|
|
1181
|
+
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1182
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1183
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1184
|
+
}
|
|
1185
|
+
} else if (hex.startsWith('rgb')) {
|
|
1186
|
+
const m = hex.match(/rgb?\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
1187
|
+
if (m) {
|
|
1188
|
+
hex =
|
|
1189
|
+
parseInt(m[1], 10).toString(16).padStart(2, '0') +
|
|
1190
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0') +
|
|
1191
|
+
parseInt(m[2], 10).toString(16).padStart(2, '0');
|
|
1192
|
+
}
|
|
1193
|
+
} else if (hex.startsWith('#')) {
|
|
1194
|
+
hex = hex.slice(1);
|
|
1195
|
+
}
|
|
1196
|
+
// convert 3-digit hex to 6-digits.
|
|
1197
|
+
if (hex.length === 3) {
|
|
1198
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
1199
|
+
}
|
|
1200
|
+
if (hex.length !== 6 && hex.length !== 8) {
|
|
1201
|
+
console.warn(`Cannot invert color: ${hex}`);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
return [
|
|
1206
|
+
parseInt(hex.slice(0, 2), 16),
|
|
1207
|
+
parseInt(hex.slice(2, 4), 16),
|
|
1208
|
+
parseInt(hex.slice(4, 6), 16),
|
|
1209
|
+
];
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Big thanks to: https://github.com/antimatter15/rgb-lab
|
|
1213
|
+
/**
|
|
1214
|
+
* Convert RGB to LAB
|
|
1215
|
+
* @param {Array<number>} rgb color in format [r,g,b]
|
|
1216
|
+
* @returns {Array<number>} lab color in format [l,a,b]
|
|
1217
|
+
*/
|
|
1218
|
+
static rgb2lab(rgb: [number, number, number]): [number, number, number] {
|
|
1219
|
+
let r = rgb[0] / 255;
|
|
1220
|
+
let g = rgb[1] / 255;
|
|
1221
|
+
let b = rgb[2] / 255;
|
|
1222
|
+
|
|
1223
|
+
r = r > 0.04045 ? ((r + 0.055) / 1.055) ** 2.4 : r / 12.92;
|
|
1224
|
+
g = g > 0.04045 ? ((g + 0.055) / 1.055) ** 2.4 : g / 12.92;
|
|
1225
|
+
b = b > 0.04045 ? ((b + 0.055) / 1.055) ** 2.4 : b / 12.92;
|
|
1226
|
+
|
|
1227
|
+
let x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
|
|
1228
|
+
let y = r * 0.2126 + g * 0.7152 + b * 0.0722; /* / 1.00000; */
|
|
1229
|
+
let z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
|
|
1230
|
+
|
|
1231
|
+
x = x > 0.008856 ? x ** 0.33333333 : 7.787 * x + 0.137931; // 16 / 116;
|
|
1232
|
+
y = y > 0.008856 ? y ** 0.33333333 : 7.787 * y + 0.137931; // 16 / 116;
|
|
1233
|
+
z = z > 0.008856 ? z ** 0.33333333 : 7.787 * z + 0.137931; // 16 / 116;
|
|
1234
|
+
|
|
1235
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)];
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Calculate the distance between two colors in LAB color space in the range 0-100^2
|
|
1240
|
+
* If distance is less than 1000, the colors are similar
|
|
1241
|
+
* @param color1 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1242
|
+
* @param color2 Color in the format '#rrggbb' or '#rgb' (or without hash) or rgb(r,g,b) or rgba(r,g,b,a)
|
|
1243
|
+
* @returns distance in the range 0-100^2
|
|
1244
|
+
*/
|
|
1245
|
+
static colorDistance(color1: string, color2: string): number {
|
|
1246
|
+
const rgb1 = Utils.color2rgb(color1);
|
|
1247
|
+
const rgb2 = Utils.color2rgb(color2);
|
|
1248
|
+
if (!rgb1 || !rgb2) {
|
|
1249
|
+
return 0;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const lab1 = Utils.rgb2lab(rgb1);
|
|
1253
|
+
const lab2 = Utils.rgb2lab(rgb2);
|
|
1254
|
+
const dltL = lab1[0] - lab2[0];
|
|
1255
|
+
const dltA = lab1[1] - lab2[1];
|
|
1256
|
+
const dltB = lab1[2] - lab2[2];
|
|
1257
|
+
const c1 = Math.sqrt(lab1[1] * lab1[1] + lab1[2] * lab1[2]);
|
|
1258
|
+
const c2 = Math.sqrt(lab2[1] * lab2[1] + lab2[2] * lab2[2]);
|
|
1259
|
+
const dltC = c1 - c2;
|
|
1260
|
+
let dltH = dltA * dltA + dltB * dltB - dltC * dltC;
|
|
1261
|
+
dltH = dltH < 0 ? 0 : Math.sqrt(dltH);
|
|
1262
|
+
const sc = 1.0 + 0.045 * c1;
|
|
1263
|
+
const sh = 1.0 + 0.015 * c1;
|
|
1264
|
+
const dltLKlsl = dltL;
|
|
1265
|
+
const dltCkcsc = dltC / sc;
|
|
1266
|
+
const dltHkhsh = dltH / sh;
|
|
1267
|
+
const i = dltLKlsl * dltLKlsl + dltCkcsc * dltCkcsc + dltHkhsh * dltHkhsh;
|
|
1268
|
+
return i < 0 ? 0 : i;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// https://github.com/lukeed/clsx/blob/master/src/index.js
|
|
1272
|
+
// License
|
|
1273
|
+
// MIT © Luke Edwards
|
|
1274
|
+
/**
|
|
1275
|
+
* @private
|
|
1276
|
+
*/
|
|
1277
|
+
static _toVal(mix: ClassValue): string {
|
|
1278
|
+
let y;
|
|
1279
|
+
let str = '';
|
|
1280
|
+
|
|
1281
|
+
if (typeof mix === 'string' || typeof mix === 'number') {
|
|
1282
|
+
str += mix;
|
|
1283
|
+
} else if (typeof mix === 'object') {
|
|
1284
|
+
if (Array.isArray(mix)) {
|
|
1285
|
+
for (let k = 0; k < mix.length; k++) {
|
|
1286
|
+
if (mix[k]) {
|
|
1287
|
+
y = Utils._toVal(mix[k]);
|
|
1288
|
+
if (y) {
|
|
1289
|
+
str && (str += ' ');
|
|
1290
|
+
str += y;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
} else {
|
|
1295
|
+
for (const k in mix) {
|
|
1296
|
+
if (mix[k]) {
|
|
1297
|
+
str && (str += ' ');
|
|
1298
|
+
str += k;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
return str;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// https://github.com/lukeed/clsx/blob/master/src/index.js
|
|
1308
|
+
// License
|
|
1309
|
+
// MIT © Luke Edwards
|
|
1310
|
+
/**
|
|
1311
|
+
* Convert any object to a string with its values.
|
|
1312
|
+
* @returns {string}
|
|
1313
|
+
*/
|
|
1314
|
+
static clsx(...inputs: ClassValue[]): string {
|
|
1315
|
+
let i = 0;
|
|
1316
|
+
let tmp;
|
|
1317
|
+
let x;
|
|
1318
|
+
let str = '';
|
|
1319
|
+
while (i < inputs.length) {
|
|
1320
|
+
// eslint-disable-next-line prefer-rest-params
|
|
1321
|
+
tmp = inputs[i++];
|
|
1322
|
+
if (tmp) {
|
|
1323
|
+
x = Utils._toVal(tmp);
|
|
1324
|
+
if (x) {
|
|
1325
|
+
str && (str += ' ');
|
|
1326
|
+
str += x;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return str;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Get the current theme name (either from local storage or the browser settings).
|
|
1335
|
+
*/
|
|
1336
|
+
static getThemeName(themeName?: ThemeName | null): ThemeName {
|
|
1337
|
+
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1338
|
+
return (window as any).vendorPrefix;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
themeName = ((window as any)._localStorage || window.localStorage).getItem('App.themeName');
|
|
1342
|
+
if (themeName) {
|
|
1343
|
+
return themeName;
|
|
1344
|
+
}
|
|
1345
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'colored';
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Get the type of theme.
|
|
1350
|
+
*/
|
|
1351
|
+
static getThemeType(themeName?: ThemeName): ThemeType {
|
|
1352
|
+
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@') {
|
|
1353
|
+
return 'light';
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
themeName = themeName || Utils.getThemeName();
|
|
1357
|
+
return themeName === 'dark' || themeName === 'blue' ? 'dark' : 'light';
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/**
|
|
1361
|
+
* Set the theme name and theme type.
|
|
1362
|
+
*/
|
|
1363
|
+
static setThemeName(themeName: ThemeName): void {
|
|
1364
|
+
const vendorPrefix = (window as any).vendorPrefix;
|
|
1365
|
+
if (vendorPrefix && vendorPrefix !== '@@vendorPrefix@@' && vendorPrefix !== 'MV') {
|
|
1366
|
+
return; // ignore
|
|
1367
|
+
}
|
|
1368
|
+
((window as any)._localStorage || window.localStorage).setItem('App.themeName', themeName);
|
|
1369
|
+
((window as any)._localStorage || window.localStorage).setItem(
|
|
1370
|
+
'App.theme',
|
|
1371
|
+
themeName === 'dark' || themeName === 'blue' ? 'dark' : 'light',
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Toggle the theme name between 'dark' and 'colored'.
|
|
1377
|
+
* @returns the new theme name.
|
|
1378
|
+
*/
|
|
1379
|
+
static toggleTheme(themeName?: ThemeName | null): ThemeName {
|
|
1380
|
+
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1381
|
+
return (window as any).vendorPrefix as ThemeName;
|
|
1382
|
+
}
|
|
1383
|
+
themeName = themeName || ((window as any)._localStorage || window.localStorage).getItem('App.themeName') || 'light';
|
|
1384
|
+
|
|
1385
|
+
// dark => blue => colored => light => dark
|
|
1386
|
+
const themes = Utils.getThemeNames();
|
|
1387
|
+
const pos = themeName ? themes.indexOf(themeName) : -1;
|
|
1388
|
+
let newTheme: ThemeName;
|
|
1389
|
+
if (pos !== -1) {
|
|
1390
|
+
newTheme = themes[(pos + 1) % themes.length];
|
|
1391
|
+
} else {
|
|
1392
|
+
newTheme = themes[0];
|
|
1393
|
+
}
|
|
1394
|
+
Utils.setThemeName(newTheme);
|
|
1395
|
+
|
|
1396
|
+
return newTheme;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Get the list of themes
|
|
1401
|
+
* @returns list of possible themes
|
|
1402
|
+
*/
|
|
1403
|
+
static getThemeNames(): ThemeName[] {
|
|
1404
|
+
if ((window as any).vendorPrefix && (window as any).vendorPrefix !== '@@vendorPrefix@@' && (window as any).vendorPrefix !== 'MV') {
|
|
1405
|
+
return [(window as any).vendorPrefix as ThemeName];
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return ['light', 'dark', 'blue', 'colored'];
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Parse a query string into its parts.
|
|
1413
|
+
*/
|
|
1414
|
+
static parseQuery(query: string): Record<string, string | number | boolean> {
|
|
1415
|
+
query = (query || '').toString().replace(/^\?/, '');
|
|
1416
|
+
const result: Record<string, string | number | boolean> = {};
|
|
1417
|
+
query.split('&').forEach(part => {
|
|
1418
|
+
part = part.trim();
|
|
1419
|
+
if (part) {
|
|
1420
|
+
const parts = part.split('=');
|
|
1421
|
+
const attr = decodeURIComponent(parts[0]).trim();
|
|
1422
|
+
if (parts.length > 1) {
|
|
1423
|
+
const value = decodeURIComponent(parts[1]);
|
|
1424
|
+
if (value === 'true') {
|
|
1425
|
+
result[attr] = true;
|
|
1426
|
+
} else if (value === 'false') {
|
|
1427
|
+
result[attr] = false;
|
|
1428
|
+
} else {
|
|
1429
|
+
const f = parseFloat(value);
|
|
1430
|
+
if (f.toString() === value) {
|
|
1431
|
+
result[attr] = f;
|
|
1432
|
+
} else {
|
|
1433
|
+
result[attr] = value;
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
} else {
|
|
1437
|
+
result[attr] = true;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
return result;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Returns parent ID.
|
|
1446
|
+
* @returns parent ID or null if no parent
|
|
1447
|
+
*/
|
|
1448
|
+
static getParentId(id: string): string | null {
|
|
1449
|
+
const p = (id || '').toString().split('.');
|
|
1450
|
+
if (p.length > 1) {
|
|
1451
|
+
p.pop();
|
|
1452
|
+
return p.join('.');
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
static formatDate(dateObj: Date, dateFormat: string): string {
|
|
1459
|
+
// format could be DD.MM.YYYY, YYYY.MM.DD or MM/DD/YYYY
|
|
1460
|
+
|
|
1461
|
+
if (!dateObj) {
|
|
1462
|
+
return '';
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
let text;
|
|
1466
|
+
const mm = (dateObj.getMonth() + 1).toString().padStart(2, '0');
|
|
1467
|
+
const dd = dateObj.getDate().toString().padStart(2, '0');
|
|
1468
|
+
|
|
1469
|
+
if (dateFormat === 'MM/DD/YYYY') {
|
|
1470
|
+
text = `${mm}/${dd}/${dateObj.getFullYear()}`;
|
|
1471
|
+
} else {
|
|
1472
|
+
text = `${dateObj.getFullYear()}-${mm}-${dd}`;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// time
|
|
1476
|
+
text += ` ${dateObj.getHours().toString().padStart(2, '0')}:${dateObj.getMinutes().toString().padStart(2, '0')}:${dateObj.getSeconds().toString().padStart(2, '0')}.${dateObj.getMilliseconds().toString().padStart(3, '0')}`;
|
|
1477
|
+
|
|
1478
|
+
return text;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/*
|
|
1482
|
+
Format seconds to string like 'h:mm:ss' or 'd.hh:mm:ss'
|
|
1483
|
+
*/
|
|
1484
|
+
static formatTime(seconds: number): string {
|
|
1485
|
+
if (seconds) {
|
|
1486
|
+
seconds = Math.round(seconds);
|
|
1487
|
+
const d = Math.floor(seconds / (3600 * 24));
|
|
1488
|
+
const h = Math.floor((seconds % (3600 * 24)) / 3600);
|
|
1489
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
1490
|
+
const s = seconds % 60;
|
|
1491
|
+
if (d) {
|
|
1492
|
+
return `${d}.${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1493
|
+
}
|
|
1494
|
+
if (h) {
|
|
1495
|
+
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
return `0:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
|
1499
|
+
}
|
|
1500
|
+
return '0:00:00';
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
static MDtext2link(text: string): string {
|
|
1504
|
+
const m = text.match(/\d+\.\)\s/);
|
|
1505
|
+
if (m) {
|
|
1506
|
+
text = text.replace(m[0], m[0].replace(/\s/, ' '));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
return text
|
|
1510
|
+
.replace(/[^a-zA-Zа-яА-Я0-9]/g, '')
|
|
1511
|
+
.trim()
|
|
1512
|
+
.replace(/\s/g, '')
|
|
1513
|
+
.toLowerCase();
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/*
|
|
1517
|
+
Open url link in the new target window
|
|
1518
|
+
*/
|
|
1519
|
+
static openLink(url: string, target?: string): void {
|
|
1520
|
+
// replace IPv6 Address with [ipv6]:port
|
|
1521
|
+
url = url.replace(/\/\/([0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*:[0-9a-f]*)(:\d+)?\//i, '//[$1]$2/');
|
|
1522
|
+
|
|
1523
|
+
if (target === 'this') {
|
|
1524
|
+
window.location.href = url;
|
|
1525
|
+
} else {
|
|
1526
|
+
window.open(url, target || '_blank');
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
static MDgetTitle(text: string): string {
|
|
1531
|
+
const result = Utils.MDextractHeader(text);
|
|
1532
|
+
const header = result.header;
|
|
1533
|
+
let body = result.body;
|
|
1534
|
+
if (!header.title) {
|
|
1535
|
+
// remove {docsify-bla}
|
|
1536
|
+
body = body.replace(/{[^}]*}/g, '');
|
|
1537
|
+
body = body.trim();
|
|
1538
|
+
const lines = body.replace(/\r/g, '').split('\n');
|
|
1539
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1540
|
+
if (lines[i].startsWith('# ')) {
|
|
1541
|
+
return lines[i].substring(2).trim();
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
return '';
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
return header.title?.toString() || '';
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
static MDextractHeader(text: string): { header: Record<string, string | boolean | number>; body: string } {
|
|
1551
|
+
const attrs: Record<string, string | boolean | number> = {};
|
|
1552
|
+
if (text.substring(0, 3) === '---') {
|
|
1553
|
+
const pos = text.substring(3).indexOf('\n---');
|
|
1554
|
+
if (pos !== -1) {
|
|
1555
|
+
const _header = text.substring(3, pos + 3);
|
|
1556
|
+
const lines = _header.replace(/\r/g, '').split('\n');
|
|
1557
|
+
lines.forEach(line => {
|
|
1558
|
+
if (!line.trim()) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const pos_ = line.indexOf(':');
|
|
1562
|
+
if (pos_ !== -1) {
|
|
1563
|
+
const attr = line.substring(0, pos_).trim();
|
|
1564
|
+
let value = line.substring(pos_ + 1).trim();
|
|
1565
|
+
value = value.replace(/^['"]|['"]$/g, '');
|
|
1566
|
+
if (value === 'true') {
|
|
1567
|
+
attrs[attr] = true;
|
|
1568
|
+
} else if (value === 'false') {
|
|
1569
|
+
attrs[attr] = false;
|
|
1570
|
+
} else if (parseFloat(value).toString() === attrs[attr]) {
|
|
1571
|
+
attrs[attr] = parseFloat(value);
|
|
1572
|
+
} else {
|
|
1573
|
+
attrs[attr] = value;
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
attrs[line.trim()] = true;
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
text = text.substring(pos + 7);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return { header: attrs, body: text };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
static MDremoveDocsify(text: string): string {
|
|
1586
|
+
const m = text.match(/{docsify-[^}]*}/g);
|
|
1587
|
+
if (m) {
|
|
1588
|
+
m.forEach(doc => (text = text.replace(doc, '')));
|
|
1589
|
+
}
|
|
1590
|
+
return text;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* Generate the file for download from JSON object.
|
|
1595
|
+
*/
|
|
1596
|
+
static generateFile(
|
|
1597
|
+
fileName: string,
|
|
1598
|
+
/** json file data */
|
|
1599
|
+
json: Record<string, any>,
|
|
1600
|
+
): void {
|
|
1601
|
+
const el = document.createElement('a');
|
|
1602
|
+
el.setAttribute(
|
|
1603
|
+
'href',
|
|
1604
|
+
`data:application/json;charset=utf-8,${encodeURIComponent(JSON.stringify(json, null, 2))}`,
|
|
1605
|
+
);
|
|
1606
|
+
el.setAttribute('download', fileName);
|
|
1607
|
+
|
|
1608
|
+
el.style.display = 'none';
|
|
1609
|
+
document.body.appendChild(el);
|
|
1610
|
+
|
|
1611
|
+
el.click();
|
|
1612
|
+
|
|
1613
|
+
document.body.removeChild(el);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Convert quality code into text
|
|
1618
|
+
* @returns lines that decode quality
|
|
1619
|
+
*/
|
|
1620
|
+
static quality2text(quality: ioBroker.STATE_QUALITY[keyof ioBroker.STATE_QUALITY]): string[] {
|
|
1621
|
+
// eslint-disable-next-line no-bitwise
|
|
1622
|
+
const custom = quality & 0xFFFF0000;
|
|
1623
|
+
const text: string = QUALITY_BITS[quality];
|
|
1624
|
+
let result;
|
|
1625
|
+
if (text) {
|
|
1626
|
+
result = [text];
|
|
1627
|
+
// eslint-disable-next-line no-bitwise
|
|
1628
|
+
} else if (quality & 0x01) {
|
|
1629
|
+
// eslint-disable-next-line no-bitwise
|
|
1630
|
+
result = [QUALITY_BITS[0x01], `0x${(quality & (0xFFFF & ~1)).toString(16)}`];
|
|
1631
|
+
// eslint-disable-next-line no-bitwise
|
|
1632
|
+
} else if (quality & 0x02) {
|
|
1633
|
+
// eslint-disable-next-line no-bitwise
|
|
1634
|
+
result = [QUALITY_BITS[0x02], `0x${(quality & (0xFFFF & ~2)).toString(16)}`];
|
|
1635
|
+
} else {
|
|
1636
|
+
result = [`0x${quality.toString(16)}`];
|
|
1637
|
+
}
|
|
1638
|
+
if (custom) {
|
|
1639
|
+
// eslint-disable-next-line no-bitwise
|
|
1640
|
+
result.push(`0x${(custom >> 16).toString(16).toUpperCase()}`);
|
|
1641
|
+
}
|
|
1642
|
+
return result;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Deep copy object
|
|
1647
|
+
*/
|
|
1648
|
+
static clone(object: Record<string, any>): Record<string, any> {
|
|
1649
|
+
return JSON.parse(JSON.stringify(object));
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Get states of object
|
|
1654
|
+
* @returns states as an object in form {"value1": "label1", "value2": "label2"} or null
|
|
1655
|
+
*/
|
|
1656
|
+
static getStates(obj: ioBroker.StateObject | null | undefined): Record<string, string> | null {
|
|
1657
|
+
const states: Record<string, string> | string[] | string | undefined | null = obj?.common?.states;
|
|
1658
|
+
let result: Record<string, string> | null | undefined;
|
|
1659
|
+
if (states) {
|
|
1660
|
+
if (typeof states === 'string' && states[0] === '{') {
|
|
1661
|
+
try {
|
|
1662
|
+
result = JSON.parse(states) as Record<string, string>;
|
|
1663
|
+
} catch (ex) {
|
|
1664
|
+
console.error(`Cannot parse states: ${states}`);
|
|
1665
|
+
result = null;
|
|
1666
|
+
}
|
|
1667
|
+
} else if (typeof states === 'string') {
|
|
1668
|
+
// if old format val1:text1;val2:text2
|
|
1669
|
+
const parts = states.split(';');
|
|
1670
|
+
result = {};
|
|
1671
|
+
for (let p = 0; p < parts.length; p++) {
|
|
1672
|
+
const s = parts[p].split(':');
|
|
1673
|
+
result[s[0]] = s[1];
|
|
1674
|
+
}
|
|
1675
|
+
} else if (Array.isArray(states)) {
|
|
1676
|
+
result = {};
|
|
1677
|
+
if (obj?.common.type === 'number') {
|
|
1678
|
+
states.forEach((value, key) => ((result as Record<string, string>)[key] = value));
|
|
1679
|
+
} else if (obj?.common.type === 'string') {
|
|
1680
|
+
states.forEach(value => ((result as Record<string, string>)[value] = value));
|
|
1681
|
+
} else if (obj?.common.type === 'boolean') {
|
|
1682
|
+
result.false = states[0];
|
|
1683
|
+
result.true = states[1];
|
|
1684
|
+
}
|
|
1685
|
+
} else if (typeof states === 'object') {
|
|
1686
|
+
result = states as Record<string, string>;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return result || null;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Get svg file as text
|
|
1695
|
+
* @param url URL of SVG file
|
|
1696
|
+
* @returns Promise with "data:image..."
|
|
1697
|
+
*/
|
|
1698
|
+
static async getSvg(url: string): Promise<string> {
|
|
1699
|
+
const response = await fetch(url);
|
|
1700
|
+
const blob = await response.blob();
|
|
1701
|
+
return new Promise(resolve => {
|
|
1702
|
+
const reader = new FileReader();
|
|
1703
|
+
// eslint-disable-next-line func-names
|
|
1704
|
+
reader.onload = function () {
|
|
1705
|
+
resolve(this.result?.toString() || '');
|
|
1706
|
+
};
|
|
1707
|
+
reader.readAsDataURL(blob);
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/**
|
|
1712
|
+
* Detect file extension by its content
|
|
1713
|
+
* @returns Detected extension, like 'jpg'
|
|
1714
|
+
*/
|
|
1715
|
+
static detectMimeType(
|
|
1716
|
+
/** Base64 encoded binary file */
|
|
1717
|
+
base64: string,
|
|
1718
|
+
): string | null {
|
|
1719
|
+
const signature = Object.keys(SIGNATURES).find(s => base64.startsWith(s));
|
|
1720
|
+
return signature ? SIGNATURES[signature] : null;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Check if configured repository is the stable repository
|
|
1725
|
+
*/
|
|
1726
|
+
static isStableRepository(
|
|
1727
|
+
/** current configured repository or multi repository */
|
|
1728
|
+
activeRepo: string | string[],
|
|
1729
|
+
): boolean {
|
|
1730
|
+
return !!((
|
|
1731
|
+
typeof activeRepo === 'string' &&
|
|
1732
|
+
activeRepo.toLowerCase().startsWith('stable')
|
|
1733
|
+
)
|
|
1734
|
+
||
|
|
1735
|
+
(
|
|
1736
|
+
activeRepo &&
|
|
1737
|
+
typeof activeRepo !== 'string' &&
|
|
1738
|
+
activeRepo.find(r => r.toLowerCase().startsWith('stable'))
|
|
1739
|
+
));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/**
|
|
1743
|
+
* Check if a given string is an integer
|
|
1744
|
+
*/
|
|
1745
|
+
static isStringInteger(str: string | number): boolean {
|
|
1746
|
+
if (typeof str === 'number') {
|
|
1747
|
+
return Math.round(str) === str;
|
|
1748
|
+
}
|
|
1749
|
+
return parseInt(str, 10).toString() === str;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* Check if the date is valid
|
|
1754
|
+
*/
|
|
1755
|
+
static isValidDate(date: any): boolean {
|
|
1756
|
+
// eslint-disable-next-line no-restricted-globals
|
|
1757
|
+
return date instanceof Date && !isNaN(date as any as number);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
static getStyle(
|
|
1761
|
+
theme: IobTheme,
|
|
1762
|
+
...args: (((_theme: IobTheme) => Record<string, any>) | undefined | Record<string, any>)[]
|
|
1763
|
+
): Record<string, any> {
|
|
1764
|
+
const result: Record<string, any> = {};
|
|
1765
|
+
|
|
1766
|
+
for (let a = 0; a < args.length; a++) {
|
|
1767
|
+
if (typeof args[a] === 'function') {
|
|
1768
|
+
Object.assign(result, (args[a] as (_theme: IobTheme) => Record<string, any>)(theme));
|
|
1769
|
+
} else if (args[a] && typeof args[a] === 'object') {
|
|
1770
|
+
Object.keys(args[a] as Record<string, any>).forEach((attr: string) => {
|
|
1771
|
+
if (typeof (args[a] as Record<string, any>)[attr] === 'function') {
|
|
1772
|
+
result[attr] = ((args[a] as Record<string, any>)[attr] as (_theme: IobTheme) => Record<string, any>)(theme);
|
|
1773
|
+
} else if (typeof (args[a] as Record<string, any>)[attr] === 'object') {
|
|
1774
|
+
const obj = (args[a] as Record<string, any>)[attr];
|
|
1775
|
+
result[attr] = {};
|
|
1776
|
+
Object.keys(obj).forEach((attr1: string) => {
|
|
1777
|
+
if (typeof obj[attr1] === 'function') {
|
|
1778
|
+
result[attr][attr1] = obj(theme);
|
|
1779
|
+
} else if (obj[attr1] || obj[attr1] === 0) {
|
|
1780
|
+
result[attr][attr1] = obj[attr1];
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
} else if ((args[a] as Record<string, any>)[attr] || (args[a] as Record<string, any>)[attr] === 0) {
|
|
1784
|
+
result[attr] = (args[a] as Record<string, any>)[attr];
|
|
1785
|
+
}
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
return result;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
export default Utils;
|