@iobroker/adapter-react-v5 6.1.2 → 6.1.5

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 (66) hide show
  1. package/README.md +4 -1
  2. package/craco-module-federation.js +1 -1
  3. package/package.json +1 -1
  4. package/src/AdminConnection.tsx +3 -0
  5. package/src/Components/404.tsx +121 -0
  6. package/src/Components/ColorPicker.tsx +315 -0
  7. package/src/Components/ComplexCron.tsx +507 -0
  8. package/src/Components/CopyToClipboard.tsx +165 -0
  9. package/src/Components/CustomModal.tsx +163 -0
  10. package/src/Components/FileBrowser.tsx +2394 -0
  11. package/src/Components/FileViewer.tsx +384 -0
  12. package/src/Components/Icon.tsx +210 -0
  13. package/src/Components/IconPicker.tsx +149 -0
  14. package/src/Components/IconSelector.tsx +2202 -0
  15. package/src/Components/Image.tsx +176 -0
  16. package/src/Components/Loader.tsx +304 -0
  17. package/src/Components/Logo.tsx +166 -0
  18. package/src/Components/MDUtils.tsx +100 -0
  19. package/src/Components/ObjectBrowser.tsx +7915 -0
  20. package/src/Components/Router.tsx +90 -0
  21. package/src/Components/SaveCloseButtons.tsx +113 -0
  22. package/src/Components/Schedule.tsx +1724 -0
  23. package/src/Components/SelectWithIcon.tsx +197 -0
  24. package/src/Components/TabContainer.tsx +55 -0
  25. package/src/Components/TabContent.tsx +37 -0
  26. package/src/Components/TabHeader.tsx +19 -0
  27. package/src/Components/TableResize.tsx +259 -0
  28. package/src/Components/TextWithIcon.tsx +148 -0
  29. package/src/Components/ToggleThemeMenu.tsx +34 -0
  30. package/src/Components/TreeTable.tsx +919 -0
  31. package/src/Components/UploadImage.tsx +599 -0
  32. package/src/Components/Utils.tsx +1794 -0
  33. package/src/Components/loader.css +222 -0
  34. package/src/Components/withWidth.tsx +21 -0
  35. package/src/Connection.tsx +7 -0
  36. package/src/Dialogs/ComplexCron.tsx +129 -0
  37. package/src/Dialogs/Confirm.tsx +162 -0
  38. package/src/Dialogs/Cron.tsx +182 -0
  39. package/src/Dialogs/Error.tsx +72 -0
  40. package/src/Dialogs/Message.tsx +71 -0
  41. package/src/Dialogs/SelectFile.tsx +270 -0
  42. package/src/Dialogs/SelectID.tsx +298 -0
  43. package/src/Dialogs/SimpleCron.tsx +100 -0
  44. package/src/Dialogs/TextInput.tsx +107 -0
  45. package/src/GenericApp.tsx +976 -0
  46. package/src/LegacyConnection.tsx +3589 -0
  47. package/src/Prompt.tsx +20 -0
  48. package/src/Theme.tsx +479 -0
  49. package/src/icons/IconAdapter.tsx +20 -0
  50. package/src/icons/IconAlias.tsx +20 -0
  51. package/src/icons/IconChannel.tsx +21 -0
  52. package/src/icons/IconClearFilter.tsx +22 -0
  53. package/src/icons/IconClosed.tsx +17 -0
  54. package/src/icons/IconCopy.tsx +16 -0
  55. package/src/icons/IconDevice.tsx +27 -0
  56. package/src/icons/IconDocument.tsx +17 -0
  57. package/src/icons/IconDocumentReadOnly.tsx +18 -0
  58. package/src/icons/IconExpert.tsx +18 -0
  59. package/src/icons/IconFx.tsx +36 -0
  60. package/src/icons/IconInstance.tsx +20 -0
  61. package/src/icons/IconLogout.tsx +30 -0
  62. package/src/icons/IconNoIcon.tsx +19 -0
  63. package/src/icons/IconOpen.tsx +17 -0
  64. package/src/icons/IconProps.tsx +15 -0
  65. package/src/icons/IconState.tsx +17 -0
  66. package/src/index.css +55 -0
@@ -0,0 +1,2394 @@
1
+ /**
2
+ * Copyright 2020-2024, Denis Haev <dogafox@gmail.com>
3
+ *
4
+ * MIT License
5
+ *
6
+ * */
7
+ import React, { Component } from 'react';
8
+ import Dropzone from 'react-dropzone';
9
+
10
+ import {
11
+ LinearProgress,
12
+ Hidden,
13
+ ListItemIcon,
14
+ ListItemText,
15
+ Menu,
16
+ MenuItem,
17
+ Tooltip,
18
+ CircularProgress,
19
+ Toolbar,
20
+ IconButton,
21
+ Fab,
22
+ Dialog,
23
+ DialogTitle,
24
+ DialogContent,
25
+ DialogContentText,
26
+ DialogActions,
27
+ Button,
28
+ Input,
29
+ Breadcrumbs,
30
+ Box,
31
+ } from '@mui/material';
32
+
33
+ // MUI Icons
34
+ import {
35
+ Refresh as RefreshIcon,
36
+ Close as CloseIcon,
37
+ Bookmark as JsonIcon,
38
+ BookmarkBorder as CssIcon,
39
+ Description as HtmlIcon,
40
+ Edit as EditIcon,
41
+ Code as JSIcon,
42
+ InsertDriveFile as FileIcon,
43
+ Publish as UploadIcon,
44
+ MusicNote as MusicIcon,
45
+ SaveAlt as DownloadIcon,
46
+ CreateNewFolder as AddFolderIcon,
47
+ FolderOpen as EmptyFilterIcon,
48
+ List as IconList,
49
+ ViewModule as IconTile,
50
+ ArrowBack as IconBack,
51
+ Delete as DeleteIcon,
52
+ Brightness6 as Brightness5Icon,
53
+ Image as TypeIconImages,
54
+ FontDownload as TypeIconTxt,
55
+ AudioFile as TypeIconAudio,
56
+ Videocam as TypeIconVideo,
57
+ KeyboardReturn as EnterIcon,
58
+ FolderSpecial as RestrictedIcon,
59
+ } from '@mui/icons-material';
60
+
61
+ import type { Connection } from '@iobroker/socket-client';
62
+
63
+ import ErrorDialog from '../Dialogs/Error';
64
+ import Utils from './Utils';
65
+ import TextInputDialog from '../Dialogs/TextInput';
66
+
67
+ // Custom Icons
68
+ import IconExpert from '../icons/IconExpert';
69
+ import IconClosed from '../icons/IconClosed';
70
+ import IconOpen from '../icons/IconOpen';
71
+ import IconNoIcon from '../icons/IconNoIcon';
72
+ import Icon from './Icon';
73
+
74
+ import withWidth from './withWidth';
75
+ import {
76
+ ThemeName, ThemeType,
77
+ Translate, IobTheme,
78
+ } from '../types';
79
+
80
+ import FileViewer, { EXTENSIONS } from './FileViewer';
81
+
82
+ const ROW_HEIGHT = 32;
83
+ const BUTTON_WIDTH = 32;
84
+ const TILE_HEIGHT = 120;
85
+ const TILE_WIDTH = 64;
86
+
87
+ const NOT_FOUND = 'Not found';
88
+
89
+ // Todo: replace with js-controller types
90
+ export interface MetaACL extends ioBroker.ObjectACL {
91
+ file: number;
92
+ }
93
+
94
+ // Todo: replace with js-controller types
95
+ export interface MetaObject extends ioBroker.MetaObject {
96
+ acl: MetaACL;
97
+ }
98
+
99
+ const FILE_TYPE_ICONS: Record<string, React.FC<{ fontSize?: 'small' }>> = {
100
+ all: FileIcon,
101
+ images: TypeIconImages,
102
+ code: JSIcon,
103
+ txt: TypeIconTxt,
104
+ audio: TypeIconAudio,
105
+ video: TypeIconVideo,
106
+ };
107
+
108
+ const styles: Record<string, any> = {
109
+ dialog: (theme: IobTheme) => ({
110
+ height: `calc(100% - ${theme.mixins.toolbar.minHeight}px)`,
111
+ }),
112
+ root: {
113
+ width: '100%',
114
+ overflow: 'hidden',
115
+ height: '100%',
116
+ position: 'relative',
117
+ },
118
+ filesDiv: {
119
+ width: 'calc(100% - 16px)',
120
+ overflowX: 'hidden',
121
+ overflowY: 'auto',
122
+ padding: 8,
123
+ },
124
+ filesDivHint: {
125
+ position: 'absolute',
126
+ bottom: 0,
127
+ left: 20,
128
+ opacity: 0.7,
129
+ fontStyle: 'italic',
130
+ fontSize: 12,
131
+ },
132
+ filesDivTable: {
133
+ height: 'calc(100% - 56px)',
134
+ },
135
+ filesDivTile: {
136
+ height: `calc(100% - ${48 * 2 + 8}px)`,
137
+ display: 'flex',
138
+ alignContent: 'flex-start',
139
+ alignItems: 'stretch',
140
+ flexWrap: 'wrap',
141
+ flex: `0 0 ${TILE_WIDTH}px`,
142
+ },
143
+
144
+ itemTile: (theme: IobTheme) => ({
145
+ position: 'relative',
146
+ userSelect: 'none',
147
+ cursor: 'pointer',
148
+ height: TILE_HEIGHT,
149
+ width: TILE_WIDTH,
150
+ display: 'inline-block',
151
+ textAlign: 'center',
152
+ opacity: 0.1,
153
+ transition: 'opacity 1s',
154
+ margin: 4,
155
+ '&:hover': {
156
+ background: theme.palette.secondary.light,
157
+ color: Utils.invertColor(theme.palette.secondary.main, true),
158
+ },
159
+ }),
160
+ itemNameFolderTile: {
161
+ fontWeight: 'bold',
162
+ },
163
+ itemNameTile: {
164
+ width: '100%',
165
+ height: 32,
166
+ overflow: 'hidden',
167
+ textOverflow: 'ellipsis',
168
+ fontSize: 12,
169
+ textAlign: 'center',
170
+ wordBreak: 'break-all',
171
+ },
172
+ itemFolderIconTile: (theme: IobTheme) => ({
173
+ width: '100%',
174
+ height: TILE_HEIGHT - 32 - 16 - 8, // name + size
175
+ display: 'block',
176
+ pl: 1,
177
+ color: theme.palette.secondary.main || '#fbff7d',
178
+ }),
179
+ itemFolderIconBack: (theme: IobTheme) => ({
180
+ position: 'absolute',
181
+ top: 22,
182
+ left: 18,
183
+ zIndex: 1,
184
+ color: theme.palette.mode === 'dark' ? '#FFF' : '#000',
185
+ }),
186
+ itemSizeTile: {
187
+ width: '100%',
188
+ height: 16,
189
+ textAlign: 'center',
190
+ fontSize: 10,
191
+ },
192
+ itemImageTile: {
193
+ width: 'calc(100% - 8px)',
194
+ height: TILE_HEIGHT - 32 - 16 - 8, // name + size
195
+ margin: 4,
196
+ display: 'block',
197
+ textAlign: 'center',
198
+ objectFit: 'contain',
199
+ },
200
+ itemIconTile: {
201
+ width: '100%',
202
+ height: TILE_HEIGHT - 32 - 16 - 8, // name + size
203
+ display: 'block',
204
+ objectFit: 'contain',
205
+ },
206
+
207
+ itemSelected: (theme: IobTheme) => ({
208
+ background: theme.palette.primary.main,
209
+ color: Utils.invertColor(theme.palette.primary.main, true),
210
+ }),
211
+
212
+ itemTable: (theme: IobTheme) => ({
213
+ userSelect: 'none',
214
+ cursor: 'pointer',
215
+ height: ROW_HEIGHT,
216
+ display: 'inline-flex',
217
+ lineHeight: `${ROW_HEIGHT}px`,
218
+ '&:hover': {
219
+ background: theme.palette.secondary.light,
220
+ color: Utils.invertColor(theme.palette.secondary.main, true),
221
+ },
222
+ }),
223
+ itemNameTable: {
224
+ display: 'inline-block',
225
+ pl: '10px',
226
+ fontSize: '1rem',
227
+ verticalAlign: 'top',
228
+ flexGrow: 1,
229
+ '@media screen and (max-width: 500px)': {
230
+ whiteSpace: 'nowrap',
231
+ overflow: 'hidden',
232
+ textOverflow: 'ellipsis',
233
+ textAlign: 'end',
234
+ direction: 'rtl',
235
+ },
236
+ },
237
+ itemNameFolderTable: {
238
+ fontWeight: 'bold',
239
+ },
240
+ itemSizeTable: {
241
+ display: 'inline-block',
242
+ width: 60,
243
+ verticalAlign: 'top',
244
+ textAlign: 'right',
245
+ whiteSpace: 'nowrap',
246
+ },
247
+ itemAccessTable: {
248
+ // display: 'inline-block',
249
+ verticalAlign: 'top',
250
+ width: 60,
251
+ textAlign: 'right',
252
+ paddingRight: 5,
253
+ display: 'flex',
254
+ justifyContent: 'center',
255
+ },
256
+ itemImageTable: {
257
+ display: 'inline-block',
258
+ width: 30,
259
+ marginTop: 1,
260
+ objectFit: 'contain',
261
+ maxHeight: 30,
262
+ },
263
+ itemNoImageTable: {
264
+ marginTop: 6,
265
+ },
266
+ itemIconTable: {
267
+ display: 'inline-block',
268
+ marginTop: 1,
269
+ width: 30,
270
+ height: 30,
271
+ },
272
+ itemFolderTable: {
273
+
274
+ },
275
+ itemFolderTemp: {
276
+ opacity: 0.4,
277
+ },
278
+ itemFolderIconTable: (theme: IobTheme) => ({
279
+ marginTop: 1,
280
+ marginLeft: 8,
281
+ display: 'inline-block',
282
+ width: 30,
283
+ height: 30,
284
+ color: theme.palette.secondary.main || '#fbff7d',
285
+ }),
286
+ itemDownloadButtonTable: (theme: IobTheme) => ({
287
+ display: 'inline-block',
288
+ width: BUTTON_WIDTH,
289
+ height: ROW_HEIGHT,
290
+ minWidth: BUTTON_WIDTH,
291
+ verticalAlign: 'middle',
292
+ textAlign: 'center',
293
+ padding: 0,
294
+ borderRadius: `${BUTTON_WIDTH / 2}px`,
295
+ '&:hover': {
296
+ backgroundColor: theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.08)' : 'rgba(0, 0, 0, 0.08)',
297
+ },
298
+ '& span': {
299
+ pt: '9px',
300
+ },
301
+ '& svg': {
302
+ width: 14,
303
+ height: 14,
304
+ fontSize: '1rem',
305
+ mt: '-3px',
306
+ verticalAlign: 'middle',
307
+ color: theme.palette.mode === 'dark' ? '#EEE' : '#111',
308
+ },
309
+ }),
310
+ itemDownloadEmptyTable: {
311
+ display: 'inline-block',
312
+ width: BUTTON_WIDTH,
313
+ height: ROW_HEIGHT,
314
+ minWidth: BUTTON_WIDTH,
315
+ padding: 0,
316
+ },
317
+ itemAclButtonTable: {
318
+ width: BUTTON_WIDTH,
319
+ height: ROW_HEIGHT,
320
+ minWidth: BUTTON_WIDTH,
321
+ verticalAlign: 'top',
322
+ padding: 0,
323
+ fontSize: 12,
324
+ display: 'flex',
325
+ },
326
+ itemDeleteButtonTable: {
327
+ display: 'inline-block',
328
+ width: BUTTON_WIDTH,
329
+ height: ROW_HEIGHT,
330
+ minWidth: BUTTON_WIDTH,
331
+ verticalAlign: 'top',
332
+ padding: 0,
333
+ '& svg': {
334
+ width: 18,
335
+ height: 18,
336
+ fontSize: '1.5rem',
337
+ },
338
+ },
339
+
340
+ uploadDiv: {
341
+ top: 0,
342
+ zIndex: 1,
343
+ bottom: 0,
344
+ left: 0,
345
+ right: 0,
346
+ position: 'absolute',
347
+ opacity: 0.9,
348
+ textAlign: 'center',
349
+ background: '#FFFFFF',
350
+ },
351
+ uploadDivDragging: {
352
+ opacity: 1,
353
+ },
354
+
355
+ uploadCenterDiv: (theme: IobTheme) => ({
356
+ m: '20px',
357
+ border: '3px dashed grey',
358
+ borderRadius: '30px',
359
+ width: 'calc(100% - 40px)',
360
+ height: 'calc(100% - 40px)',
361
+ position: 'relative',
362
+ color: theme.palette.mode === 'dark' ? '#222' : '#CCC',
363
+ display: 'flex',
364
+ alignItems: 'center',
365
+ justifyContent: 'center',
366
+ }),
367
+ uploadCenterIcon: {
368
+ width: '25%',
369
+ height: '25%',
370
+ },
371
+ uploadCenterText: {
372
+ fontSize: 24,
373
+ fontWeight: 'bold',
374
+ },
375
+ uploadCloseButton: {
376
+ zIndex: 2,
377
+ position: 'absolute',
378
+ top: 30,
379
+ right: 30,
380
+ },
381
+ uploadCenterTextAndIcon: {
382
+ position: 'absolute',
383
+ height: '30%',
384
+ width: '100%',
385
+ margin: 'auto',
386
+ opacity: 0.3,
387
+ },
388
+ menuButtonExpertActive: {
389
+ color: '#c00000',
390
+ },
391
+ menuButtonRestrictActive: {
392
+ color: '#c05000',
393
+ },
394
+ pathDiv: (theme: IobTheme) => ({
395
+ display: 'flex',
396
+ width: 'calc(100% - 16px)',
397
+ ml: 1,
398
+ mr: 1,
399
+ textOverflow: 'clip',
400
+ overflow: 'hidden',
401
+ whiteSpace: 'nowrap',
402
+ backgroundColor: theme.palette.secondary.main,
403
+ }),
404
+ pathDivInput: {
405
+ width: '100%',
406
+ },
407
+ pathDivBreadcrumbDir: (theme: IobTheme) => ({
408
+ pl: '2px',
409
+ pr: '2px',
410
+ cursor: 'pointer',
411
+ '&:hover': {
412
+ background: theme.palette.primary.main,
413
+ },
414
+ }),
415
+ backgroundImageLight: {
416
+ background: 'white',
417
+ },
418
+ backgroundImageDark: {
419
+ background: 'black',
420
+ },
421
+ backgroundImageColored: {
422
+ background: 'silver',
423
+ },
424
+ specialFolder: (theme: IobTheme) => ({
425
+ color: theme.palette.mode === 'dark' ? '#229b0f' : '#5dd300',
426
+ }),
427
+ tooltip: {
428
+ pointerEvents: 'none',
429
+ },
430
+ };
431
+
432
+ const USER_DATA = '0_userdata.0';
433
+
434
+ function getParentDir(dir: string | null): string {
435
+ const parts = (dir || '').split('/');
436
+ parts.length && parts.pop();
437
+ return parts.join('/');
438
+ }
439
+
440
+ function isFile(path: string): boolean {
441
+ const ext = Utils.getFileExtension(path);
442
+ return !!(ext?.toLowerCase().match(/[a-z]+/) && ext.length < 5);
443
+ }
444
+
445
+ const TABLE = 'Table';
446
+ const TILE = 'Tile';
447
+
448
+ export interface FileBrowserProps {
449
+ /** The key to identify this component. */
450
+ key?: string;
451
+ /** Additional styling for this component. */
452
+ style?: React.CSSProperties;
453
+ /** The CSS class name. */
454
+ className?: string;
455
+ /** Translation function. */
456
+ t: Translate;
457
+ /** The selected language. */
458
+ lang: ioBroker.Languages;
459
+ /** The socket connection. */
460
+ socket: Connection;
461
+ /** Is the component data ready. */
462
+ ready?: boolean;
463
+ /** Is expert mode enabled? (default: false) */
464
+ expertMode?: boolean;
465
+ /** Show the toolbar? (default: false) */
466
+ showToolbar?: boolean;
467
+ /** If defined, allow selecting only files from this folder and subfolders */
468
+ limitPath?: string;
469
+ /** Allow upload of new files? (default: false) */
470
+ allowUpload?: boolean;
471
+ /** Allow download of files? (default: false) */
472
+ allowDownload?: boolean;
473
+ /** Allow creation of new folders? (default: false) */
474
+ allowCreateFolder?: boolean;
475
+ /** Allow deleting files? (default: false) */
476
+ allowDelete?: boolean;
477
+ /** Allow viewing files? (default: false) */
478
+ allowView?: boolean;
479
+ /** Prefix (default: '.') */
480
+ imagePrefix?: string;
481
+ /** Show the expert button? */
482
+ showExpertButton?: boolean;
483
+ /** Type of view */
484
+ viewType?: 'Table' | 'Tile';
485
+ /** Show the buttons to switch the view from table to tile? (default: false) */
486
+ showViewTypeButton?: boolean;
487
+ /** The ID of the selected file. */
488
+ selected?: string | string[];
489
+ /** The file extensions to show, like ['png', 'svg', 'bmp', 'jpg', 'jpeg', 'gif']. */
490
+ filterFiles?: string[];
491
+ /** The file extension categories to show. */
492
+ filterByType?: 'images' | 'code' | 'txt';
493
+ /** Callback for file selection. */
494
+ onSelect?: (id: string | string[], isDoubleClick?: boolean, isFolder?: boolean) => void;
495
+ /** Theme name */
496
+ themeName?: ThemeName;
497
+ /** Theme type. */
498
+ themeType?: ThemeType;
499
+ /** Theme object. */
500
+ theme: IobTheme;
501
+
502
+ /** Padding in pixels for folder levels */
503
+ levelPadding?: number;
504
+
505
+ restrictToFolder?: string;
506
+
507
+ // eslint-disable-next-line no-use-before-define
508
+ modalEditOfAccessControl?: (obj: FileBrowserClass) => React.JSX.Element | null;
509
+
510
+ allowNonRestricted?: boolean;
511
+
512
+ showTypeSelector?: boolean;
513
+ }
514
+
515
+ export interface FolderOrFileItem {
516
+ id: string;
517
+ level: number;
518
+ name: string;
519
+ folder: boolean;
520
+ temp?: boolean;
521
+
522
+ size?: number | undefined;
523
+ ext?: string | null;
524
+ modified?: number;
525
+ title?: ioBroker.StringOrTranslated;
526
+ meta?: boolean;
527
+ from?: string;
528
+ ts?: number;
529
+ color?: string;
530
+ icon?: string;
531
+ acl?: ioBroker.EvaluatedFileACL | MetaACL;
532
+ }
533
+
534
+ export type Folders = Record<string, FolderOrFileItem[]>;
535
+
536
+ function sortFolders(a: FolderOrFileItem, b: FolderOrFileItem) {
537
+ if (a.folder && b.folder) {
538
+ return a.name > b.name ? 1 : (a.name < b.name ? -1 : 0);
539
+ }
540
+ if (a.folder) {
541
+ return -1;
542
+ }
543
+ if (b.folder) {
544
+ return 1;
545
+ }
546
+ return a.name > b.name ? 1 : (a.name < b.name ? -1 : 0);
547
+ }
548
+
549
+ interface FileBrowserState {
550
+ viewType: string;
551
+ folders: Folders;
552
+ filterEmpty: boolean;
553
+ expanded: string[];
554
+ currentDir: string;
555
+ expertMode: boolean;
556
+ addFolder: boolean;
557
+ uploadFile: boolean | 'dragging';
558
+ deleteItem: string;
559
+ viewer: string;
560
+ formatEditFile: string | null;
561
+ path: string;
562
+ selected: string;
563
+ errorText: string;
564
+ modalEditOfAccess: boolean;
565
+ backgroundImage: string | null;
566
+ queueLength: number;
567
+ loadAllFolders: boolean;
568
+ fileErrors: string[];
569
+ filterByType: string;
570
+ showTypesMenu: HTMLButtonElement | null;
571
+ restrictToFolder: string;
572
+ pathFocus: boolean;
573
+ }
574
+
575
+ export class FileBrowserClass extends Component<FileBrowserProps, FileBrowserState> {
576
+ private readonly imagePrefix: string;
577
+
578
+ private readonly levelPadding: number;
579
+
580
+ private mounted: boolean;
581
+
582
+ private suppressDeleteConfirm: number;
583
+
584
+ private browseList:
585
+ | {
586
+ processing?: boolean;
587
+ resolve: null | ((files: ioBroker.ReadDirResult[]) => void);
588
+ reject: null | ((e: any) => void);
589
+ adapter: string | null;
590
+ relPath: string | null;
591
+ }[]
592
+ | null;
593
+
594
+ private browseListRunning: boolean;
595
+
596
+ private initialReadFinished: boolean;
597
+
598
+ private supportSubscribes: boolean | null;
599
+
600
+ private _tempTimeout: Record<string, ReturnType<typeof setTimeout>>;
601
+
602
+ private readonly limitToObjectID: string | null = null;
603
+
604
+ private readonly limitToPath: string | null = null;
605
+
606
+ private lastSelect: number | null = null;
607
+
608
+ private setOpacityTimer: ReturnType<typeof setTimeout> | null = null;
609
+
610
+ private cacheFoldersTimeout: ReturnType<typeof setTimeout> | null = null;
611
+
612
+ private foldersLoading: boolean | null = null;
613
+
614
+ private cacheFolders: Folders | null = null;
615
+
616
+ private readonly localStorage: Storage;
617
+
618
+ constructor(props: FileBrowserProps) {
619
+ super(props);
620
+
621
+ this.localStorage = ((window as any)._localStorage || window.localStorage);
622
+ const expandedStr = this.localStorage.getItem('files.expanded') || '[]';
623
+
624
+ if (this.props.limitPath) {
625
+ const parts = this.props.limitPath.split('/');
626
+ this.limitToObjectID = parts[0];
627
+ this.limitToPath = !parts.length ? null : parts.length === 1 && parts[0] === '' ? null : parts.join('/');
628
+ if (this.limitToPath && this.limitToPath.endsWith('/')) {
629
+ this.limitToPath.substring(0, this.limitToPath.length - 1);
630
+ }
631
+ }
632
+
633
+ let expanded: string[];
634
+ try {
635
+ expanded = JSON.parse(expandedStr);
636
+ if (this.limitToPath) {
637
+ expanded = expanded.filter(id => id.startsWith(`${this.limitToPath}/`) ||
638
+ id === this.limitToPath || this.limitToPath?.startsWith(`${id}/`));
639
+ }
640
+ } catch (e) {
641
+ expanded = [];
642
+ }
643
+
644
+ let viewType;
645
+ if (this.props.showViewTypeButton) {
646
+ viewType = this.localStorage.getItem('files.viewType') || TABLE;
647
+ } else {
648
+ viewType = TABLE;
649
+ }
650
+
651
+ let selected =
652
+ this.props.selected ||
653
+ this.localStorage.getItem('files.selected') ||
654
+ USER_DATA;
655
+
656
+ let currentDir: string;
657
+
658
+ if (props.restrictToFolder) {
659
+ selected = props.restrictToFolder;
660
+ currentDir = props.restrictToFolder;
661
+ const parts = props.restrictToFolder.split('/');
662
+ expanded = [];
663
+ let path = '';
664
+ for (let i = 0; i < parts.length; i++) {
665
+ path += (path ? '/' : '') + parts[i];
666
+ expanded.push(path);
667
+ }
668
+ } else {
669
+ // TODO: Now we do not support multiple selection
670
+ if (Array.isArray(selected)) {
671
+ selected = selected[0];
672
+ }
673
+
674
+ if (isFile(selected)) {
675
+ currentDir = getParentDir(selected);
676
+ } else {
677
+ currentDir = selected;
678
+ }
679
+ }
680
+ const backgroundImage =
681
+ this.localStorage.getItem('files.backgroundImage') || null;
682
+
683
+ this.state = {
684
+ viewType,
685
+ folders: {},
686
+ filterEmpty: this.localStorage.getItem('files.empty') !== 'false',
687
+ expanded,
688
+ currentDir,
689
+ expertMode: !!props.expertMode,
690
+ addFolder: false,
691
+ uploadFile: false,
692
+ deleteItem: '',
693
+ // marked: [],
694
+ viewer: '',
695
+ formatEditFile: '',
696
+ path: selected,
697
+ selected,
698
+ errorText: '',
699
+ modalEditOfAccess: false,
700
+ backgroundImage,
701
+ queueLength: 0,
702
+ loadAllFolders: false,
703
+ // allFoldersLoaded: false,
704
+ fileErrors: [],
705
+ filterByType: props.filterByType || window.localStorage.getItem('files.filterByType') || '',
706
+ showTypesMenu: null,
707
+ restrictToFolder: props.restrictToFolder || '',
708
+ pathFocus: false,
709
+ };
710
+
711
+ this.imagePrefix = this.props.imagePrefix || './files/';
712
+
713
+ this.levelPadding = this.props.levelPadding || 20;
714
+ this.mounted = true;
715
+ this.suppressDeleteConfirm = 0;
716
+
717
+ this.browseList = [];
718
+ this.browseListRunning = false;
719
+ this.initialReadFinished = false;
720
+ this.supportSubscribes = null;
721
+ this._tempTimeout = {};
722
+ }
723
+
724
+ static getDerivedStateFromProps(
725
+ props: FileBrowserProps,
726
+ state: FileBrowserState,
727
+ ): Partial<FileBrowserState> | null {
728
+ if (props.expertMode !== undefined && props.expertMode !== state.expertMode) {
729
+ return { expertMode: props.expertMode, loadAllFolders: true };
730
+ }
731
+
732
+ return null;
733
+ }
734
+
735
+ async loadFolders() {
736
+ this.initialReadFinished = false;
737
+
738
+ let folders = (await this.browseFolder('/')) as unknown as Folders;
739
+
740
+ if (this.state.viewType === TABLE) {
741
+ folders = (await this.browseFolders([...this.state.expanded], folders)) as unknown as Folders;
742
+ } else if (
743
+ this.state.currentDir &&
744
+ this.state.currentDir !== '/' &&
745
+ (!this.limitToObjectID || this.state.currentDir.startsWith(this.limitToObjectID))
746
+ ) {
747
+ folders = (await this.browseFolder(this.state.currentDir, folders)) as unknown as Folders;
748
+ }
749
+
750
+ this.setState({ folders }, () => {
751
+ if (this.state.viewType === TABLE && !this.findItem(this.state.selected)) {
752
+ const parts = this.state.selected.split('/');
753
+ while (parts.length && !this.findItem(parts.join('/'))) {
754
+ parts.pop();
755
+ }
756
+ let selected;
757
+ if (parts.length) {
758
+ selected = parts.join('/');
759
+ } else {
760
+ selected = USER_DATA;
761
+ }
762
+ this.setState({ selected, path: selected, pathFocus: false }, () => this.scrollToSelected());
763
+ } else {
764
+ this.scrollToSelected();
765
+ }
766
+ this.initialReadFinished = true;
767
+ });
768
+ }
769
+
770
+ scrollToSelected() {
771
+ if (this.mounted) {
772
+ const el = document.getElementById(this.state.selected);
773
+ el && el.scrollIntoView();
774
+ }
775
+ }
776
+
777
+ async componentDidMount() {
778
+ this.mounted = true;
779
+ this.loadFolders()
780
+ .catch(error => console.error(`Cannot load folders: ${error}`));
781
+
782
+ this.supportSubscribes = await this.props.socket.checkFeatureSupported('BINARY_STATE_EVENT');
783
+ this.supportSubscribes && (await this.props.socket.subscribeFiles('*', '*', this.onFileChange));
784
+ }
785
+
786
+ componentWillUnmount() {
787
+ this.supportSubscribes && this.props.socket.unsubscribeFiles('*', '*', this.onFileChange);
788
+ this.mounted = false;
789
+ this.browseList = null;
790
+ this.browseListRunning = false;
791
+ Object.values(this._tempTimeout)
792
+ .forEach(timer => timer && clearTimeout(timer));
793
+ this._tempTimeout = {};
794
+ }
795
+
796
+ browseFoldersCb(
797
+ foldersList: string[],
798
+ newFoldersNotNull: Folders,
799
+ cb: (folders: Folders) => void,
800
+ ): void {
801
+ if (!foldersList?.length) {
802
+ cb(newFoldersNotNull);
803
+ } else {
804
+ const folder = foldersList.shift();
805
+ if (folder) {
806
+ this.browseFolder(folder, newFoldersNotNull)
807
+ .catch((e: Error) => console.error(`Cannot read folder ${folder}: ${e}`))
808
+ .then(() => {
809
+ setTimeout(() => this.browseFoldersCb(foldersList, newFoldersNotNull, cb), 0);
810
+ });
811
+ } else {
812
+ setTimeout(() => this.browseFoldersCb(foldersList, newFoldersNotNull, cb), 0);
813
+ }
814
+ }
815
+ }
816
+
817
+ browseFolders(
818
+ foldersList: string[],
819
+ _newFolders?: Folders | null,
820
+ ): Promise<Folders> {
821
+ let newFoldersNotNull: Folders;
822
+ if (!_newFolders) {
823
+ newFoldersNotNull = {};
824
+ Object.keys(this.state.folders).forEach(folder => (newFoldersNotNull[folder] = this.state.folders[folder]));
825
+ } else {
826
+ newFoldersNotNull = _newFolders;
827
+ }
828
+
829
+ if (!foldersList?.length) {
830
+ return Promise.resolve(newFoldersNotNull);
831
+ }
832
+ return new Promise(resolve => {
833
+ this.browseFoldersCb(foldersList, newFoldersNotNull, resolve);
834
+ });
835
+ }
836
+
837
+ readDirSerial(adapter: string, relPath: string): Promise<ioBroker.ReadDirResult[]> {
838
+ return new Promise((resolve, reject) => {
839
+ if (this.browseList) {
840
+ // if component still mounted
841
+ this.browseList.push({
842
+ resolve: resolve as unknown as (files: ioBroker.ReadDirResult[]) => void,
843
+ reject,
844
+ adapter,
845
+ relPath,
846
+ });
847
+ !this.browseListRunning && this.processBrowseList();
848
+ }
849
+ });
850
+ }
851
+
852
+ processBrowseList(level: number = 0): void {
853
+ if (!this.browseListRunning && this.browseList && this.browseList.length) {
854
+ this.browseListRunning = true;
855
+ if (this.browseList.length > 10) {
856
+ // not too often
857
+ if (!(this.browseList.length % 10)) {
858
+ this.setState({ queueLength: this.browseList.length });
859
+ }
860
+ } else {
861
+ this.setState({ queueLength: this.browseList.length });
862
+ }
863
+
864
+ this.browseList[0].processing = true;
865
+ this.props.socket
866
+ .readDir(this.browseList[0].adapter, this.browseList[0].relPath as string)
867
+ .then(files => {
868
+ if (this.browseList) {
869
+ // if component still mounted
870
+ const item = this.browseList.shift();
871
+ if (item) {
872
+ const resolve = item.resolve;
873
+ item.resolve = null;
874
+ item.reject = null;
875
+ item.adapter = null;
876
+ item.relPath = null;
877
+ resolve && resolve(files);
878
+ this.browseListRunning = false;
879
+ if (this.browseList.length) {
880
+ if (level < 5) {
881
+ this.processBrowseList(level + 1);
882
+ } else {
883
+ setTimeout(() => this.processBrowseList(0), 0);
884
+ }
885
+ } else {
886
+ this.setState({ queueLength: 0 });
887
+ }
888
+ } else {
889
+ this.setState({ queueLength: 0 });
890
+ }
891
+ }
892
+ })
893
+ .catch(e => {
894
+ if (this.browseList) {
895
+ // if component still mounted
896
+ const item = this.browseList.shift();
897
+ if (item) {
898
+ const reject = item.reject;
899
+ item.resolve = null;
900
+ item.reject = null;
901
+ item.adapter = null;
902
+ item.relPath = null;
903
+ reject && reject(e);
904
+ this.browseListRunning = false;
905
+ if (this.browseList.length) {
906
+ if (level < 5) {
907
+ this.processBrowseList(level + 1);
908
+ } else {
909
+ setTimeout(() => this.processBrowseList(0), 0);
910
+ }
911
+ } else {
912
+ this.setState({ queueLength: 0 });
913
+ }
914
+ } else {
915
+ this.setState({ queueLength: 0 });
916
+ }
917
+ }
918
+ });
919
+ }
920
+ }
921
+
922
+ async browseFolder(
923
+ folderId: string,
924
+ _newFolders?: Folders | null,
925
+ _checkEmpty?: boolean,
926
+ force?: boolean,
927
+ ): Promise<Folders> {
928
+ let newFoldersNotNull: Folders;
929
+ if (!_newFolders) {
930
+ newFoldersNotNull = {};
931
+ Object.keys(this.state.folders).forEach(folder => {
932
+ newFoldersNotNull[folder] = this.state.folders[folder];
933
+ });
934
+ } else {
935
+ newFoldersNotNull = _newFolders;
936
+ }
937
+
938
+ if (newFoldersNotNull[folderId] && !force) {
939
+ if (!_checkEmpty) {
940
+ return new Promise((resolve, reject) => {
941
+ Promise.all(newFoldersNotNull[folderId].filter(item => item.folder).map(item =>
942
+ this.browseFolder(item.id, newFoldersNotNull, true)
943
+ .catch(() => undefined)))
944
+ .then(() => resolve(newFoldersNotNull))
945
+ .catch(error => reject(error));
946
+ });
947
+ }
948
+
949
+ return Promise.resolve(newFoldersNotNull);
950
+ }
951
+
952
+ // if root folder
953
+ if (!folderId || folderId === '/') {
954
+ try {
955
+ let objs = (await this.props.socket.readMetaItems()) as MetaObject[];
956
+ const _folders: FolderOrFileItem[] = [];
957
+ let userData = null;
958
+
959
+ if (this.state.restrictToFolder) {
960
+ const adapter = this.state.restrictToFolder.split('/')[0];
961
+ objs = objs.filter(obj => obj._id === adapter);
962
+ } else if (!this.state.expertMode) {
963
+ // load only adapter.admin and not other meta files like hm-rpc.0.devices.blablabla
964
+ objs = objs.filter(obj => !obj._id.endsWith('.admin'));
965
+ }
966
+
967
+ const pos = objs.findIndex(obj => obj._id === 'system.meta.uuid');
968
+ if (pos !== -1) {
969
+ objs.splice(pos, 1);
970
+ }
971
+
972
+ objs.forEach(obj => {
973
+ if (this.limitToObjectID && this.limitToObjectID !== obj._id) {
974
+ return;
975
+ }
976
+
977
+ const item: FolderOrFileItem = {
978
+ id: obj._id,
979
+ name: obj._id,
980
+ title: (obj.common && obj.common.name) || obj._id,
981
+ meta: true,
982
+ from: obj.from,
983
+ ts: obj.ts,
984
+ color: obj.common && obj.common.color,
985
+ icon: obj.common && obj.common.icon,
986
+ folder: true,
987
+ acl: obj.acl,
988
+ level: 0,
989
+ };
990
+
991
+ if (item.id === USER_DATA) {
992
+ // user data must be first
993
+ userData = item;
994
+ } else {
995
+ _folders.push(item);
996
+ }
997
+ });
998
+
999
+ _folders.sort((a, b) => (a.id > b.id ? 1 : (a.id < b.id ? -1 : 0)));
1000
+ if (!this.limitToObjectID || this.limitToObjectID === USER_DATA) {
1001
+ userData && _folders.unshift(userData);
1002
+ }
1003
+
1004
+ newFoldersNotNull[folderId || '/'] = _folders;
1005
+
1006
+ if (!_checkEmpty) {
1007
+ return Promise.all(_folders.filter(item => item.folder)
1008
+ .map(item =>
1009
+ this.browseFolder(item.id, newFoldersNotNull, true)
1010
+ .catch(() => undefined)))
1011
+ .then(() => newFoldersNotNull);
1012
+ }
1013
+ } catch (e) {
1014
+ this.initialReadFinished && window.alert(`Cannot read meta items: ${e}`);
1015
+ newFoldersNotNull[folderId || '/'] = [];
1016
+ }
1017
+ return newFoldersNotNull;
1018
+ }
1019
+
1020
+ const parts = folderId.split('/');
1021
+ const level = parts.length;
1022
+ const adapter = parts.shift();
1023
+ const relPath = parts.join('/');
1024
+
1025
+ // make all requests here serial
1026
+ const files = await this.readDirSerial(adapter || '', relPath);
1027
+ try {
1028
+ const _folders: FolderOrFileItem[] = [];
1029
+
1030
+ files.forEach(file => {
1031
+ const item: FolderOrFileItem = {
1032
+ id: `${folderId}/${file.file}`,
1033
+ ext: Utils.getFileExtension(file.file),
1034
+ folder: file.isDir,
1035
+ name: file.file,
1036
+ size: file.stats?.size,
1037
+ modified: file.modifiedAt,
1038
+ acl: file.acl,
1039
+ level,
1040
+ };
1041
+
1042
+ if (this.state.restrictToFolder) {
1043
+ if (
1044
+ item.folder &&
1045
+ (item.id.startsWith(`${this.state.restrictToFolder}/`) ||
1046
+ item.id === this.state.restrictToFolder ||
1047
+ this.state.restrictToFolder.startsWith(`${item.id}/`))
1048
+ ) {
1049
+ _folders.push(item);
1050
+ } else if (item.id.startsWith(`${this.state.restrictToFolder}/`)) {
1051
+ _folders.push(item);
1052
+ }
1053
+ } else if (this.limitToPath) {
1054
+ if (
1055
+ item.folder &&
1056
+ (item.id.startsWith(`${this.limitToPath}/`) ||
1057
+ item.id === this.limitToPath ||
1058
+ this.limitToPath.startsWith(`${item.id}/`))
1059
+ ) {
1060
+ _folders.push(item);
1061
+ } else if (item.id.startsWith(`${this.limitToPath}/`)) {
1062
+ _folders.push(item);
1063
+ }
1064
+ } else {
1065
+ _folders.push(item);
1066
+ }
1067
+ });
1068
+
1069
+ _folders.sort(sortFolders);
1070
+ newFoldersNotNull[folderId] = _folders;
1071
+
1072
+ if (!_checkEmpty) {
1073
+ return Promise.all(_folders
1074
+ .filter(item => item.folder)
1075
+ .map(item => this.browseFolder(item.id, newFoldersNotNull, true)))
1076
+ .then(() => newFoldersNotNull);
1077
+ }
1078
+ } catch (e) {
1079
+ this.initialReadFinished && window.alert(`Cannot read ${adapter}${relPath ? `/${relPath}` : ''}: ${e}`);
1080
+ newFoldersNotNull[folderId] = [];
1081
+ }
1082
+
1083
+ return newFoldersNotNull;
1084
+ }
1085
+
1086
+ toggleFolder(item: FolderOrFileItem, e: React.MouseEvent<Element>) {
1087
+ e?.stopPropagation();
1088
+ const expanded = [...this.state.expanded];
1089
+ const pos = expanded.indexOf(item.id);
1090
+ if (pos === -1) {
1091
+ expanded.push(item.id);
1092
+ expanded.sort();
1093
+
1094
+ this.localStorage.setItem('files.expanded', JSON.stringify(expanded));
1095
+
1096
+ if (!item.temp) {
1097
+ this.browseFolder(item.id)
1098
+ .then(folders => this.setState({ expanded, folders }))
1099
+ .catch(err => window.alert(
1100
+ err === NOT_FOUND
1101
+ ? this.props.t('ra_Cannot find "%s"', item.id)
1102
+ : this.props.t('ra_Cannot read "%s"', item.id),
1103
+ ));
1104
+ } else {
1105
+ this.setState({ expanded });
1106
+ }
1107
+ } else {
1108
+ expanded.splice(pos, 1);
1109
+ this.localStorage.setItem('files.expanded', JSON.stringify(expanded));
1110
+ this.setState({ expanded });
1111
+ }
1112
+ }
1113
+
1114
+ onFileChange = (id: string, fileName: string, size: number | null) => {
1115
+ const key = `${id}/${fileName}`;
1116
+ const pos = key.lastIndexOf('/');
1117
+ const folder = key.substring(0, pos);
1118
+ console.log(`File changed ${key}[${size}]`);
1119
+
1120
+ if (this.state.folders[folder]) {
1121
+ this._tempTimeout[folder] && clearTimeout(this._tempTimeout[folder]);
1122
+
1123
+ this._tempTimeout[folder] = setTimeout(() => {
1124
+ delete this._tempTimeout[folder];
1125
+
1126
+ this.browseFolder(folder, null, false, true)
1127
+ .then(folders => this.setState({ folders }));
1128
+ }, 300);
1129
+ }
1130
+ };
1131
+
1132
+ changeFolder(e: React.MouseEvent<HTMLDivElement>, folder?: string) {
1133
+ e && e.stopPropagation();
1134
+
1135
+ this.lastSelect = Date.now();
1136
+
1137
+ let _folder = folder || getParentDir(this.state.currentDir);
1138
+
1139
+ if (_folder === '/') {
1140
+ _folder = '';
1141
+ }
1142
+
1143
+ this.localStorage.setItem('files.currentDir', _folder);
1144
+
1145
+ if (folder && e && (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey)) {
1146
+ return this.setState({ selected: _folder });
1147
+ }
1148
+
1149
+ if (_folder && !this.state.folders[_folder]) {
1150
+ return this.browseFolder(_folder)
1151
+ .then(folders => this.setState(
1152
+ {
1153
+ folders,
1154
+ path: _folder,
1155
+ currentDir: _folder,
1156
+ selected: _folder,
1157
+ pathFocus: false,
1158
+ },
1159
+ () => this.props.onSelect && this.props.onSelect(''),
1160
+ ));
1161
+ }
1162
+
1163
+ return this.setState(
1164
+ {
1165
+ currentDir: _folder,
1166
+ selected: _folder,
1167
+ path: _folder,
1168
+ pathFocus: false,
1169
+ },
1170
+ () => this.props.onSelect && this.props.onSelect(''),
1171
+ );
1172
+ }
1173
+
1174
+ select(id: string, e?: React.MouseEvent<HTMLDivElement> | null, cb?: () => void) {
1175
+ e && e.stopPropagation();
1176
+ this.lastSelect = Date.now();
1177
+
1178
+ this.localStorage.setItem('files.selected', id);
1179
+
1180
+ this.setState({ selected: id, path: id, pathFocus: false }, () => {
1181
+ if (this.props.onSelect) {
1182
+ const ext = Utils.getFileExtension(id);
1183
+ if (
1184
+ (!this.props.filterFiles || (ext && this.props.filterFiles.includes(ext))) &&
1185
+ (!this.state.filterByType ||
1186
+ (ext && (EXTENSIONS as Record<string, string[]>)[this.state.filterByType].includes(ext)))
1187
+ ) {
1188
+ this.props.onSelect(id, false, !!this.state.folders[id]);
1189
+ } else {
1190
+ this.props.onSelect('');
1191
+ }
1192
+ }
1193
+ cb && cb();
1194
+ });
1195
+ }
1196
+
1197
+ getText(text?: ioBroker.StringOrTranslated | null): string | undefined {
1198
+ if (text) {
1199
+ if (typeof text === 'object') {
1200
+ return text[this.props.lang] || text.en || undefined;
1201
+ }
1202
+ return text;
1203
+ }
1204
+ return undefined;
1205
+ }
1206
+
1207
+ renderFolder(item: FolderOrFileItem, expanded?: boolean): React.JSX.Element | null {
1208
+ if (
1209
+ this.state.viewType === TABLE &&
1210
+ this.state.filterEmpty &&
1211
+ (!this.state.folders[item.id] || !this.state.folders[item.id].length) &&
1212
+ item.id !== USER_DATA &&
1213
+ !item.temp
1214
+ ) {
1215
+ return null;
1216
+ }
1217
+ const IconEl = expanded ? IconOpen : IconClosed;
1218
+ const padding = this.state.viewType === TABLE ? item.level * this.levelPadding : 0;
1219
+ const isUserData = item.name === USER_DATA;
1220
+ const isSpecialData = isUserData || item.name === 'vis.0' || item.name === 'vis-2.0';
1221
+
1222
+ return <Box
1223
+ component="div"
1224
+ key={item.id}
1225
+ id={item.id}
1226
+ style={this.state.viewType === TABLE ? { marginLeft: padding, width: `calc(100% - ${padding}px` } : {}}
1227
+ onClick={e => (this.state.viewType === TABLE ? this.select(item.id, e) : this.changeFolder(e, item.id))}
1228
+ onDoubleClick={e => this.state.viewType === TABLE && this.toggleFolder(item, e)}
1229
+ title={this.getText(item.title)}
1230
+ className="browserItem"
1231
+ sx={Utils.getStyle(
1232
+ this.props.theme,
1233
+ styles[`item${this.state.viewType}`],
1234
+ styles[`itemFolder${this.state.viewType}`],
1235
+ this.state.selected === item.id ? styles.itemSelected : {},
1236
+ item.temp ? styles.itemFolderTemp : {},
1237
+ )}
1238
+ >
1239
+ <IconEl
1240
+ style={Utils.getStyle(this.props.theme, styles[`itemFolderIcon${this.state.viewType}`], isSpecialData && styles.specialFolder)}
1241
+ onClick={this.state.viewType === TABLE ? (e: React.MouseEvent<Element>) => this.toggleFolder(item, e) : undefined}
1242
+ />
1243
+
1244
+ <Box
1245
+ component="div"
1246
+ sx={Utils.getStyle(
1247
+ this.props.theme,
1248
+ styles[`itemName${this.state.viewType}`],
1249
+ styles[`itemNameFolder${this.state.viewType}`],
1250
+ )}
1251
+ >
1252
+ {isUserData ? this.props.t('ra_User files') : item.name}
1253
+ </Box>
1254
+
1255
+ <Hidden smDown>
1256
+ <div style={styles[`itemSize${this.state.viewType}`]}>
1257
+ {this.state.viewType === TABLE && this.state.folders[item.id]
1258
+ ? this.state.folders[item.id].length
1259
+ : ''}
1260
+ </div>
1261
+ </Hidden>
1262
+
1263
+ <Hidden smDown>
1264
+ {this.state.viewType === TABLE && this.props.expertMode ? this.formatAcl(item.acl) : null}
1265
+ </Hidden>
1266
+
1267
+ <Hidden smDown>
1268
+ {this.state.viewType === TABLE && this.props.expertMode ?
1269
+ <Box component="div" sx={styles[`itemDeleteButton${this.state.viewType}`]} /> : null}
1270
+ </Hidden>
1271
+ {this.state.viewType === TABLE && this.props.allowDownload ?
1272
+ <div style={styles[`itemDownloadEmpty${this.state.viewType}`]} /> : null}
1273
+
1274
+ {this.state.viewType === TABLE &&
1275
+ this.props.allowDelete &&
1276
+ this.state.folders[item.id] &&
1277
+ this.state.folders[item.id].length ? <IconButton
1278
+ aria-label="delete"
1279
+ onClick={e => {
1280
+ e.stopPropagation();
1281
+ if (this.suppressDeleteConfirm > Date.now()) {
1282
+ this.deleteItem(item.id);
1283
+ } else {
1284
+ this.setState({ deleteItem: item.id });
1285
+ }
1286
+ }}
1287
+ sx={styles[`itemDeleteButton${this.state.viewType}`]}
1288
+ size="large"
1289
+ >
1290
+ <DeleteIcon fontSize="small" />
1291
+ </IconButton> :
1292
+ this.state.viewType === TABLE && this.props.allowDelete ?
1293
+ <Box component="div" sx={styles[`itemDeleteButton${this.state.viewType}`]} /> : null}
1294
+ </Box>;
1295
+ }
1296
+
1297
+ renderBackFolder() {
1298
+ return <Box
1299
+ component="div"
1300
+ key={this.state.currentDir}
1301
+ id={this.state.currentDir}
1302
+ onClick={e => this.changeFolder(e)}
1303
+ title={this.props.t('ra_Back to %s', getParentDir(this.state.currentDir))}
1304
+ className="browserItem"
1305
+ sx={Utils.getStyle(
1306
+ this.props.theme,
1307
+ styles[`item${this.state.viewType}`],
1308
+ styles[`itemFolder${this.state.viewType}`],
1309
+ )}
1310
+ >
1311
+ <IconClosed style={styles[`itemFolderIcon${this.state.viewType}`]} />
1312
+ <IconBack sx={styles.itemFolderIconBack} />
1313
+
1314
+ <Box
1315
+ component="div"
1316
+ sx={Utils.getStyle(
1317
+ this.props.theme,
1318
+ styles[`itemName${this.state.viewType}`],
1319
+ styles[`itemNameFolder${this.state.viewType}`],
1320
+ )}
1321
+ >
1322
+ ..
1323
+ </Box>
1324
+ </Box>;
1325
+ }
1326
+
1327
+ formatSize(size: number | null | undefined) {
1328
+ return <div style={styles[`itemSize${this.state.viewType}`]}>
1329
+ {size || size === 0 ? Utils.formatBytes(size) : ''}
1330
+ </div>;
1331
+ }
1332
+
1333
+ formatAcl(acl: ioBroker.EvaluatedFileACL | MetaACL | undefined) {
1334
+ const access: number = acl ? ((acl as ioBroker.EvaluatedFileACL).permissions || (acl as MetaACL).file) : 0;
1335
+ let accessStr: string;
1336
+ if (access) {
1337
+ accessStr = access.toString(16).padStart(3, '0');
1338
+ } else {
1339
+ accessStr = '';
1340
+ }
1341
+
1342
+ return <div style={styles[`itemAccess${this.state.viewType}`]}>
1343
+ {this.props.modalEditOfAccessControl ? <IconButton
1344
+ size="large"
1345
+ onClick={() => this.setState({ modalEditOfAccess: true })}
1346
+ sx={styles[`itemAclButton${this.state.viewType}`]}
1347
+ >
1348
+ {accessStr || '---'}
1349
+ </IconButton> : accessStr || '---'}
1350
+ </div>;
1351
+ }
1352
+
1353
+ getFileIcon(ext: string | null) {
1354
+ switch (ext) {
1355
+ case 'json':
1356
+ case 'json5':
1357
+ return <JsonIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1358
+
1359
+ case 'css':
1360
+ return <CssIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1361
+
1362
+ case 'js':
1363
+ case 'ts':
1364
+ return <JSIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1365
+
1366
+ case 'html':
1367
+ case 'md':
1368
+ return <HtmlIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1369
+
1370
+ case 'mp3':
1371
+ case 'ogg':
1372
+ case 'wav':
1373
+ case 'm4a':
1374
+ case 'mp4':
1375
+ case 'flac':
1376
+ return <MusicIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1377
+
1378
+ default:
1379
+ return <FileIcon style={styles[`itemIcon${this.state.viewType}`]} />;
1380
+ }
1381
+ }
1382
+
1383
+ static getEditFile(ext: string | null): boolean {
1384
+ switch (ext) {
1385
+ case 'json':
1386
+ case 'json5':
1387
+ case 'js':
1388
+ case 'html':
1389
+ case 'txt':
1390
+ case 'css':
1391
+ case 'log':
1392
+ return true;
1393
+ default:
1394
+ return false;
1395
+ }
1396
+ }
1397
+
1398
+ setStateBackgroundImage = () => {
1399
+ const array = ['light', 'dark', 'colored', 'delete'];
1400
+ this.setState(({ backgroundImage }) => {
1401
+ if (backgroundImage && array.indexOf(backgroundImage) !== -1 && array.length - 1 !== array.indexOf(backgroundImage)) {
1402
+ this.localStorage.setItem(
1403
+ 'files.backgroundImage',
1404
+ array[array.indexOf(backgroundImage) + 1],
1405
+ );
1406
+ return { backgroundImage: array[array.indexOf(backgroundImage) + 1] };
1407
+ }
1408
+ this.localStorage.setItem('files.backgroundImage', array[0]);
1409
+ return { backgroundImage: array[0] };
1410
+ });
1411
+ };
1412
+
1413
+ getStyleBackgroundImage = () => {
1414
+ // ['light', 'dark', 'colored', 'delete']
1415
+ switch (this.state.backgroundImage) {
1416
+ case 'light':
1417
+ return styles.backgroundImageLight;
1418
+ case 'dark':
1419
+ return styles.backgroundImageDark;
1420
+ case 'colored':
1421
+ return styles.backgroundImageColored;
1422
+ case 'delete':
1423
+ return null;
1424
+ default:
1425
+ return null;
1426
+ }
1427
+ };
1428
+
1429
+ renderFile(item: FolderOrFileItem): React.JSX.Element {
1430
+ const padding = this.state.viewType === TABLE ? item.level * this.levelPadding : 0;
1431
+ const ext = Utils.getFileExtension(item.name);
1432
+
1433
+ return <Box
1434
+ component="div"
1435
+ key={item.id}
1436
+ id={item.id}
1437
+ onDoubleClick={e => {
1438
+ e.stopPropagation();
1439
+ if (!this.props.onSelect) {
1440
+ this.setState({ viewer: this.imagePrefix + item.id, formatEditFile: ext });
1441
+ } else if (
1442
+ (!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) &&
1443
+ (!this.state.filterByType ||
1444
+ (item.ext &&
1445
+ (EXTENSIONS as Record<string, string[]>)[this.state.filterByType].includes(item.ext)))
1446
+ ) {
1447
+ this.props.onSelect(item.id, true, !!this.state.folders[item.id]);
1448
+ }
1449
+ }}
1450
+ onClick={e => this.select(item.id, e)}
1451
+ style={this.state.viewType === TABLE ? { marginLeft: padding, width: `calc(100% - ${padding}px)` } : {}}
1452
+ className="browserItem"
1453
+ sx={Utils.getStyle(
1454
+ this.props.theme,
1455
+ styles[`item${this.state.viewType}`],
1456
+ styles[`itemFile${this.state.viewType}`],
1457
+ this.state.selected === item.id ? styles.itemSelected : undefined,
1458
+ )}
1459
+ >
1460
+ {ext && EXTENSIONS.images.includes(ext) ?
1461
+ this.state.fileErrors.includes(item.id) ?
1462
+ <IconNoIcon
1463
+ style={{
1464
+ ...styles[`itemImage${this.state.viewType}`],
1465
+ ...this.getStyleBackgroundImage(),
1466
+ ...styles[`itemNoImage${this.state.viewType}`],
1467
+ }}
1468
+ /> :
1469
+ <Icon
1470
+ onError={e => {
1471
+ (e.target as HTMLImageElement).onerror = null;
1472
+ const fileErrors = [...this.state.fileErrors];
1473
+ if (!fileErrors.includes(item.id)) {
1474
+ fileErrors.push(item.id);
1475
+ this.setState({ fileErrors });
1476
+ }
1477
+ }}
1478
+ style={{ ...styles[`itemImage${this.state.viewType}`], ...this.getStyleBackgroundImage() }}
1479
+ src={this.imagePrefix + item.id}
1480
+ alt={item.name}
1481
+ />
1482
+ :
1483
+ this.getFileIcon(ext)}
1484
+ <Box component="div" sx={styles[`itemName${this.state.viewType}`]}>{item.name}</Box>
1485
+ <Hidden smDown>{this.formatSize(item.size)}</Hidden>
1486
+ <Hidden smDown>
1487
+ {this.state.viewType === TABLE && this.props.expertMode ? this.formatAcl(item.acl) : null}
1488
+ </Hidden>
1489
+ <Hidden smDown>
1490
+ {this.state.viewType === TABLE && this.props.expertMode && FileBrowserClass.getEditFile(ext) ?
1491
+ <IconButton
1492
+ aria-label="edit"
1493
+ onClick={e => {
1494
+ e.stopPropagation();
1495
+ if (!this.props.onSelect) {
1496
+ this.setState({ viewer: this.imagePrefix + item.id, formatEditFile: ext });
1497
+ } else if (
1498
+ (!this.props.filterFiles ||
1499
+ (item.ext && this.props.filterFiles.includes(item.ext))) &&
1500
+ (!this.state.filterByType ||
1501
+ (item.ext &&
1502
+ (EXTENSIONS as Record<string, string[]>)[this.state.filterByType].includes(
1503
+ item.ext,
1504
+ )))
1505
+ ) {
1506
+ this.props.onSelect(item.id, true, !!this.state.folders[item.id]);
1507
+ }
1508
+ }}
1509
+ sx={styles[`itemDeleteButton${this.state.viewType}`]}
1510
+ size="large"
1511
+ >
1512
+ <EditIcon fontSize="small" />
1513
+ </IconButton>
1514
+ :
1515
+ <Box component="div" sx={styles[`itemDeleteButton${this.state.viewType}`]} />}
1516
+ </Hidden>
1517
+ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
1518
+ {this.state.viewType === TABLE && this.props.allowDownload ? <Box
1519
+ component="a"
1520
+ className="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeLarge"
1521
+ sx={styles.itemDownloadButtonTable}
1522
+ tabIndex={0}
1523
+ download={item.id}
1524
+ href={this.imagePrefix + item.id}
1525
+ onClick={e => e.stopPropagation()}
1526
+ >
1527
+ <DownloadIcon />
1528
+ </Box> : null}
1529
+
1530
+ {this.state.viewType === TABLE &&
1531
+ this.props.allowDelete &&
1532
+ item.id !== 'vis.0/' &&
1533
+ item.id !== 'vis-2.0/' &&
1534
+ item.id !== USER_DATA ? <IconButton
1535
+ aria-label="delete"
1536
+ onClick={e => {
1537
+ e.stopPropagation();
1538
+ if (this.suppressDeleteConfirm > Date.now()) {
1539
+ this.deleteItem(item.id);
1540
+ } else {
1541
+ this.setState({ deleteItem: item.id });
1542
+ }
1543
+ }}
1544
+ sx={styles[`itemDeleteButton${this.state.viewType}`]}
1545
+ size="large"
1546
+ >
1547
+ <DeleteIcon fontSize="small" />
1548
+ </IconButton>
1549
+ :
1550
+ (this.state.viewType === TABLE && this.props.allowDelete ?
1551
+ <Box component="div" sx={styles[`itemDeleteButton${this.state.viewType}`]} /> : null)}
1552
+ </Box>;
1553
+ }
1554
+
1555
+ renderItems(folderId: string): React.JSX.Element | (React.JSX.Element | null)[] {
1556
+ if (this.state.folders && this.state.folders[folderId]) {
1557
+ // tile
1558
+ if (this.state.viewType === TILE) {
1559
+ const res: (React.JSX.Element | null)[] = [];
1560
+ if (folderId && folderId !== '/') {
1561
+ res.push(this.renderBackFolder());
1562
+ }
1563
+ this.state.folders[folderId].forEach(item => {
1564
+ if (item.folder) {
1565
+ res.push(this.renderFolder(item));
1566
+ } else if (
1567
+ (!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) &&
1568
+ (!this.state.filterByType ||
1569
+ (item.ext &&
1570
+ (EXTENSIONS as Record<string, string[]>)[this.state.filterByType].includes(item.ext)))
1571
+ ) {
1572
+ res.push(this.renderFile(item));
1573
+ }
1574
+ });
1575
+ return res;
1576
+ }
1577
+
1578
+ const totalResult: (React.JSX.Element | null)[] = [];
1579
+ this.state.folders[folderId].forEach(item => {
1580
+ if (item.folder) {
1581
+ const expanded = this.state.expanded.includes(item.id);
1582
+
1583
+ const folders = this.renderFolder(item, expanded);
1584
+ if (Array.isArray(folders)) {
1585
+ folders.forEach(folder => totalResult.push(folder));
1586
+ } else {
1587
+ totalResult.push(folders);
1588
+ }
1589
+ if (this.state.folders[item.id] && expanded) {
1590
+ const items = this.renderItems(item.id);
1591
+ if (Array.isArray(items)) {
1592
+ items.forEach(_item => totalResult.push(_item));
1593
+ } else {
1594
+ totalResult.push(items);
1595
+ }
1596
+ }
1597
+ } else if (
1598
+ (!this.props.filterFiles || (item.ext && this.props.filterFiles.includes(item.ext))) &&
1599
+ (!this.state.filterByType ||
1600
+ (item.ext &&
1601
+ (EXTENSIONS as Record<string, string[]>)[this.state.filterByType].includes(item.ext)))
1602
+ ) {
1603
+ totalResult.push(this.renderFile(item));
1604
+ }
1605
+ });
1606
+
1607
+ return totalResult;
1608
+ }
1609
+
1610
+ return <div style={{ position: 'relative' }}>
1611
+ <CircularProgress key={folderId} color="secondary" size={24} />
1612
+ <div
1613
+ style={{
1614
+ position: 'absolute',
1615
+ zIndex: 2,
1616
+ top: 4,
1617
+ width: 24,
1618
+ textAlign: 'center',
1619
+ }}
1620
+ >
1621
+ {this.state.queueLength}
1622
+ </div>
1623
+ </div>;
1624
+ }
1625
+
1626
+ renderToolbar() {
1627
+ const IconType: React.FC< { fontSize?: 'small' }> | null = this.props.showTypeSelector
1628
+ ? FILE_TYPE_ICONS[this.state.filterByType || 'all'] ||
1629
+ FILE_TYPE_ICONS.all
1630
+ : null;
1631
+
1632
+ const isInFolder = this.findFirstFolder(this.state.selected);
1633
+
1634
+ return <Toolbar key="toolbar" variant="dense">
1635
+ {this.props.allowNonRestricted && this.props.restrictToFolder ? <IconButton
1636
+ edge="start"
1637
+ title={
1638
+ this.state.restrictToFolder
1639
+ ? this.props.t('ra_Show all folders')
1640
+ : this.props.t('ra_Restrict to folder')
1641
+ }
1642
+ style={{
1643
+ ...styles.menuButton,
1644
+ ...(this.state.restrictToFolder ? styles.menuButtonRestrictActive : undefined),
1645
+ }}
1646
+ aria-label="restricted to folder"
1647
+ onClick={() =>
1648
+ this.setState({
1649
+ restrictToFolder:
1650
+ (this.state.restrictToFolder ? '' : this.props.restrictToFolder) || '',
1651
+ loadAllFolders: true,
1652
+ })}
1653
+ size="small"
1654
+ >
1655
+ <RestrictedIcon fontSize="small" />
1656
+ </IconButton> : null}
1657
+ {this.props.showExpertButton ? <IconButton
1658
+ edge="start"
1659
+ title={this.props.t('ra_Toggle expert mode')}
1660
+ style={{
1661
+ ...styles.menuButton,
1662
+ ...(this.state.expertMode ? styles.menuButtonExpertActive : undefined),
1663
+ }}
1664
+ aria-label="expert mode"
1665
+ onClick={() => this.setState({ expertMode: !this.state.expertMode })}
1666
+ size="small"
1667
+ >
1668
+ <IconExpert />
1669
+ </IconButton> : null}
1670
+ {this.props.showViewTypeButton ? <IconButton
1671
+ edge="start"
1672
+ title={this.props.t('ra_Toggle view mode')}
1673
+ style={styles.menuButton}
1674
+ aria-label="view mode"
1675
+ onClick={() => {
1676
+ const viewType = this.state.viewType === TABLE ? TILE : TABLE;
1677
+ this.localStorage.setItem('files.viewType', viewType);
1678
+ let currentDir = this.state.selected;
1679
+ if (isFile(currentDir)) {
1680
+ currentDir = getParentDir(currentDir);
1681
+ }
1682
+ this.setState({ viewType, currentDir }, () => {
1683
+ if (this.state.viewType === TABLE) {
1684
+ this.scrollToSelected();
1685
+ }
1686
+ });
1687
+ }}
1688
+ size="small"
1689
+ >
1690
+ {this.state.viewType !== TABLE ? <IconList fontSize="small" /> : <IconTile fontSize="small" />}
1691
+ </IconButton> : null}
1692
+ <IconButton
1693
+ edge="start"
1694
+ title={this.props.t('ra_Hide empty folders')}
1695
+ style={styles.menuButton}
1696
+ color={this.state.filterEmpty ? 'secondary' : 'inherit'}
1697
+ aria-label="filter empty"
1698
+ onClick={() => {
1699
+ this.localStorage.setItem(
1700
+ 'file.empty',
1701
+ this.state.filterEmpty ? 'false' : 'true',
1702
+ );
1703
+ this.setState({ filterEmpty: !this.state.filterEmpty });
1704
+ }}
1705
+ size="small"
1706
+ >
1707
+ <EmptyFilterIcon fontSize="small" />
1708
+ </IconButton>
1709
+ <IconButton
1710
+ edge="start"
1711
+ title={this.props.t('ra_Reload files')}
1712
+ style={styles.menuButton}
1713
+ color="inherit"
1714
+ aria-label="reload files"
1715
+ onClick={() => this.setState({ folders: {} }, () => this.loadFolders())}
1716
+ size="small"
1717
+ >
1718
+ <RefreshIcon fontSize="small" />
1719
+ </IconButton>
1720
+ {this.props.allowCreateFolder ? <IconButton
1721
+ edge="start"
1722
+ disabled={
1723
+ !this.state.selected ||
1724
+ !isInFolder ||
1725
+ (!!this.limitToPath &&
1726
+ !this.state.selected.startsWith(`${this.limitToPath}/`) &&
1727
+ this.limitToPath !== this.state.selected)
1728
+ }
1729
+ title={this.props.t('ra_Create folder')}
1730
+ style={styles.menuButton}
1731
+ color="inherit"
1732
+ aria-label="add folder"
1733
+ onClick={() => this.setState({ addFolder: true })}
1734
+ size="small"
1735
+ >
1736
+ <AddFolderIcon fontSize="small" />
1737
+ </IconButton> : null}
1738
+ {this.props.allowUpload ? <IconButton
1739
+ edge="start"
1740
+ disabled={
1741
+ !this.state.selected ||
1742
+ !isInFolder ||
1743
+ (!!this.limitToPath &&
1744
+ !this.state.selected.startsWith(`${this.limitToPath}/`) &&
1745
+ this.limitToPath !== this.state.selected)
1746
+ }
1747
+ title={this.props.t('ra_Upload file')}
1748
+ style={styles.menuButton}
1749
+ color="inherit"
1750
+ aria-label="upload file"
1751
+ onClick={() => this.setState({ uploadFile: true })}
1752
+ size="small"
1753
+ >
1754
+ <UploadIcon fontSize="small" />
1755
+ </IconButton> : null}
1756
+ {this.props.showTypeSelector && IconType ? <Tooltip title={this.props.t('ra_Filter files')} componentsProps={{ popper: { sx: styles.tooltip } }}>
1757
+ <IconButton size="small" onClick={e => this.setState({ showTypesMenu: e.target as HTMLButtonElement })}>
1758
+ <IconType fontSize="small" />
1759
+ </IconButton>
1760
+ </Tooltip> : null}
1761
+ {this.state.showTypesMenu ? <Menu
1762
+ open={!0}
1763
+ anchorEl={this.state.showTypesMenu}
1764
+ onClose={() => this.setState({ showTypesMenu: null })}
1765
+ >
1766
+ {Object.keys(FILE_TYPE_ICONS).map(type => {
1767
+ const MyIcon: React.FC<{ fontSize?: 'small' }> = FILE_TYPE_ICONS[type];
1768
+ return <MenuItem
1769
+ key={type}
1770
+ selected={this.state.filterByType === type}
1771
+ onClick={() => {
1772
+ if (type === 'all') {
1773
+ this.localStorage.removeItem(
1774
+ 'files.filterByType',
1775
+ );
1776
+ this.setState({ filterByType: '', showTypesMenu: null });
1777
+ } else {
1778
+ this.localStorage.setItem(
1779
+ 'files.filterByType',
1780
+ type,
1781
+ );
1782
+ this.setState({ filterByType: type, showTypesMenu: null });
1783
+ }
1784
+ }}
1785
+ >
1786
+ <ListItemIcon>
1787
+ <MyIcon fontSize="small" />
1788
+ </ListItemIcon>
1789
+ <ListItemText>{this.props.t(`ra_fileType_${type}`)}</ListItemText>
1790
+ </MenuItem>;
1791
+ })}
1792
+ </Menu> : null}
1793
+ <Tooltip title={this.props.t('ra_Background image')} componentsProps={{ popper: { sx: styles.tooltip } }}>
1794
+ <IconButton
1795
+ color="inherit"
1796
+ edge="start"
1797
+ style={styles.menuButton}
1798
+ onClick={this.setStateBackgroundImage}
1799
+ size="small"
1800
+ >
1801
+ <Brightness5Icon fontSize="small" />
1802
+ </IconButton>
1803
+ </Tooltip>
1804
+ {this.state.viewType !== TABLE && this.props.allowDelete ? <Tooltip
1805
+ title={this.props.t('ra_Delete')}
1806
+ componentsProps={{ popper: { sx: styles.tooltip } }}
1807
+ >
1808
+ <span>
1809
+ <IconButton
1810
+ aria-label="delete"
1811
+ disabled={
1812
+ !this.state.selected ||
1813
+ this.state.selected === 'vis.0/' ||
1814
+ this.state.selected === 'vis-2.0/' ||
1815
+ this.state.selected === USER_DATA
1816
+ }
1817
+ color="inherit"
1818
+ edge="start"
1819
+ style={styles.menuButton}
1820
+ onClick={e => {
1821
+ e.stopPropagation();
1822
+ if (this.suppressDeleteConfirm > Date.now()) {
1823
+ this.deleteItem(this.state.selected);
1824
+ } else {
1825
+ this.setState({ deleteItem: this.state.selected });
1826
+ }
1827
+ }}
1828
+ size="small"
1829
+ >
1830
+ <DeleteIcon fontSize="small" />
1831
+ </IconButton>
1832
+ </span>
1833
+ </Tooltip> : null}
1834
+ </Toolbar>;
1835
+ }
1836
+
1837
+ findItem(
1838
+ id: string,
1839
+ folders?: Folders | null,
1840
+ ) {
1841
+ folders = folders || this.state.folders;
1842
+ if (!folders) {
1843
+ return null;
1844
+ }
1845
+ const parts = id.split('/');
1846
+ parts.pop();
1847
+ const parentFolder = parts.join('/') || '/';
1848
+ if (!folders[parentFolder]) {
1849
+ return null;
1850
+ }
1851
+ return folders[parentFolder].find(item => item.id === id);
1852
+ }
1853
+
1854
+ renderInputDialog() {
1855
+ if (this.state.addFolder) {
1856
+ const parentFolder = this.findFirstFolder(this.state.selected);
1857
+
1858
+ if (!parentFolder) {
1859
+ window.alert(this.props.t('ra_Invalid parent folder!'));
1860
+ return null;
1861
+ }
1862
+
1863
+ return <TextInputDialog
1864
+ key="inputDialog"
1865
+ applyText={this.props.t('ra_Create')}
1866
+ cancelText={this.props.t('ra_Cancel')}
1867
+ titleText={this.props.t('ra_Create new folder in %s', this.state.selected)}
1868
+ promptText={this.props.t(
1869
+ 'ra_If no file will be created in the folder, it will disappear after the browser closed',
1870
+ )}
1871
+ labelText={this.props.t('ra_Folder name')}
1872
+ verify={(text: string) =>
1873
+ (this.state.folders[parentFolder].find(item => item.name === text)
1874
+ ? ''
1875
+ : this.props.t('ra_Duplicate name'))}
1876
+ onClose={(name: string | null) => {
1877
+ if (name) {
1878
+ const folders: Folders = {};
1879
+ Object.keys(this.state.folders).forEach(
1880
+ folder => (folders[folder] = this.state.folders[folder]),
1881
+ );
1882
+ const parent = this.findItem(parentFolder);
1883
+ const id = `${parentFolder}/${name}`;
1884
+ folders[parentFolder].push({
1885
+ id,
1886
+ level: (parent?.level || 0) + 1,
1887
+ name,
1888
+ folder: true,
1889
+ temp: true,
1890
+ });
1891
+
1892
+ folders[parentFolder].sort(sortFolders);
1893
+
1894
+ folders[id] = [];
1895
+ const expanded = [...this.state.expanded];
1896
+ if (!expanded.includes(parentFolder)) {
1897
+ expanded.push(parentFolder);
1898
+ expanded.sort();
1899
+ }
1900
+ this.localStorage.setItem(
1901
+ 'files.expanded',
1902
+ JSON.stringify(expanded),
1903
+ );
1904
+ this.setState({ addFolder: false, folders, expanded }, () => this.select(id));
1905
+ } else {
1906
+ this.setState({ addFolder: false });
1907
+ }
1908
+ }}
1909
+ replace={(text: string) => text.replace(/[^-_\w]/, '_')}
1910
+ />;
1911
+ }
1912
+ return null;
1913
+ }
1914
+
1915
+ componentDidUpdate(/* prevProps , prevState, snapshot */) {
1916
+ this.setOpacityTimer && clearTimeout(this.setOpacityTimer);
1917
+ this.setOpacityTimer = setTimeout(() => {
1918
+ this.setOpacityTimer = null;
1919
+ const items = window.document.getElementsByClassName('browserItem');
1920
+ for (let i = 0; i < items.length; i++) {
1921
+ (items[i] as HTMLElement).style.opacity = '1';
1922
+ }
1923
+ }, 100);
1924
+ }
1925
+
1926
+ findFirstFolder(id: string) {
1927
+ let parentFolder = id;
1928
+ const item = this.findItem(parentFolder);
1929
+ // find folder
1930
+ if (item && !item.folder) {
1931
+ const parts = parentFolder.split('/');
1932
+ parts.pop();
1933
+ parentFolder = '';
1934
+ while (parts.length) {
1935
+ const _item = this.findItem(parts.join('/'));
1936
+ if (_item?.folder) {
1937
+ parentFolder = parts.join('/');
1938
+ break;
1939
+ }
1940
+ parts.pop();
1941
+ }
1942
+ if (!parts.length) {
1943
+ return null;
1944
+ }
1945
+ }
1946
+
1947
+ return parentFolder;
1948
+ }
1949
+
1950
+ async uploadFile(fileName: string, data: string): Promise<void> {
1951
+ const parts: string[] = fileName.split('/');
1952
+ const adapterName = parts.shift();
1953
+ try {
1954
+ await this.props.socket.writeFile64(adapterName || '', parts.join('/'), data);
1955
+ } catch (e) {
1956
+ window.alert(`Cannot write file: ${e}`);
1957
+ }
1958
+ }
1959
+
1960
+ renderUpload() {
1961
+ if (this.state.uploadFile) {
1962
+ return [
1963
+ <Fab
1964
+ key="close"
1965
+ color="primary"
1966
+ aria-label="close"
1967
+ style={styles.uploadCloseButton}
1968
+ onClick={() => this.setState({ uploadFile: false })}
1969
+ >
1970
+ <CloseIcon />
1971
+ </Fab>,
1972
+ <Dropzone
1973
+ key="dropzone"
1974
+ onDragEnter={() => this.setState({ uploadFile: 'dragging' })}
1975
+ onDragLeave={() => this.setState({ uploadFile: true })}
1976
+ onDrop={acceptedFiles => {
1977
+ let count = acceptedFiles.length;
1978
+
1979
+ acceptedFiles.forEach(file => {
1980
+ const reader = new FileReader();
1981
+
1982
+ reader.onabort = () => console.log('file reading was aborted');
1983
+ reader.onerror = () => console.log('file reading has failed');
1984
+ reader.onload = () => {
1985
+ const parentFolder = this.findFirstFolder(this.state.selected);
1986
+
1987
+ if (!parentFolder) {
1988
+ window.alert(this.props.t('ra_Invalid parent folder!'));
1989
+ } else {
1990
+ const id = `${parentFolder}/${file.name}`;
1991
+
1992
+ this.uploadFile(id, reader.result as string)
1993
+ .then(() => {
1994
+ if (!--count) {
1995
+ this.setState({ uploadFile: false }, () => {
1996
+ if (this.supportSubscribes) {
1997
+ // open current folder
1998
+ const expanded = [...this.state.expanded];
1999
+ if (!expanded.includes(parentFolder)) {
2000
+ expanded.push(parentFolder);
2001
+ expanded.sort();
2002
+ this.localStorage.setItem(
2003
+ 'files.expanded',
2004
+ JSON.stringify(expanded),
2005
+ );
2006
+ }
2007
+ this.setState({ expanded }, () => this.select(id));
2008
+ } else {
2009
+ setTimeout(
2010
+ () =>
2011
+ this.browseFolder(parentFolder, null, false, true)
2012
+ .then(folders => {
2013
+ // open current folder
2014
+ const expanded = [...this.state.expanded];
2015
+ if (!expanded.includes(parentFolder)) {
2016
+ expanded.push(parentFolder);
2017
+ expanded.sort();
2018
+ this.localStorage.setItem(
2019
+ 'files.expanded',
2020
+ JSON.stringify(expanded),
2021
+ );
2022
+ }
2023
+ this.setState({ folders, expanded }, () =>
2024
+ this.select(id));
2025
+ }),
2026
+ 500,
2027
+ );
2028
+ }
2029
+ });
2030
+ }
2031
+ });
2032
+ }
2033
+ };
2034
+
2035
+ reader.readAsArrayBuffer(file);
2036
+ });
2037
+ }}
2038
+ >
2039
+ {({ getRootProps, getInputProps }) => <div
2040
+ style={{
2041
+ ...styles.uploadDiv,
2042
+ ...(this.state.uploadFile === 'dragging' ? styles.uploadDivDragging : undefined),
2043
+ }}
2044
+ {...getRootProps()}
2045
+ >
2046
+ <input {...getInputProps()} />
2047
+ <Box component="div" sx={styles.uploadCenterDiv}>
2048
+ <div style={styles.uploadCenterTextAndIcon}>
2049
+ <UploadIcon style={styles.uploadCenterIcon} />
2050
+ <div style={styles.uploadCenterText}>
2051
+ {this.state.uploadFile === 'dragging'
2052
+ ? this.props.t('ra_Drop file here')
2053
+ : this.props.t(
2054
+ 'ra_Place your files here or click here to open the browse dialog',
2055
+ )}
2056
+ </div>
2057
+ </div>
2058
+ </Box>
2059
+ </div>}
2060
+ </Dropzone>,
2061
+ ];
2062
+ }
2063
+ return null;
2064
+ }
2065
+
2066
+ deleteRecursive(id: string): Promise<void> {
2067
+ const item = this.findItem(id);
2068
+ if (item?.folder) {
2069
+ return (
2070
+ this.state.folders[id]
2071
+ ? Promise.all(this.state.folders[id].map(_item => this.deleteRecursive(_item.id)))
2072
+ : Promise.resolve()
2073
+ )
2074
+ .then(() => {
2075
+ // If it is a folder of second level
2076
+ if (item.level >= 1) {
2077
+ const parts = id.split('/');
2078
+ const adapter = parts.shift();
2079
+ this.props.socket.deleteFolder(adapter || '', parts.join('/')).then(() => {
2080
+ // remove this folder
2081
+ const folders = JSON.parse(JSON.stringify(this.state.folders));
2082
+ delete folders[item.id];
2083
+ // delete folder from parent item
2084
+ const parentId = getParentDir(item.id);
2085
+ const parentFolder = folders[parentId];
2086
+ if (parentFolder) {
2087
+ const pos = parentFolder.findIndex((f: FolderOrFileItem) => f.id === item.id);
2088
+ if (pos !== -1) {
2089
+ parentFolder.splice(pos, 1);
2090
+ }
2091
+
2092
+ this.select(parentId, null, () => this.setState({ folders }));
2093
+ }
2094
+ });
2095
+ }
2096
+ });
2097
+ }
2098
+
2099
+ const parts = id.split('/');
2100
+ const adapter = parts.shift();
2101
+ if (parts.length) {
2102
+ return this.props.socket
2103
+ .deleteFile(adapter || '', parts.join('/'))
2104
+ .catch(e => window.alert(`Cannot delete file: ${e}`));
2105
+ }
2106
+ return Promise.resolve();
2107
+ }
2108
+
2109
+ deleteItem(deleteItem: string) {
2110
+ deleteItem = deleteItem || this.state.deleteItem;
2111
+
2112
+ this.setState({ deleteItem: '' }, () =>
2113
+ this.deleteRecursive(deleteItem)
2114
+ .then(() => {
2115
+ const newState: Partial<FileBrowserState> = {};
2116
+ const pos = this.state.expanded.indexOf(deleteItem);
2117
+ if (pos !== -1) {
2118
+ const expanded = [...this.state.expanded];
2119
+ expanded.splice(pos, 1);
2120
+ this.localStorage.setItem('files.expanded', JSON.stringify(expanded));
2121
+ newState.expanded = expanded;
2122
+ }
2123
+
2124
+ if (this.state.selected === deleteItem) {
2125
+ const parts = this.state.selected.split('/');
2126
+ parts.pop();
2127
+ newState.selected = parts.join('/');
2128
+ }
2129
+
2130
+ if (!this.supportSubscribes) {
2131
+ const parentFolder = this.findFirstFolder(deleteItem);
2132
+ const folders: Folders = {};
2133
+
2134
+ Object.keys(this.state.folders).forEach(name => {
2135
+ if (name !== parentFolder && !name.startsWith(`${parentFolder}/`)) {
2136
+ folders[name] = this.state.folders[name];
2137
+ }
2138
+ });
2139
+
2140
+ newState.folders = folders;
2141
+
2142
+ this.setState(newState as FileBrowserState, () =>
2143
+ setTimeout(() => {
2144
+ (this.browseFolders([...this.state.expanded], folders) as Promise<Folders>)
2145
+ .then(_folders => this.setState({ folders: _folders }));
2146
+ }, 200));
2147
+ } else {
2148
+ // @ts-expect-error fix later
2149
+ this.setState(newState);
2150
+ }
2151
+ }));
2152
+ }
2153
+
2154
+ renderDeleteDialog() {
2155
+ if (this.state.deleteItem) {
2156
+ return <Dialog
2157
+ key="deleteDialog"
2158
+ open={!0}
2159
+ onClose={() => this.setState({ deleteItem: '' })}
2160
+ aria-labelledby="ar_dialog_file_delete_title"
2161
+ >
2162
+ <DialogTitle id="ar_dialog_file_delete_title">
2163
+ {this.props.t('ra_Confirm deletion of %s', this.state.deleteItem.split('/').pop() as string)}
2164
+ </DialogTitle>
2165
+ <DialogContent>
2166
+ <DialogContentText>{this.props.t('ra_Are you sure?')}</DialogContentText>
2167
+ </DialogContent>
2168
+ <DialogActions>
2169
+ <Button
2170
+ color="grey"
2171
+ variant="contained"
2172
+ onClick={() => {
2173
+ this.suppressDeleteConfirm = Date.now() + 60000 * 5;
2174
+ this.deleteItem('');
2175
+ }}
2176
+ >
2177
+ {this.props.t('ra_Delete (no confirm for 5 mins)')}
2178
+ </Button>
2179
+ <Button
2180
+ variant="contained"
2181
+ onClick={() => this.deleteItem('')}
2182
+ color="primary"
2183
+ autoFocus
2184
+ >
2185
+ {this.props.t('ra_Delete')}
2186
+ </Button>
2187
+ <Button
2188
+ variant="contained"
2189
+ onClick={() => this.setState({ deleteItem: '' })}
2190
+ color="grey"
2191
+ >
2192
+ {this.props.t('ra_Cancel')}
2193
+ </Button>
2194
+ </DialogActions>
2195
+ </Dialog>;
2196
+ }
2197
+ return false;
2198
+ }
2199
+
2200
+ renderViewDialog() {
2201
+ return this.state.viewer ? <FileViewer
2202
+ supportSubscribes={this.supportSubscribes}
2203
+ key={this.state.viewer}
2204
+ href={this.state.viewer}
2205
+ formatEditFile={this.state.formatEditFile}
2206
+ themeType={this.props.themeType}
2207
+ setStateBackgroundImage={this.setStateBackgroundImage}
2208
+ getStyleBackgroundImage={this.getStyleBackgroundImage}
2209
+ t={this.props.t}
2210
+ socket={this.props.socket}
2211
+ lang={this.props.lang}
2212
+ expertMode={this.state.expertMode}
2213
+ onClose={() => this.setState({ viewer: '', formatEditFile: '' })}
2214
+ /> : null;
2215
+ }
2216
+
2217
+ renderError() {
2218
+ if (this.state.errorText) {
2219
+ return <ErrorDialog
2220
+ key="errorDialog"
2221
+ text={this.state.errorText}
2222
+ onClose={() => this.setState({ errorText: '' })}
2223
+ />;
2224
+ }
2225
+ return null;
2226
+ }
2227
+
2228
+ // used in tabs/Files
2229
+ // eslint-disable-next-line react/no-unused-class-component-methods
2230
+ updateItemsAcl(info: FolderOrFileItem[]) {
2231
+ this.cacheFolders = this.cacheFolders || JSON.parse(JSON.stringify(this.state.folders));
2232
+ let changed;
2233
+
2234
+ info.forEach(it => {
2235
+ const item = this.findItem(it.id, this.cacheFolders);
2236
+ if (item && JSON.stringify(item.acl) !== JSON.stringify(it.acl)) {
2237
+ item.acl = it.acl;
2238
+ changed = true;
2239
+ }
2240
+ });
2241
+ if (changed) {
2242
+ this.cacheFoldersTimeout && clearTimeout(this.cacheFoldersTimeout);
2243
+ this.cacheFoldersTimeout = setTimeout(() => {
2244
+ this.cacheFoldersTimeout = null;
2245
+ const folders = this.cacheFolders || {};
2246
+ this.cacheFolders = null;
2247
+ this.setState({ folders });
2248
+ }, 200);
2249
+ }
2250
+ }
2251
+
2252
+ changeToPath() {
2253
+ setTimeout(() => {
2254
+ if (this.state.path !== this.state.selected && (!this.lastSelect || Date.now() - this.lastSelect > 100)) {
2255
+ let folder = this.state.path;
2256
+ if (isFile(this.state.path)) {
2257
+ folder = getParentDir(this.state.path);
2258
+ }
2259
+ new Promise(resolve => {
2260
+ if (!this.state.folders[folder]) {
2261
+ this.browseFolder(folder)
2262
+ .then(folders => this.setState({ folders }, () => resolve(true)))
2263
+ .catch(err =>
2264
+ this.setState({
2265
+ errorText:
2266
+ err === NOT_FOUND
2267
+ ? this.props.t('ra_Cannot find "%s"', folder)
2268
+ : this.props.t('ra_Cannot read "%s"', folder),
2269
+ }));
2270
+ } else {
2271
+ resolve(true);
2272
+ }
2273
+ })
2274
+ .then(result =>
2275
+ result && this.setState({ selected: this.state.path, currentDir: folder, pathFocus: false }));
2276
+ } else if (!this.lastSelect || Date.now() - this.lastSelect > 100) {
2277
+ this.setState({ pathFocus: false });
2278
+ }
2279
+ }, 100);
2280
+ }
2281
+
2282
+ renderBreadcrumb() {
2283
+ const parts = this.state.currentDir.startsWith('/')
2284
+ ? this.state.currentDir.split('/')
2285
+ : `/${this.state.currentDir}`.split('/');
2286
+ const p: string[] = [];
2287
+ return <Breadcrumbs style={{ paddingLeft: 8 }}>
2288
+ {parts.map((part, i) => {
2289
+ part && p.push(part);
2290
+ const path = p.join('/');
2291
+ if (i < parts.length - 1) {
2292
+ return <Box
2293
+ component="div"
2294
+ key={`${this.state.selected}_${i}`}
2295
+ sx={styles.pathDivBreadcrumbDir}
2296
+ onClick={e => this.changeFolder(e, path || '/')}
2297
+ >
2298
+ {part || this.props.t('ra_Root')}
2299
+ </Box>;
2300
+ }
2301
+
2302
+ return <div
2303
+ style={styles.pathDivBreadcrumbSelected}
2304
+ key={`${this.state.selected}_${i}`}
2305
+ onClick={() => this.setState({ pathFocus: true })}
2306
+ >
2307
+ {part}
2308
+ </div>;
2309
+ })}
2310
+ </Breadcrumbs>;
2311
+ }
2312
+
2313
+ renderPath() {
2314
+ return <Box component="div" key="path" sx={styles.pathDiv}>
2315
+ {this.state.pathFocus ?
2316
+ <Input
2317
+ value={this.state.path}
2318
+ onKeyDown={e => {
2319
+ if (e.key === 'Enter') {
2320
+ this.changeToPath();
2321
+ } else if (e.key === 'Escape') {
2322
+ this.setState({ pathFocus: false });
2323
+ }
2324
+ }}
2325
+ endAdornment={
2326
+ <IconButton size="small" onClick={() => this.changeToPath()}>
2327
+ <EnterIcon />
2328
+ </IconButton>
2329
+ }
2330
+ onBlur={() => this.changeToPath()}
2331
+ onChange={e => this.setState({ path: e.target.value })}
2332
+ style={styles.pathDivInput}
2333
+ />
2334
+ :
2335
+ this.renderBreadcrumb()}
2336
+ </Box>;
2337
+ }
2338
+
2339
+ render() {
2340
+ if (!this.props.ready) {
2341
+ return <LinearProgress />;
2342
+ }
2343
+
2344
+ if (this.state.loadAllFolders && !this.foldersLoading) {
2345
+ this.foldersLoading = true;
2346
+ setTimeout(() => {
2347
+ this.setState({ loadAllFolders: false, folders: {} }, () => {
2348
+ this.foldersLoading = false;
2349
+ this.loadFolders()
2350
+ .catch(error => console.error(`Cannot load folders: ${error}`));
2351
+ });
2352
+ }, 300);
2353
+ }
2354
+
2355
+ return <div
2356
+ style={{ ...styles.root, ...this.props.style }}
2357
+ className={this.props.className}
2358
+ >
2359
+ {this.props.showToolbar ? this.renderToolbar() : null}
2360
+ {this.state.viewType === TILE ? this.renderPath() : null}
2361
+ <div
2362
+ style={{
2363
+ ...styles.filesDiv,
2364
+ ...styles[`filesDiv${this.state.viewType}`],
2365
+ }}
2366
+ onClick={e => {
2367
+ if (this.state.viewType !== TABLE) {
2368
+ if (this.state.selected !== (this.state.currentDir || '/')) {
2369
+ this.changeFolder(e, this.state.currentDir || '/');
2370
+ } else {
2371
+ e.stopPropagation();
2372
+ }
2373
+ }
2374
+ }}
2375
+ >
2376
+ {this.state.viewType === TABLE
2377
+ ? this.renderItems('/')
2378
+ : this.renderItems(this.state.currentDir || '/')}
2379
+ {this.state.viewType !== TABLE ?
2380
+ <div style={styles.filesDivHint}>{this.props.t('ra_select_folder_hint')}</div> : null}
2381
+ </div>
2382
+ {this.props.allowUpload ? this.renderInputDialog() : null}
2383
+ {this.props.allowUpload ? this.renderUpload() : null}
2384
+ {this.props.allowDelete ? this.renderDeleteDialog() : null}
2385
+ {this.props.allowView ? this.renderViewDialog() : null}
2386
+ {this.state.modalEditOfAccess && this.props.modalEditOfAccessControl
2387
+ ? this.props.modalEditOfAccessControl(this)
2388
+ : null}
2389
+ {this.renderError()}
2390
+ </div>;
2391
+ }
2392
+ }
2393
+
2394
+ export default withWidth()(FileBrowserClass);