StatusBar.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. const throttle = require('lodash.throttle')
  2. const classNames = require('classnames')
  3. const statusBarStates = require('./StatusBarStates')
  4. const prettyBytes = require('prettier-bytes')
  5. const prettyETA = require('@uppy/utils/lib/prettyETA')
  6. const { h } = require('preact')
  7. function calculateProcessingProgress (files) {
  8. // Collect pre or postprocessing progress states.
  9. const progresses = []
  10. Object.keys(files).forEach((fileID) => {
  11. const { progress } = files[fileID]
  12. if (progress.preprocess) {
  13. progresses.push(progress.preprocess)
  14. }
  15. if (progress.postprocess) {
  16. progresses.push(progress.postprocess)
  17. }
  18. })
  19. // In the future we should probably do this differently. For now we'll take the
  20. // mode and message from the first file…
  21. const { mode, message } = progresses[0]
  22. const value = progresses.filter(isDeterminate).reduce((total, progress, index, all) => {
  23. return total + progress.value / all.length
  24. }, 0)
  25. function isDeterminate (progress) {
  26. return progress.mode === 'determinate'
  27. }
  28. return {
  29. mode,
  30. message,
  31. value
  32. }
  33. }
  34. function togglePauseResume (props) {
  35. if (props.isAllComplete) return
  36. if (!props.resumableUploads) {
  37. return props.cancelAll()
  38. }
  39. if (props.isAllPaused) {
  40. return props.resumeAll()
  41. }
  42. return props.pauseAll()
  43. }
  44. module.exports = (props) => {
  45. props = props || {}
  46. const { newFiles,
  47. allowNewUpload,
  48. isUploadInProgress,
  49. isAllPaused,
  50. resumableUploads,
  51. error,
  52. hideUploadButton,
  53. hidePauseResumeButton,
  54. hideCancelButton,
  55. hideRetryButton } = props
  56. const uploadState = props.uploadState
  57. let progressValue = props.totalProgress
  58. let progressMode
  59. let progressBarContent
  60. if (uploadState === statusBarStates.STATE_PREPROCESSING || uploadState === statusBarStates.STATE_POSTPROCESSING) {
  61. const progress = calculateProcessingProgress(props.files)
  62. progressMode = progress.mode
  63. if (progressMode === 'determinate') {
  64. progressValue = progress.value * 100
  65. }
  66. progressBarContent = ProgressBarProcessing(progress)
  67. } else if (uploadState === statusBarStates.STATE_COMPLETE) {
  68. progressBarContent = ProgressBarComplete(props)
  69. } else if (uploadState === statusBarStates.STATE_UPLOADING) {
  70. if (!props.supportsUploadProgress) {
  71. progressMode = 'indeterminate'
  72. progressValue = null
  73. }
  74. progressBarContent = ProgressBarUploading(props)
  75. } else if (uploadState === statusBarStates.STATE_ERROR) {
  76. progressValue = undefined
  77. progressBarContent = ProgressBarError(props)
  78. }
  79. const width = typeof progressValue === 'number' ? progressValue : 100
  80. const isHidden = (uploadState === statusBarStates.STATE_WAITING && props.hideUploadButton) ||
  81. (uploadState === statusBarStates.STATE_WAITING && !props.newFiles > 0) ||
  82. (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish)
  83. const showUploadBtn = !error && newFiles &&
  84. !isUploadInProgress && !isAllPaused &&
  85. allowNewUpload && !hideUploadButton
  86. const showCancelBtn = !hideCancelButton &&
  87. uploadState !== statusBarStates.STATE_WAITING &&
  88. uploadState !== statusBarStates.STATE_COMPLETE
  89. const showPauseResumeBtn = resumableUploads && !hidePauseResumeButton &&
  90. uploadState !== statusBarStates.STATE_WAITING &&
  91. uploadState !== statusBarStates.STATE_PREPROCESSING &&
  92. uploadState !== statusBarStates.STATE_POSTPROCESSING &&
  93. uploadState !== statusBarStates.STATE_COMPLETE
  94. const showRetryBtn = error && !hideRetryButton
  95. const progressClassNames = `uppy-StatusBar-progress
  96. ${progressMode ? 'is-' + progressMode : ''}`
  97. const statusBarClassNames = classNames(
  98. { 'uppy-Root': props.isTargetDOMEl },
  99. 'uppy-StatusBar',
  100. `is-${uploadState}`
  101. )
  102. return (
  103. <div class={statusBarClassNames} aria-hidden={isHidden}>
  104. <div class={progressClassNames}
  105. style={{ width: width + '%' }}
  106. role="progressbar"
  107. aria-valuemin="0"
  108. aria-valuemax="100"
  109. aria-valuenow={progressValue} />
  110. {progressBarContent}
  111. <div class="uppy-StatusBar-actions">
  112. { showUploadBtn ? <UploadBtn {...props} uploadState={uploadState} /> : null }
  113. { showRetryBtn ? <RetryBtn {...props} /> : null }
  114. { showPauseResumeBtn ? <PauseResumeButton {...props} /> : null }
  115. { showCancelBtn ? <CancelBtn {...props} /> : null }
  116. </div>
  117. </div>
  118. )
  119. }
  120. const UploadBtn = (props) => {
  121. const uploadBtnClassNames = classNames(
  122. 'uppy-u-reset',
  123. 'uppy-c-btn',
  124. 'uppy-StatusBar-actionBtn',
  125. 'uppy-StatusBar-actionBtn--upload',
  126. { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING }
  127. )
  128. return <button type="button"
  129. class={uploadBtnClassNames}
  130. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  131. onclick={props.startUpload}>
  132. {props.newFiles && props.isUploadStarted
  133. ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
  134. : props.i18n('uploadXFiles', { smart_count: props.newFiles })
  135. }
  136. </button>
  137. }
  138. const RetryBtn = (props) => {
  139. return <button type="button"
  140. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
  141. aria-label={props.i18n('retryUpload')}
  142. onclick={props.retryAll}>{props.i18n('retry')}</button>
  143. }
  144. const CancelBtn = (props) => {
  145. return <button
  146. type="button"
  147. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  148. title={props.i18n('cancel')}
  149. aria-label={props.i18n('cancel')}
  150. onclick={props.cancelAll}>
  151. <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
  152. <path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm1.414-8l2.122-2.121-1.415-1.415L8 6.586 5.879 4.464 4.464 5.88 6.586 8l-2.122 2.121 1.415 1.415L8 9.414l2.121 2.122 1.415-1.415L9.414 8z" fill="#949494" fill-rule="evenodd" />
  153. </svg>
  154. </button>
  155. }
  156. const PauseResumeButton = (props) => {
  157. const { isAllPaused, i18n } = props
  158. const title = isAllPaused ? i18n('resume') : i18n('pause')
  159. return <button
  160. title={title}
  161. aria-label={title}
  162. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  163. type="button"
  164. onclick={() => togglePauseResume(props)}>
  165. {isAllPaused
  166. ? <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
  167. <path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM6 5v6l5-3-5-3z" fill="#949494" fill-rule="evenodd" />
  168. </svg>
  169. : <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
  170. <path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM5 5v6h2V5H5zm4 0v6h2V5H9z" fill="#949494" fill-rule="evenodd" />
  171. </svg>
  172. }
  173. </button>
  174. }
  175. const LoadingSpinner = (props) => {
  176. return <svg class="uppy-StatusBar-spinner" width="14" height="14" xmlns="http://www.w3.org/2000/svg">
  177. <path d="M13.983 6.547c-.12-2.509-1.64-4.893-3.939-5.936-2.48-1.127-5.488-.656-7.556 1.094C.524 3.367-.398 6.048.162 8.562c.556 2.495 2.46 4.52 4.94 5.183 2.932.784 5.61-.602 7.256-3.015-1.493 1.993-3.745 3.309-6.298 2.868-2.514-.434-4.578-2.349-5.153-4.84a6.226 6.226 0 0 1 2.98-6.778C6.34.586 9.74 1.1 11.373 3.493c.407.596.693 1.282.842 1.988.127.598.073 1.197.161 1.794.078.525.543 1.257 1.15.864.525-.341.49-1.05.456-1.592-.007-.15.02.3 0 0" fill-rule="evenodd" />
  178. </svg>
  179. }
  180. const ProgressBarProcessing = (props) => {
  181. const value = Math.round(props.value * 100)
  182. return <div class="uppy-StatusBar-content">
  183. <LoadingSpinner {...props} />
  184. {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
  185. {props.message}
  186. </div>
  187. }
  188. const ProgressDetails = (props) => {
  189. return <div class="uppy-StatusBar-statusSecondary">
  190. { props.numUploads > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads }) + ' \u00B7 ' }
  191. { props.i18n('dataUploadedOfTotal', {
  192. complete: prettyBytes(props.totalUploadedSize),
  193. total: prettyBytes(props.totalSize)
  194. }) + ' \u00B7 ' }
  195. { props.i18n('xTimeLeft', { time: prettyETA(props.totalETA) }) }
  196. </div>
  197. }
  198. const UnknownProgressDetails = (props) => {
  199. return <div class="uppy-StatusBar-statusSecondary">
  200. { props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads }) }
  201. </div>
  202. }
  203. const UploadNewlyAddedFiles = (props) => {
  204. const uploadBtnClassNames = classNames(
  205. 'uppy-u-reset',
  206. 'uppy-c-btn',
  207. 'uppy-StatusBar-actionBtn'
  208. )
  209. return <div class="uppy-StatusBar-statusSecondary">
  210. <div class="uppy-StatusBar-statusSecondaryHint">
  211. { props.i18n('xMoreFilesAdded', { smart_count: props.newFiles }) }
  212. </div>
  213. <button type="button"
  214. class={uploadBtnClassNames}
  215. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  216. onclick={props.startUpload}>
  217. {props.i18n('upload')}
  218. </button>
  219. </div>
  220. }
  221. const ThrottledProgressDetails = throttle(ProgressDetails, 500, { leading: true, trailing: true })
  222. const ProgressBarUploading = (props) => {
  223. if (!props.isUploadStarted || props.isAllComplete) {
  224. return null
  225. }
  226. const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
  227. const showUploadNewlyAddedFiles = props.newFiles && props.isUploadStarted
  228. return (
  229. <div class="uppy-StatusBar-content" aria-label={title} title={title}>
  230. { !props.isAllPaused ? <LoadingSpinner {...props} /> : null }
  231. <div class="uppy-StatusBar-status">
  232. <div class="uppy-StatusBar-statusPrimary">
  233. {props.supportsUploadProgress ? `${title}: ${props.totalProgress}%` : title}
  234. </div>
  235. { !props.isAllPaused && !showUploadNewlyAddedFiles && props.showProgressDetails
  236. ? (props.supportsUploadProgress ? <ThrottledProgressDetails {...props} /> : <UnknownProgressDetails {...props} />)
  237. : null
  238. }
  239. { showUploadNewlyAddedFiles ? <UploadNewlyAddedFiles {...props} /> : null }
  240. </div>
  241. </div>
  242. )
  243. }
  244. const ProgressBarComplete = ({ totalProgress, i18n }) => {
  245. return (
  246. <div class="uppy-StatusBar-content" role="status" title={i18n('complete')}>
  247. <svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="18" height="17" viewBox="0 0 23 17">
  248. <path d="M8.944 17L0 7.865l2.555-2.61 6.39 6.525L20.41 0 23 2.645z" />
  249. </svg>
  250. {i18n('complete')}
  251. </div>
  252. )
  253. }
  254. const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
  255. return (
  256. <div class="uppy-StatusBar-content" role="alert">
  257. <span class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</span>
  258. {/* {!hideRetryButton &&
  259. <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span>
  260. } */}
  261. <span class="uppy-StatusBar-details"
  262. aria-label={error}
  263. data-microtip-position="top-right"
  264. data-microtip-size="medium"
  265. role="tooltip">?</span>
  266. </div>
  267. )
  268. }