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