@iobroker/adapter-react-v5 7.0.1 → 7.0.2

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