@iobroker/dm-gui-components 10.0.22 → 10.1.0

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.
Files changed (46) hide show
  1. package/README.md +13 -0
  2. package/build/AnimatedIcons.d.ts +37 -0
  3. package/build/AnimatedIcons.js +108 -0
  4. package/build/AnimatedIcons.js.map +1 -0
  5. package/build/Communication.d.ts +7 -0
  6. package/build/Communication.js +43 -11
  7. package/build/Communication.js.map +1 -1
  8. package/build/DeviceCard.d.ts +25 -13
  9. package/build/DeviceCard.js +177 -282
  10. package/build/DeviceCard.js.map +1 -1
  11. package/build/DeviceControl.js +38 -26
  12. package/build/DeviceControl.js.map +1 -1
  13. package/build/DeviceImageUpload.d.ts +5 -1
  14. package/build/DeviceImageUpload.js +79 -62
  15. package/build/DeviceImageUpload.js.map +1 -1
  16. package/build/DeviceList.d.ts +8 -0
  17. package/build/DeviceList.js +74 -59
  18. package/build/DeviceList.js.map +1 -1
  19. package/build/DeviceStatus.d.ts +20 -4
  20. package/build/DeviceStatus.js +146 -191
  21. package/build/DeviceStatus.js.map +1 -1
  22. package/build/StatusIndicator.js +9 -37
  23. package/build/StatusIndicator.js.map +1 -1
  24. package/build/StatusPill.d.ts +18 -0
  25. package/build/StatusPill.js +30 -0
  26. package/build/StatusPill.js.map +1 -0
  27. package/build/Switch.d.ts +3 -0
  28. package/build/Switch.js +36 -26
  29. package/build/Switch.js.map +1 -1
  30. package/build/Utils.js +92 -60
  31. package/build/Utils.js.map +1 -1
  32. package/build/i18n/de.json +3 -0
  33. package/build/i18n/en.json +3 -0
  34. package/build/i18n/es.json +3 -0
  35. package/build/i18n/fr.json +3 -0
  36. package/build/i18n/it.json +3 -0
  37. package/build/i18n/nl.json +3 -0
  38. package/build/i18n/pl.json +3 -0
  39. package/build/i18n/pt.json +3 -0
  40. package/build/i18n/ru.json +3 -0
  41. package/build/i18n/uk.json +3 -0
  42. package/build/i18n/zh-cn.json +3 -0
  43. package/build/styles.d.ts +17 -0
  44. package/build/styles.js +226 -0
  45. package/build/styles.js.map +1 -0
  46. package/package.json +10 -10
@@ -1,14 +1,16 @@
1
- import React, { PureComponent } from 'react';
2
- import { Close as CloseIcon, VideogameAsset as ControlIcon, MoreVert as MoreVertIcon, ExpandMore, } from '@mui/icons-material';
3
- import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Fab, IconButton, Paper, Skeleton, Tooltip, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
4
- import { DeviceTypeIcon, Utils, Icon, } from '@iobroker/gui-components';
1
+ import React, { PureComponent, useState } from 'react';
2
+ import { Close as CloseIcon, VideogameAsset as ControlIcon, MoreVert as MoreVertIcon } from '@mui/icons-material';
3
+ import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Skeleton, Tooltip, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
4
+ import { DeviceTypeIcon, Utils, Icon, IconCopyToClipboard, IconFolder, } from '@iobroker/gui-components';
5
5
  import DeviceActionButton from './DeviceActionButton';
6
6
  import DeviceControlComponent from './DeviceControl';
7
7
  import DeviceImageUpload from './DeviceImageUpload';
8
- import DeviceStatusComponent from './DeviceStatus';
8
+ import DeviceStatusComponent, { DeviceLed } from './DeviceStatus';
9
9
  import JsonConfig from './JsonConfig';
10
10
  import { StatusIndicators } from './StatusIndicator';
11
+ import Switch from './Switch';
11
12
  import { getText, getTranslation } from './Utils';
13
+ import { ensureDmStyles, getThemeVars } from './styles';
12
14
  /** Reserved action names (this is copied from https://github.com/ioBroker/dm-utils/blob/main/src/types/base.ts as we can only have type references to dm-utils) */
13
15
  const ACTIONS = {
14
16
  /** This action will be called when the user clicks on the connection icon */
@@ -25,10 +27,10 @@ const ACTIONS = {
25
27
  * the same size, so that neither the layout nor the length of the scrollbar changes.
26
28
  */
27
29
  export const CARD_WIDTH = 300;
28
- export const CARD_MIN_HEIGHT = 280;
29
- export const CARD_MARGIN = 10;
30
- export const SMALL_CARD_WIDTH = 200;
31
- export const SMALL_CARD_MIN_HEIGHT = 200;
30
+ export const CARD_MIN_HEIGHT = 250;
31
+ export const CARD_MARGIN = 8;
32
+ export const SMALL_CARD_WIDTH = 210;
33
+ export const SMALL_CARD_MIN_HEIGHT = 190;
32
34
  export const SMALL_CARD_MARGIN = 5;
33
35
  const cardSize = { width: CARD_WIDTH, minHeight: CARD_MIN_HEIGHT, margin: CARD_MARGIN };
34
36
  const smallCardSize = {
@@ -38,6 +40,7 @@ const smallCardSize = {
38
40
  };
39
41
  /** A card that sits in a container which already has the footprint of a card */
40
42
  const filledCardSize = { width: '100%', margin: 0 };
43
+ const tooltipSlotProps = { popper: { sx: { pointerEvents: 'none' } } };
41
44
  /**
42
45
  * Icons read from the file storage of the adapter, keyed by `<instance>/<file>`.
43
46
  *
@@ -45,69 +48,44 @@ const filledCardSize = { width: '100%', margin: 0 };
45
48
  * would repeat the `readFile` round trip for an icon that is very often not there at all.
46
49
  */
47
50
  const fileIconCache = new Map();
48
- const styles = {
49
- cardStyle: (theme) => ({
50
- overflow: 'hidden',
51
- display: 'flex',
52
- flexDirection: 'column',
53
- backgroundColor: theme.palette.mode === 'dark' ? '#0b0b0b' : '#d5d5d5',
54
- }),
55
- headerStyle: {
56
- display: 'flex',
57
- alignItems: 'center',
58
- gap: 8,
59
- paddingLeft: 8,
60
- paddingRight: 8,
61
- position: 'relative',
62
- minHeight: 60,
63
- color: '#000',
64
- },
65
- imgAreaStyle: {
66
- height: 45,
67
- width: 45,
68
- justifyContent: 'center',
69
- display: 'flex',
70
- alignItems: 'center',
71
- },
72
- imgStyle: {
73
- zIndex: 2,
74
- maxWidth: '100%',
75
- maxHeight: '100%',
76
- color: '#FFF',
77
- },
78
- titleStyle: {
79
- fontSize: 16,
80
- fontWeight: 'bold',
81
- // whiteSpace: 'nowrap',
82
- overflow: 'hidden',
83
- textOverflow: 'ellipsis',
84
- },
85
- detailsButtonStyle: {
86
- right: 20,
87
- bottom: -20,
88
- position: 'absolute',
89
- },
90
- bodyStyle: {
91
- flex: 1,
92
- display: 'flex',
93
- flexDirection: 'column',
94
- },
95
- deviceInfoStyle: {
96
- padding: '20px 16px 0 16px',
97
- },
98
- statusStyle: {
99
- padding: '15px 25px 0 15px',
100
- // The line may grow if the device provides custom indicators, so only the minimum is fixed
101
- minHeight: 41,
102
- },
103
- };
104
- function NoImageIcon(props) {
105
- return (React.createElement("svg", { viewBox: "0 0 24 24", width: "24", height: "24", style: props.style, className: props.className },
106
- React.createElement("path", { fill: "currentColor", d: "M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z" })));
51
+ /**
52
+ * True if the icon is a picture (a photo of the device, an uploaded image). A picture keeps its own
53
+ * colors and sits on a neutral tile; a device type or a symbol takes the color of the device.
54
+ */
55
+ function isPicture(src) {
56
+ if (src.startsWith('data:image/svg')) {
57
+ return false;
58
+ }
59
+ return src.startsWith('data:image') || src.includes('/') || /\.(png|jpe?g|gif|webp|ico|bmp)$/i.test(src);
60
+ }
61
+ /** The status entries of a device as a list */
62
+ function getStatusList(device) {
63
+ if (!device.status) {
64
+ return [];
65
+ }
66
+ return Array.isArray(device.status) ? device.status : [device.status];
67
+ }
68
+ /** A group of controls in the control dialog: the folder opens with the group */
69
+ function ControlGroup(props) {
70
+ const [expanded, setExpanded] = useState(false);
71
+ const { control } = props;
72
+ return (React.createElement(Accordion, { expanded: expanded, onChange: (_e, isExpanded) => setExpanded(isExpanded), disableGutters: true },
73
+ React.createElement(AccordionSummary, { expandIcon: React.createElement(IconFolder, { open: expanded }),
74
+ // The folder shows the state itself, it must not be turned around like the arrow
75
+ sx: { '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { transform: 'none' } } },
76
+ React.createElement(Typography, { component: "span", style: { display: 'flex', alignItems: 'center', gap: 8, color: control.color } },
77
+ control.icon ? React.createElement(Icon, { src: control.icon }) : null,
78
+ getTranslation(control.label))),
79
+ React.createElement(AccordionDetails, { style: { display: 'flex', flexDirection: 'column' } }, props.children)));
107
80
  }
108
81
  /**
109
82
  * Device Card Component.
110
83
  *
84
+ * The card is laid out like the type plate of a device: its symbol (in the color of the device, with
85
+ * an LED for the connection) and its name at the top, the status line below, then the facts and at
86
+ * the bottom the actions. The status values are neutral while all is well and tinted when they need
87
+ * attention, so that in a long list the devices with a problem stand out.
88
+ *
111
89
  * A `PureComponent`: the list re-renders on every loading step, on every filter change and on every
112
90
  * resolved device value. Without the shallow property comparison all cards would be re-rendered
113
91
  * every time, so all properties the list passes down are kept stable there.
@@ -117,6 +95,7 @@ export default class DeviceCard extends PureComponent {
117
95
  mounted = false;
118
96
  constructor(props) {
119
97
  super(props);
98
+ ensureDmStyles();
120
99
  const cacheKey = DeviceCard.iconCacheKey(props);
121
100
  this.state = {
122
101
  open: false,
@@ -161,17 +140,6 @@ export default class DeviceCard extends PureComponent {
161
140
  return;
162
141
  }
163
142
  this.setState({ localIcon });
164
- // const response = await fetch(url);
165
- // if (response.ok) {
166
- // const blob = await response.blob();
167
- // const reader = new FileReader();
168
- // reader.onloadend = () => {
169
- // setIcon(reader.result);
170
- // };
171
- // reader.readAsDataURL(blob);
172
- // } else {
173
- // throw new Error('Response not ok');
174
- // }
175
143
  }
176
144
  catch {
177
145
  fileIconCache.set(cacheKey, '');
@@ -207,15 +175,13 @@ export default class DeviceCard extends PureComponent {
207
175
  this.setState({ details, data: details?.data || {} });
208
176
  }
209
177
  /**
210
- * Copy the device ID to the clipboard
178
+ * Copy the device ID to the clipboard. The copy symbol shows a check mark as confirmation.
211
179
  */
212
180
  copyToClipboard = () => {
213
181
  const textToCopy = this.props.fields.identifier;
214
- if (!textToCopy) {
215
- return;
182
+ if (textToCopy) {
183
+ Utils.copyToClipboard(textToCopy);
216
184
  }
217
- Utils.copyToClipboard(textToCopy);
218
- alert(`${getTranslation('copied')} ${textToCopy} ${getTranslation('toClipboard')}!`);
219
185
  };
220
186
  renderDialog() {
221
187
  if (!this.state.open || !this.state.details) {
@@ -233,12 +199,7 @@ export default class DeviceCard extends PureComponent {
233
199
  .map(control => {
234
200
  if (control.type === 'group') {
235
201
  const children = allControls.filter(ctrl => ctrl.group === control.id);
236
- return (React.createElement(Accordion, { key: control.id },
237
- React.createElement(AccordionSummary, { expandIcon: React.createElement(ExpandMore, null) },
238
- React.createElement(Typography, { component: "span", style: { display: 'flex', gap: 8, color: control.color } },
239
- control.icon ? React.createElement(Icon, { src: control.icon }) : null,
240
- getTranslation(control.label))),
241
- React.createElement(AccordionDetails, { style: { display: 'flex', flexDirection: 'column' } }, this.renderControlItems(children, allControls, colors, control.id))));
202
+ return (React.createElement(ControlGroup, { key: control.id, control: control }, this.renderControlItems(children, allControls, colors, control.id)));
242
203
  }
243
204
  return (React.createElement(DeviceControlComponent, { disabled: false, key: control.id, control: control, socket: this.props.socket, colors: colors, deviceId: this.props.device.id, controlHandler: this.props.controlHandler, controlStateHandler: this.props.controlStateHandler }));
244
205
  });
@@ -268,8 +229,8 @@ export default class DeviceCard extends PureComponent {
268
229
  }
269
230
  if (this.props.device.controls?.length) {
270
231
  // place a button and show a control dialog
271
- return (React.createElement(Fab, { size: "small", style: { width: 32, height: 32, minHeight: 32 }, disabled: !this.props.alive, onClick: () => this.setState({ showControlDialog: true }) },
272
- React.createElement(ControlIcon, null)));
232
+ return (React.createElement(IconButton, { size: "small", color: "primary", disabled: !this.props.alive, onClick: () => this.setState({ showControlDialog: true }) },
233
+ React.createElement(ControlIcon, { className: "dm-ani dm-ani-tick" })));
273
234
  }
274
235
  return null;
275
236
  }
@@ -313,7 +274,7 @@ export default class DeviceCard extends PureComponent {
313
274
  };
314
275
  };
315
276
  /** Custom indicators and the actions that requested to be shown in the status line */
316
- renderIndicators(small) {
277
+ renderIndicators() {
317
278
  const reserved = [ACTIONS.STATUS, ACTIONS.ENABLE_DISABLE, ACTIONS.UPDATE, ACTIONS.BATTERY];
318
279
  const statusActions = this.props.device.actions?.filter(a => a.placement === 'status' && !reserved.includes(a.id)) || [];
319
280
  // An indicator switched off by the user is not shown at all. An action referenced by it stays
@@ -324,7 +285,9 @@ export default class DeviceCard extends PureComponent {
324
285
  if (!indicators?.length && !statusActions.length) {
325
286
  return null;
326
287
  }
327
- return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler, disabled: !this.props.alive, resolveAction: this.resolveIndicatorAction, style: { marginTop: small ? 2 : 4 } }, statusActions.map(action => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: action.id, deviceId: this.props.device.id, action: action, deviceHandler: this.props.deviceHandler })))));
288
+ return (React.createElement(StatusIndicators, { indicators: indicators, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler, disabled: !this.props.alive, resolveAction: this.resolveIndicatorAction,
289
+ // The indicators flow in the status line together with the built-in values
290
+ style: { display: 'contents' } }, statusActions.map(action => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: action.id, deviceId: this.props.device.id, action: action, deviceHandler: this.props.deviceHandler })))));
328
291
  }
329
292
  renderActions() {
330
293
  const statusActionIds = this.getStatusActionIds();
@@ -333,67 +296,8 @@ export default class DeviceCard extends PureComponent {
333
296
  ? actions.map(a => (React.createElement(DeviceActionButton, { disabled: !this.props.alive, key: a.id, deviceId: this.props.device.id, action: a, deviceHandler: this.props.deviceHandler })))
334
297
  : null;
335
298
  }
336
- renderSmall() {
337
- const status = !this.props.device.status
338
- ? []
339
- : Array.isArray(this.props.device.status)
340
- ? this.props.device.status
341
- : [this.props.device.status];
342
- const iconSrc = this.state.localIcon || this.props.fields.icon;
343
- const icon = iconSrc ? (React.createElement(DeviceTypeIcon, { src: iconSrc, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
344
- const headerStyle = this.getCardHeaderStyle(this.props.theme);
345
- const title = this.state.details?.data?.name || this.props.device.name || '';
346
- return (React.createElement(Paper, { style: this.props.fillContainer ? filledCardSize : smallCardSize, sx: styles.cardStyle },
347
- React.createElement(Box, { sx: headerStyle, style: { ...styles.headerStyle, minHeight: 48 } },
348
- React.createElement("div", { style: { ...styles.imgAreaStyle, height: 32, width: 32 } },
349
- this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
350
- if (imageData) {
351
- this.setState({ localIcon: imageData });
352
- }
353
- }, socket: this.props.socket })) : null,
354
- icon),
355
- React.createElement(Box, { style: { ...styles.titleStyle, fontSize: 14 }, title: title.length > 15 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.props.fields.name),
356
- this.props.fields.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
357
- if (!this.state.open) {
358
- this.loadDetails().catch(console.error);
359
- this.setState({ open: true });
360
- }
361
- }, color: "primary" },
362
- React.createElement(MoreVertIcon, null))) : null),
363
- React.createElement("div", { style: { ...styles.statusStyle, height: 'auto', padding: '8px 15px 0 15px' } },
364
- status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, enabled: this.props.fields.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler }))),
365
- this.renderIndicators(true)),
366
- React.createElement("div", { style: styles.bodyStyle },
367
- React.createElement(Typography, { variant: "body2", style: { ...styles.deviceInfoStyle, padding: '10px 10px 0 10px' } },
368
- this.props.fields.identifier ? (React.createElement("div", { onClick: this.copyToClipboard, style: { textOverflow: 'ellipsis', overflow: 'hidden' } },
369
- React.createElement("b", null,
370
- getText(this.props.identifierLabel),
371
- ":"),
372
- React.createElement("span", { style: { marginLeft: 4 } }, this.props.fields.identifier))) : null,
373
- this.props.fields.manufacturer ? (React.createElement(Tooltip, { title: getTranslation('manufacturer'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
374
- React.createElement("div", null, this.props.fields.manufacturer))) : null,
375
- this.props.fields.model ? (React.createElement(Tooltip, { title: getTranslation('model'), slotProps: { popper: { sx: { pointerEvents: 'none' } } } },
376
- React.createElement("div", null, this.props.fields.model))) : null),
377
- this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 10px 10px' } },
378
- React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
379
- /* ignore */
380
- }, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
381
- !!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
382
- marginTop: 'auto',
383
- display: 'flex',
384
- gap: 4,
385
- paddingBottom: 5,
386
- minHeight: 34,
387
- paddingLeft: 8,
388
- paddingRight: 8,
389
- } },
390
- this.renderActions(),
391
- React.createElement("div", { style: { flexGrow: 1 } }),
392
- this.renderControls()))),
393
- this.renderDialog(),
394
- this.renderControlDialog()));
395
- }
396
- getCardHeaderStyle(theme, maxWidth) {
299
+ /** Color of the device symbol and of the glyph on it */
300
+ getAccent(theme) {
397
301
  const backgroundColor = this.props.fields.backgroundColor === 'primary'
398
302
  ? theme.palette.primary.main
399
303
  : this.props.fields.backgroundColor === 'secondary'
@@ -424,142 +328,133 @@ export default class DeviceCard extends PureComponent {
424
328
  color = Utils.invertColor(backgroundColor, true);
425
329
  }
426
330
  }
427
- return {
428
- backgroundColor,
429
- color,
430
- maxWidth,
431
- };
331
+ return { background: backgroundColor, color };
432
332
  }
433
- renderBig() {
434
- const status = !this.props.device.status
435
- ? []
436
- : Array.isArray(this.props.device.status)
437
- ? this.props.device.status
438
- : [this.props.device.status];
333
+ /** The device symbol with the LED and, if allowed, the image upload */
334
+ renderAvatar(name, small) {
439
335
  const iconSrc = this.state.localIcon || this.props.fields.icon;
440
- const icon = iconSrc ? (React.createElement(DeviceTypeIcon, { src: iconSrc, style: styles.imgStyle })) : (React.createElement(NoImageIcon, { style: styles.imgStyle }));
441
- const headerStyle = this.getCardHeaderStyle(this.props.theme);
442
- const title = this.state.details?.data?.name || this.props.device.name || '';
443
- return (React.createElement(Paper, { style: this.props.fillContainer ? filledCardSize : cardSize, sx: styles.cardStyle },
444
- React.createElement(Box, { sx: headerStyle, style: styles.headerStyle },
445
- React.createElement("div", { style: styles.imgAreaStyle },
446
- this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
447
- if (imageData) {
448
- this.setState({ localIcon: imageData });
449
- }
450
- }, socket: this.props.socket })) : null,
451
- icon),
452
- React.createElement(Box, { style: styles.titleStyle, title: title.length > 20 ? title : undefined, sx: theme => ({ color: headerStyle.color || theme.palette.secondary.contrastText }) }, this.state.details?.data?.name || this.props.fields.name),
453
- this.props.fields.hasDetails ? (React.createElement(Fab, { disabled: !this.props.alive, size: "small", style: styles.detailsButtonStyle, onClick: () => {
454
- if (!this.state.open) {
455
- this.loadDetails().catch(console.error);
456
- this.setState({ open: true });
457
- }
458
- }, color: "primary" },
459
- React.createElement(MoreVertIcon, null))) : null),
460
- React.createElement("div", { style: styles.statusStyle },
461
- status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, enabled: this.props.fields.enabled, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), disableEnableAction: this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler }))),
462
- this.renderIndicators()),
463
- React.createElement("div", { style: styles.bodyStyle },
464
- React.createElement(Typography, { variant: "body1", style: styles.deviceInfoStyle },
465
- this.props.fields.identifier ? (React.createElement("div", { onClick: this.copyToClipboard },
466
- React.createElement("b", { style: { marginRight: 4 } },
467
- getText(this.props.identifierLabel),
468
- ":"),
469
- this.props.fields.identifier)) : null,
470
- this.props.fields.manufacturer ? (React.createElement("div", null,
471
- React.createElement("b", { style: { marginRight: 4 } },
472
- getTranslation('manufacturer'),
473
- ":"),
474
- this.props.fields.manufacturer)) : null,
475
- this.props.fields.model ? (React.createElement("div", null,
476
- React.createElement("b", { style: { marginRight: 4 } },
477
- getTranslation('model'),
478
- ":"),
479
- this.props.fields.model)) : null),
480
- this.props.device.customInfo ? (React.createElement("div", { style: { padding: '0 16px 16px' } },
481
- React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
482
- /* ignore */
483
- }, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
484
- !!(this.hasFooterActions() || this.props.device.controls?.length) && (React.createElement("div", { style: {
485
- marginTop: 'auto',
486
- display: 'flex',
487
- gap: 8,
488
- paddingBottom: 5,
489
- minHeight: 34,
490
- paddingLeft: 10,
491
- paddingRight: 10,
492
- } },
493
- this.renderActions(),
494
- React.createElement("div", { style: { flexGrow: 1 } }),
495
- this.renderControls()))),
496
- this.renderDialog(),
497
- this.renderControlDialog()));
336
+ const picture = !!iconSrc && isPicture(iconSrc);
337
+ const accent = this.getAccent(this.props.theme);
338
+ const firstStatus = getStatusList(this.props.device)[0];
339
+ let glyph;
340
+ if (iconSrc) {
341
+ // The size is given inline, because `DeviceTypeIcon` sets an inline size of its own
342
+ const glyphSize = small ? 20 : 26;
343
+ glyph = (React.createElement(DeviceTypeIcon, { src: iconSrc, className: "dm-avatar-glyph", style: picture
344
+ ? { width: '100%', height: '100%', objectFit: 'contain' }
345
+ : { width: glyphSize, height: glyphSize, color: accent.color } }));
346
+ }
347
+ else {
348
+ // Without an icon, the initial of the name gives each device its own symbol
349
+ glyph = React.createElement("span", { className: "dm-avatar-letter" }, Array.from(name.trim())[0] || '?');
350
+ }
351
+ return (React.createElement("div", { className: picture ? 'dm-avatar dm-avatar-img' : 'dm-avatar', style: picture
352
+ ? undefined
353
+ : {
354
+ '--dm-accent': accent.background,
355
+ '--dm-accent-text': accent.color,
356
+ } },
357
+ glyph,
358
+ this.props.uploadImagesToInstance ? (React.createElement(DeviceImageUpload, { uploadImagesToInstance: this.props.uploadImagesToInstance, deviceId: this.props.device.id, manufacturer: this.props.fields.manufacturer, model: this.props.fields.model, onImageSelect: (imageData) => {
359
+ if (imageData) {
360
+ this.setState({ localIcon: imageData });
361
+ }
362
+ }, socket: this.props.socket })) : null,
363
+ firstStatus ? (React.createElement(DeviceLed, { status: firstStatus, stateOrObjectHandler: this.props.stateOrObjectHandler, alive: this.props.alive, enabled: this.props.fields.enabled })) : null));
498
364
  }
499
- render() {
500
- if (this.props.smallCards) {
501
- return this.renderSmall();
365
+ /** The switch that enables or disables the device */
366
+ renderEnabled(small) {
367
+ if (typeof this.props.fields.enabled !== 'boolean') {
368
+ return null;
502
369
  }
503
- return this.renderBig();
370
+ const action = this.props.device.actions?.find(a => a.id === ACTIONS.ENABLE_DISABLE);
371
+ const tooltip = this.props.fields.enabled
372
+ ? getTranslation('disableIconTooltip')
373
+ : getTranslation('enableIconTooltip');
374
+ return (React.createElement("div", { className: "dm-status-end" },
375
+ React.createElement(Tooltip, { title: tooltip, slotProps: tooltipSlotProps },
376
+ React.createElement("div", null,
377
+ React.createElement(Switch, { size: small ? 'small' : undefined, checked: this.props.fields.enabled, disabled: !action || !this.props.alive, ariaLabel: tooltip, onChange: () => action && this.props.deviceHandler(this.props.device.id, action)(), theme: this.props.theme })))));
504
378
  }
505
- }
506
- export class DeviceCardSkeleton extends PureComponent {
507
- render() {
508
- if (this.props.smallCards) {
509
- return this.renderSmall();
379
+ /** Identifier, manufacturer and model as a table of labels and values */
380
+ renderFacts() {
381
+ const { identifier, manufacturer, model } = this.props.fields;
382
+ if (!identifier && !manufacturer && !model) {
383
+ return null;
510
384
  }
511
- return this.renderBig();
385
+ return (React.createElement("div", { className: "dm-facts" },
386
+ identifier ? (React.createElement(React.Fragment, null,
387
+ React.createElement("span", { className: "dm-fact-label" }, getText(this.props.identifierLabel)),
388
+ React.createElement("span", { className: "dm-fact-copy" },
389
+ React.createElement("span", { title: identifier }, identifier),
390
+ React.createElement(Tooltip, { title: getTranslation('ctcButtonText'), slotProps: tooltipSlotProps },
391
+ React.createElement(IconButton, { size: "small", className: "dm-copy-btn", "aria-label": getTranslation('ctcButtonText'), onClick: this.copyToClipboard },
392
+ React.createElement(IconCopyToClipboard, null)))))) : null,
393
+ manufacturer ? (React.createElement(React.Fragment, null,
394
+ React.createElement("span", { className: "dm-fact-label" }, getTranslation('manufacturer')),
395
+ React.createElement("span", { className: "dm-fact-value", title: manufacturer }, manufacturer))) : null,
396
+ model ? (React.createElement(React.Fragment, null,
397
+ React.createElement("span", { className: "dm-fact-label" }, getTranslation('model')),
398
+ React.createElement("span", { className: "dm-fact-value", title: model }, model))) : null));
512
399
  }
513
- renderSmall() {
514
- const headerStyle = this.getCardHeaderStyle(this.props.theme);
515
- return (React.createElement(Paper, { sx: styles.cardStyle, style: smallCardSize },
516
- React.createElement(Box, { sx: headerStyle, style: { ...styles.headerStyle, minHeight: 48 } },
517
- React.createElement("div", { style: { ...styles.imgAreaStyle, height: 32, width: 32 } },
518
- React.createElement(Skeleton, { variant: "rounded", width: 24, height: 24 })),
519
- React.createElement(Box, { style: { ...styles.titleStyle, fontSize: 14, minWidth: '50%' }, sx: theme => ({
520
- color: headerStyle.color || theme.palette.secondary.contrastText,
521
- }) },
522
- React.createElement(Skeleton, null))),
523
- React.createElement("div", { style: { ...styles.statusStyle, height: 'auto', padding: '8px 15px 0 15px' } }),
524
- React.createElement("div", { style: styles.bodyStyle },
525
- React.createElement(Typography, { variant: "body2", style: { ...styles.deviceInfoStyle, padding: '10px 10px 0 10px' } },
526
- React.createElement("div", null,
527
- React.createElement(Skeleton, null)),
528
- React.createElement("div", null,
529
- React.createElement(Skeleton, null)),
530
- React.createElement("div", null,
531
- React.createElement(Skeleton, null))))));
400
+ render() {
401
+ const small = !!this.props.smallCards;
402
+ const status = getStatusList(this.props.device);
403
+ const name = this.state.details?.data?.name || this.props.fields.name || '';
404
+ const off = this.props.fields.enabled === false;
405
+ let className = small ? 'dm-card dm-small' : 'dm-card';
406
+ if (off) {
407
+ className += ' dm-off';
408
+ }
409
+ const size = this.props.fillContainer ? filledCardSize : small ? smallCardSize : cardSize;
410
+ return (React.createElement("div", { className: className, style: { ...getThemeVars(this.props.theme), ...size } },
411
+ React.createElement("div", { className: "dm-head" },
412
+ this.renderAvatar(name, small),
413
+ React.createElement("div", { className: "dm-name", title: name.length > (small ? 15 : 20) ? name : undefined }, name),
414
+ this.props.fields.hasDetails ? (React.createElement("div", { className: "dm-head-end" },
415
+ React.createElement(IconButton, { size: "small", disabled: !this.props.alive, "aria-label": getTranslation('moreInformation'), onClick: () => {
416
+ if (!this.state.open) {
417
+ this.loadDetails().catch(console.error);
418
+ this.setState({ open: true });
419
+ }
420
+ } },
421
+ React.createElement(MoreVertIcon, { className: "dm-ani dm-ani-turn" })))) : null),
422
+ React.createElement("div", { className: "dm-status" },
423
+ status.map((s, i) => (React.createElement(DeviceStatusComponent, { key: i, socket: this.props.socket, deviceId: this.props.device.id, connectionType: this.props.fields.connectionType, status: s, statusAction: this.props.device.actions?.find(a => a.id === ACTIONS.STATUS), update: i === 0 ? this.props.device.update : undefined, updateAction: i === 0 ? this.props.device.actions?.find(a => a.id === ACTIONS.UPDATE) : undefined, batteryAction: this.props.device.actions?.find(a => a.id === ACTIONS.BATTERY), deviceHandler: this.props.deviceHandler, theme: this.props.theme, stateOrObjectHandler: this.props.stateOrObjectHandler }))),
424
+ this.renderIndicators(),
425
+ this.renderEnabled(small)),
426
+ this.renderFacts(),
427
+ this.props.device.customInfo ? (React.createElement("div", { className: "dm-custom" },
428
+ React.createElement(JsonConfig, { instanceId: this.props.instanceId, socket: this.props.socket, schema: this.props.device.customInfo.schema, data: this.props.device.customInfo.data || {}, onChange: (_data) => {
429
+ /* ignore */
430
+ }, themeName: this.props.themeName, themeType: this.props.themeType, theme: this.props.theme, isFloatComma: this.props.isFloatComma, dateFormat: this.props.dateFormat }))) : null,
431
+ this.hasFooterActions() || this.props.device.controls?.length ? (React.createElement("div", { className: "dm-foot" },
432
+ this.renderActions(),
433
+ React.createElement("div", { className: "dm-foot-end" }, this.renderControls()))) : null,
434
+ this.renderDialog(),
435
+ this.renderControlDialog()));
532
436
  }
533
- renderBig() {
534
- const headerStyle = this.getCardHeaderStyle(this.props.theme);
535
- return (React.createElement(Paper, { sx: styles.cardStyle, style: cardSize },
536
- React.createElement(Box, { sx: headerStyle, style: styles.headerStyle },
537
- React.createElement("div", { style: styles.imgAreaStyle },
538
- React.createElement(Skeleton, { variant: "rounded", width: 24, height: 24 })),
539
- React.createElement(Box, { style: styles.titleStyle, sx: theme => ({
540
- color: headerStyle.color || theme.palette.secondary.contrastText,
541
- minWidth: '50%',
542
- }) },
543
- React.createElement(Skeleton, null))),
544
- React.createElement("div", { style: styles.statusStyle }),
545
- React.createElement("div", { style: styles.bodyStyle },
546
- React.createElement(Typography, { variant: "body1", style: styles.deviceInfoStyle },
547
- React.createElement("div", null,
548
- React.createElement(Skeleton, null)),
549
- React.createElement("div", null,
550
- React.createElement(Skeleton, null)),
551
- React.createElement("div", null,
552
- React.createElement(Skeleton, null))))));
437
+ }
438
+ /** Placeholder of a device that is not loaded yet, with the layout of the card */
439
+ export class DeviceCardSkeleton extends PureComponent {
440
+ constructor(props) {
441
+ super(props);
442
+ ensureDmStyles();
553
443
  }
554
- // eslint-disable-next-line class-methods-use-this
555
- getCardHeaderStyle(theme, maxWidth) {
556
- const backgroundColor = theme.palette.secondary.main;
557
- const color = theme.palette.secondary.contrastText;
558
- return {
559
- backgroundColor,
560
- color,
561
- maxWidth,
562
- };
444
+ render() {
445
+ const small = !!this.props.smallCards;
446
+ return (React.createElement("div", { className: small ? 'dm-card dm-small dm-skeleton' : 'dm-card dm-skeleton', style: { ...getThemeVars(this.props.theme), ...(small ? smallCardSize : cardSize) } },
447
+ React.createElement("div", { className: "dm-head" },
448
+ React.createElement("div", { className: "dm-avatar" }),
449
+ React.createElement("div", { className: "dm-name" },
450
+ React.createElement(Skeleton, { width: "70%" }))),
451
+ React.createElement("div", { className: "dm-status" },
452
+ React.createElement(Skeleton, { variant: "rounded", width: small ? 120 : 150, height: 20 })),
453
+ React.createElement("div", { className: "dm-facts" }, [0, 1, 2].map(i => (React.createElement(React.Fragment, { key: i },
454
+ React.createElement(Skeleton, { width: small ? 42 : 56 }),
455
+ React.createElement(Skeleton, { width: `${70 - i * 12}%` }))))),
456
+ React.createElement("div", { className: "dm-foot" },
457
+ React.createElement(Skeleton, { variant: "rounded", width: small ? 110 : 150, height: 20 }))));
563
458
  }
564
459
  }
565
460
  //# sourceMappingURL=DeviceCard.js.map