@iobroker/adapter-react-v5 7.2.4 → 7.2.6

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