@iobroker/adapter-react-v5 7.0.1 → 7.1.0

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