@iobroker/adapter-react-v5 6.1.10 → 7.0.1

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