FileExplorer.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. /**
  2. * FileExplorer.js
  3. *
  4. * @author realor
  5. */
  6. import { Panel } from './Panel.js'
  7. import { Controls } from './Controls.js'
  8. import { Dialog } from './Dialog.js'
  9. import { LoginDialog } from './LoginDialog.js'
  10. import { ServiceDialog } from './ServiceDialog.js'
  11. import { MessageDialog } from './MessageDialog.js'
  12. import { ConfirmDialog } from './ConfirmDialog.js'
  13. import { Toast } from './Toast.js'
  14. import { ServiceManager } from '../io/ServiceManager.js'
  15. import { FileService, Metadata, Result } from '../io/FileService.js'
  16. import { I18N } from '../i18n/I18N.js'
  17. class FileExplorer extends Panel {
  18. constructor(application, createContextButtons = true) {
  19. super(application)
  20. this.id = 'file_explorer'
  21. this.title = 'title.file_explorer'
  22. this.position = 'left'
  23. this.group = 'model' // service group
  24. this.minimumHeight = 200
  25. this.service = null // current service
  26. this.basePath = '/'
  27. this.entryName = ''
  28. this.entryType = null // COLLECTION or FILE
  29. this.showFileSize = true
  30. this.serviceElem = document.createElement('div')
  31. this.serviceElem.className = 'service_panel'
  32. this.headerElem = document.createElement('div')
  33. this.headerElem.className = 'header'
  34. this.homeButtonElem = Controls.addImageButton(this.headerElem, 'home', 'button.home', event => this.goHome(), 'image_button home')
  35. this.backButtonElem = Controls.addImageButton(this.headerElem, 'home', 'button.back', event => this.goBack(), 'image_button back')
  36. this.directoryElem = document.createElement('div')
  37. this.directoryElem.className = 'directory'
  38. this.entriesElem = document.createElement('ul')
  39. this.entriesElem.className = 'path_entries'
  40. this.footerElem = document.createElement('div')
  41. this.footerElem.className = 'footer'
  42. this.buttonsPanelElem = document.createElement('div')
  43. this.buttonsPanelElem.className = 'buttons_panel'
  44. this.bodyElem.appendChild(this.serviceElem)
  45. this.serviceElem.appendChild(this.headerElem)
  46. this.serviceElem.appendChild(this.entriesElem)
  47. this.serviceElem.appendChild(this.footerElem)
  48. this.headerElem.appendChild(this.homeButtonElem)
  49. this.headerElem.appendChild(this.backButtonElem)
  50. this.headerElem.appendChild(this.directoryElem)
  51. this.footerElem.appendChild(this.buttonsPanelElem)
  52. this.showButtonsPanel()
  53. if (createContextButtons) {
  54. this.addContextButtons()
  55. }
  56. }
  57. addContextButton(name, label, action, isVisible) {
  58. const buttonElem = Controls.addButton(this.buttonsPanelElem, name, label, action)
  59. buttonElem._isVisible = isVisible
  60. }
  61. addContextButtons() {
  62. this.addContextButton(
  63. 'open',
  64. 'button.open',
  65. () => this.openEntry(),
  66. () => this.isEntrySelected()
  67. )
  68. this.addCommonContextButtons()
  69. this.addServiceContextButtons()
  70. }
  71. addCommonContextButtons() {
  72. this.addContextButton(
  73. 'delete',
  74. 'button.delete',
  75. () => this.showDeleteDialog(),
  76. () => this.isEntrySelected()
  77. )
  78. this.addContextButton(
  79. 'folder',
  80. 'button.folder',
  81. () => this.showFolderDialog(),
  82. () => this.isDirectoryList()
  83. )
  84. this.addContextButton(
  85. 'upload',
  86. 'button.upload',
  87. () => this.showUploadDialog(),
  88. () => this.isDirectoryList()
  89. )
  90. this.addContextButton(
  91. 'download',
  92. 'button.download',
  93. () => this.download(this.basePath + '/' + this.entryName),
  94. () => this.isDirectoryList() && this.isFileEntrySelected()
  95. )
  96. }
  97. addServiceContextButtons() {
  98. this.addContextButton(
  99. 'add',
  100. 'button.add',
  101. () => this.showAddDialog(),
  102. () => this.isServiceList()
  103. )
  104. this.addContextButton(
  105. 'edit',
  106. 'button.edit',
  107. () => this.showEditDialog(),
  108. () => this.isServiceList() && this.isEntrySelected()
  109. )
  110. }
  111. isServiceList() {
  112. return this.service === null
  113. }
  114. isDirectoryList() {
  115. return this.service !== null
  116. }
  117. isEntrySelected() {
  118. return this.entryName !== ''
  119. }
  120. isFileEntrySelected() {
  121. return this.entryType === Metadata.FILE
  122. }
  123. isCollectionEntrySelected() {
  124. return this.entryType === Metadata.COLLECTION
  125. }
  126. showAddDialog() {
  127. const serviceTypes = ServiceManager.getTypesOf(FileService)
  128. let dialog = new ServiceDialog('title.add_cloud_service', serviceTypes)
  129. dialog.setI18N(this.application.i18n)
  130. dialog.onSave = (serviceType, name, description, url, username, password) => {
  131. const service = new ServiceManager.classes[serviceType]()
  132. service.name = name
  133. service.description = description
  134. service.url = url
  135. service.username = username
  136. service.password = password
  137. this.application.addService(service, this.group)
  138. this.showServices()
  139. }
  140. dialog.show()
  141. }
  142. showEditDialog() {
  143. const service = this.application.services[this.group][this.entryName]
  144. const serviceTypes = ServiceManager.getTypesOf(FileService)
  145. let dialog = new ServiceDialog('title.edit_cloud_service', serviceTypes, service.constructor.type, service.name, service.description, service.url, service.username, service.password)
  146. dialog.setI18N(this.application.i18n)
  147. dialog.serviceTypeSelect.disabled = true
  148. dialog.nameElem.readOnly = true
  149. dialog.onSave = (serviceType, name, description, url, username, password) => {
  150. service.description = description
  151. service.url = url
  152. service.username = username
  153. service.password = password
  154. this.application.addService(service, this.group)
  155. this.showServices()
  156. }
  157. dialog.show()
  158. }
  159. showDeleteDialog() {
  160. const application = this.application
  161. let name = this.entryName
  162. if (this.service === null) {
  163. ConfirmDialog.create('title.delete_cloud_service', 'question.delete_service', name)
  164. .setAction(() => {
  165. let service = application.services[this.group][name]
  166. application.removeService(service, this.group)
  167. this.entryName = ''
  168. this.entryType = null
  169. this.showServices()
  170. })
  171. .setAcceptLabel('button.delete')
  172. .setI18N(application.i18n)
  173. .show()
  174. } else {
  175. let question = this.entryType === Metadata.FILE ? 'question.delete_file' : 'question.delete_folder'
  176. ConfirmDialog.create('title.delete_from_cloud', question, name)
  177. .setAction(() => this.deletePath(this.basePath + '/' + name))
  178. .setAcceptLabel('button.delete')
  179. .setI18N(application.i18n)
  180. .show()
  181. }
  182. }
  183. showFolderDialog() {
  184. const application = this.application
  185. const dialog = new Dialog('title.create_folder_in_cloud')
  186. dialog.setSize(250, 130)
  187. dialog.setI18N(application.i18n)
  188. let nameElem = dialog.addTextField('folder_name', 'label.folder_name')
  189. nameElem.setAttribute('spellcheck', 'false')
  190. dialog.addButton('folder_accept', 'button.create', () => dialog.onAccept())
  191. dialog.addButton('folder_cancel', 'button.cancel', () => dialog.onCancel())
  192. dialog.onAccept = () => {
  193. this.makeFolder(this.basePath + '/' + nameElem.value)
  194. dialog.hide()
  195. }
  196. dialog.onCancel = () => {
  197. dialog.hide()
  198. }
  199. dialog.onShow = () => {
  200. nameElem.focus()
  201. }
  202. dialog.show()
  203. }
  204. showUploadDialog() {
  205. let inputFile = document.createElement('input')
  206. inputFile.type = 'file'
  207. inputFile.addEventListener('change', event => this.upload(inputFile.files))
  208. inputFile.click()
  209. }
  210. openPath(path, onSuccess) {
  211. this.showProgressBar('Reading...')
  212. this.service.open(
  213. path,
  214. result => this.handleOpenResult(path, result, onSuccess),
  215. data => this.setProgress(data.progress, data.message)
  216. )
  217. }
  218. savePath(path, data, onSuccess) {
  219. this.showProgressBar('Saving...')
  220. this.service.save(
  221. path,
  222. data,
  223. result => this.handleSaveResult(path, data, result, onSuccess),
  224. data => this.setProgress(data.progress, data.message)
  225. )
  226. }
  227. deletePath(path, onSuccess) {
  228. this.showProgressBar('Deleting...')
  229. this.service.remove(path, result => this.handleDeleteResult(path, result, onSuccess))
  230. }
  231. makeFolder(path, onSuccess) {
  232. this.showProgressBar('Creating folder...')
  233. this.service.makeCollection(path, result => this.handleMakeFolderResult(path, result, onSuccess))
  234. }
  235. download(path, onSuccess) {
  236. this.showProgressBar('Downloading file...')
  237. this.service.open(
  238. path,
  239. result => this.handleDownloadResult(path, result, onSuccess),
  240. data => this.setProgress(data.progress)
  241. )
  242. }
  243. upload(files, onSuccess) {
  244. if (files.length > 0) {
  245. const application = this.application
  246. let file = files[0]
  247. let reader = new FileReader()
  248. reader.onload = event => {
  249. const data = event.target.result
  250. const path = this.basePath + '/' + file.name
  251. this.showProgressBar('Uploading file...')
  252. this.service.save(
  253. path,
  254. data,
  255. result => {
  256. this.entryName = file.name
  257. this.entryType = Metadata.FILE
  258. this.handleUploadResult(files, result, onSuccess)
  259. },
  260. data => this.setProgress(data.progress)
  261. )
  262. }
  263. reader.readAsText(file)
  264. }
  265. }
  266. handleOpenResult(path, result, onSuccess) {
  267. this.showButtonsPanel()
  268. if (result.status === Result.OK) {
  269. if (onSuccess) onSuccess()
  270. if (result.entries) {
  271. this.entryName = ''
  272. this.entryType = null
  273. this.showDirectory(path, result)
  274. } else {
  275. this.openFile(this.service.url + path, result.data)
  276. }
  277. } else {
  278. this.handleError(
  279. result,
  280. false,
  281. () => this.openPath(path),
  282. () => {
  283. if (path === '/') this.service = null
  284. }
  285. )
  286. }
  287. }
  288. handleSaveResult(path, data, result, onSuccess) {
  289. const application = this.application
  290. this.showButtonsPanel()
  291. if (result.status === Result.OK) {
  292. if (onSuccess) onSuccess()
  293. Toast.create('message.file_saved')
  294. .setI18N(application.i18n)
  295. .show()
  296. // reload current directory
  297. this.openPath(this.basePath)
  298. } else {
  299. this.handleError(result, true, () => this.savePath(path, data, onSuccess))
  300. }
  301. }
  302. handleDeleteResult(path, result, onSuccess) {
  303. const application = this.application
  304. this.showButtonsPanel()
  305. if (result.status === Result.OK) {
  306. if (onSuccess) onSuccess()
  307. if (this.entryType === Metadata.COLLECTION) {
  308. Toast.create('message.folder_deleted')
  309. .setI18N(application.i18n)
  310. .show()
  311. } else {
  312. Toast.create('message.file_deleted')
  313. .setI18N(application.i18n)
  314. .show()
  315. }
  316. this.entryName = ''
  317. this.entryType = null
  318. // reload basePath
  319. this.openPath(this.basePath)
  320. } else {
  321. this.handleError(result, true, () => this.deletePath(path))
  322. }
  323. }
  324. handleMakeFolderResult(path, result, onSuccess) {
  325. const application = this.application
  326. this.showButtonsPanel()
  327. if (result.status === Result.OK) {
  328. if (onSuccess) onSuccess()
  329. Toast.create('message.folder_created')
  330. .setI18N(application.i18n)
  331. .show()
  332. this.entryName = ''
  333. this.entryType = null
  334. // reload basePath
  335. this.openPath(this.basePath)
  336. } else {
  337. this.handleError(result, true, () => this.makeFolder(path))
  338. }
  339. }
  340. handleDownloadResult(path, result, onSuccess) {
  341. this.showButtonsPanel()
  342. if (result.status === Result.OK) {
  343. if (onSuccess) onSuccess()
  344. const data = result.data
  345. if (this.downloadUrl) {
  346. window.URL.revokeObjectURL(this.downloadUrl)
  347. }
  348. const blob = new Blob([data], { type: 'application/octet-stream' })
  349. this.downloadUrl = window.URL.createObjectURL(blob)
  350. let linkElem = document.createElement('a')
  351. linkElem.download = this.entryName
  352. linkElem.target = '_blank'
  353. linkElem.href = this.downloadUrl
  354. linkElem.style.display = 'block'
  355. linkElem.click()
  356. } else {
  357. this.handleError(result, false, () => this.download(path))
  358. }
  359. }
  360. handleUploadResult(files, result, onSuccess) {
  361. const application = this.application
  362. this.showButtonsPanel()
  363. if (result.status === Result.OK) {
  364. if (onSuccess) onSuccess()
  365. Toast.create('message.file_saved')
  366. .setI18N(application.i18n)
  367. .show()
  368. // reload current directory
  369. this.openPath(this.basePath)
  370. } else {
  371. this.handleError(result, true, () => this.upload(files))
  372. }
  373. }
  374. showServices() {
  375. const application = this.application
  376. this.basePath = '/'
  377. const COLLECTION = Metadata.COLLECTION
  378. this.directoryElem.innerHTML = '/'
  379. this.entriesElem.innerHTML = ''
  380. let firstLink = null
  381. for (let serviceName in application.services[this.group]) {
  382. let service = application.services[this.group][serviceName]
  383. let entryElem = document.createElement('li')
  384. entryElem.className = 'entry service'
  385. entryElem.entryName = service.name
  386. let linkElem = document.createElement('a')
  387. linkElem.href = '#'
  388. linkElem.innerHTML = service.description || service.name
  389. linkElem.addEventListener('click', event => this.onEntry(service.name, COLLECTION))
  390. linkElem.addEventListener('dblclick', event => this.openEntry())
  391. entryElem.appendChild(linkElem)
  392. if (firstLink === null) firstLink = linkElem
  393. this.entriesElem.appendChild(entryElem)
  394. }
  395. this.highlight()
  396. this.updateButtons()
  397. if (firstLink) firstLink.focus()
  398. }
  399. showDirectory(path, result) {
  400. const application = this.application
  401. const service = this.service
  402. this.basePath = path
  403. const FILE = Metadata.FILE
  404. if (path === '/') {
  405. // service home
  406. this.directoryElem.innerHTML = service.description || service.name
  407. } else {
  408. this.directoryElem.innerHTML = result.metadata.name
  409. }
  410. let entries = result.entries
  411. entries.sort(this.entryComparator)
  412. this.entriesElem.innerHTML = ''
  413. let firstLink = null
  414. for (let entry of entries) {
  415. let entryElem = document.createElement('li')
  416. let className = 'entry ' + (entry.type === FILE ? 'file' : 'collection')
  417. entryElem.className = className
  418. entryElem.entryName = entry.name
  419. let linkElem = document.createElement('a')
  420. linkElem.href = '#'
  421. let label = entry.description
  422. let size = entry.size
  423. if (entry.type === FILE && size > 0 && this.showFileSize) {
  424. label += ' ('
  425. if (size > 1000000) label += (size / 1000000).toFixed(0) + ' Mb'
  426. else if (size > 1000) label += (size / 1000).toFixed(0) + ' Kb'
  427. else label += '1 Kb'
  428. label += ')'
  429. }
  430. linkElem.innerHTML = label
  431. linkElem.addEventListener('click', event => this.onEntry(entry.name, entry.type))
  432. linkElem.addEventListener('dblclick', event => this.openEntry())
  433. entryElem.appendChild(linkElem)
  434. if (firstLink === null) firstLink = linkElem
  435. this.entriesElem.appendChild(entryElem)
  436. }
  437. this.highlight()
  438. this.updateButtons()
  439. if (firstLink) firstLink.focus()
  440. }
  441. highlight() {
  442. const entriesElem = this.entriesElem
  443. for (let i = 0; i < entriesElem.childNodes.length; i++) {
  444. let childNode = entriesElem.childNodes[i]
  445. if (childNode.nodeName === 'LI') {
  446. if (childNode.entryName === this.entryName) {
  447. childNode.classList.add('selected')
  448. } else {
  449. childNode.classList.remove('selected')
  450. }
  451. }
  452. }
  453. }
  454. goHome() {
  455. this.entryName = ''
  456. this.entryType = null
  457. this.service = null
  458. this.showServices()
  459. }
  460. goBack() {
  461. if (this.service === null) return
  462. this.entryName = ''
  463. this.entryType = null
  464. if (this.basePath === '/') {
  465. this.service = null
  466. this.showServices()
  467. } else {
  468. const index = this.basePath.lastIndexOf('/')
  469. if (index <= 0) {
  470. this.openPath('/')
  471. } else {
  472. this.openPath(this.basePath.substring(0, index))
  473. }
  474. }
  475. }
  476. onEntry(entryName, entryType) {
  477. this.entryName = entryName
  478. this.entryType = entryType
  479. this.highlight()
  480. this.updateButtons()
  481. }
  482. openEntry() {
  483. let path
  484. if (this.service === null) {
  485. this.service = this.application.services[this.group][this.entryName]
  486. path = '/'
  487. } else {
  488. path = this.basePath
  489. if (!this.basePath.endsWith('/')) path += '/'
  490. path += this.entryName
  491. }
  492. this.openPath(path)
  493. }
  494. openFile(url, data) {
  495. this.application.progressBar.visible = false
  496. this.showButtonsPanel()
  497. }
  498. updateButtons() {
  499. const children = this.buttonsPanelElem.children
  500. let firstVisibleButton = null
  501. for (let child of children) {
  502. if (child._isVisible()) {
  503. child.style.display = 'inline'
  504. if (firstVisibleButton === null) firstVisibleButton = child
  505. } else {
  506. child.style.display = 'none'
  507. }
  508. }
  509. if (firstVisibleButton) {
  510. firstVisibleButton.focus()
  511. }
  512. }
  513. showButtonsPanel() {
  514. this.buttonsPanelElem.style.display = 'block'
  515. this.application.progressBar.visible = false
  516. }
  517. showProgressBar(message = '') {
  518. this.buttonsPanelElem.style.display = 'none'
  519. this.application.progressBar.message = message
  520. this.application.progressBar.progress = undefined
  521. this.application.progressBar.visible = true
  522. }
  523. setProgress(progress, message) {
  524. this.application.progressBar.progress = progress
  525. if (message) {
  526. this.application.progressBar.message = message
  527. }
  528. }
  529. entryComparator(a, b) {
  530. const COLLECTION = Metadata.COLLECTION
  531. const FILE = Metadata.FILE
  532. if (a.type === COLLECTION && b.type === FILE) return -1
  533. if (a.type === FILE && b.type === COLLECTION) return 1
  534. if (a.name < b.name) return -1
  535. if (a.name > b.name) return 1
  536. return 0
  537. }
  538. handleError(result, isWriteAction, onLogin, onFailed) {
  539. if (result.status === Result.INVALID_CREDENTIALS) {
  540. this.requestCredentials('message.invalid_credentials', onLogin, onFailed)
  541. } else if (result.status === Result.FORBIDDEN) {
  542. this.requestCredentials(isWriteAction ? 'message.action_denied' : 'message.access_denied', onLogin, onFailed)
  543. } else {
  544. if (onFailed) onFailed()
  545. MessageDialog.create('ERROR', result.message)
  546. .setClassName('error')
  547. .setI18N(this.application.i18n)
  548. .show()
  549. }
  550. }
  551. requestCredentials(message, onLogin, onFailed) {
  552. const loginDialog = new LoginDialog(this.application, message)
  553. loginDialog.login = (username, password) => {
  554. this.service.username = username
  555. this.service.password = password
  556. if (onLogin) onLogin()
  557. }
  558. loginDialog.onCancel = () => {
  559. loginDialog.hide()
  560. if (onFailed) onFailed()
  561. }
  562. loginDialog.show()
  563. }
  564. }
  565. export { FileExplorer }