StatusBar.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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 (
  140. <button type="button"
  141. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry" aria-label={props.i18n('retryUpload')} onclick={props.retryAll}>
  142. <svg aria-hidden="true" class="UppyIcon" width="8" height="10" viewBox="0 0 8 10">
  143. <path d="M4 2.408a2.75 2.75 0 1 0 2.75 2.75.626.626 0 0 1 1.25.018v.023a4 4 0 1 1-4-4.041V.25a.25.25 0 0 1 .389-.208l2.299 1.533a.25.25 0 0 1 0 .416l-2.3 1.533A.25.25 0 0 1 4 3.316v-.908z" />
  144. </svg>
  145. {props.i18n('retry')}
  146. </button>
  147. )
  148. }
  149. const CancelBtn = (props) => {
  150. return <button
  151. type="button"
  152. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  153. title={props.i18n('cancel')}
  154. aria-label={props.i18n('cancel')}
  155. onclick={props.cancelAll}>
  156. <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  157. <g fill="none" fill-rule="evenodd">
  158. <circle fill="#888" cx="8" cy="8" r="8" />
  159. <path fill="#FFF" d="M9.283 8l2.567 2.567-1.283 1.283L8 9.283 5.433 11.85 4.15 10.567 6.717 8 4.15 5.433 5.433 4.15 8 6.717l2.567-2.567 1.283 1.283z" />
  160. </g>
  161. </svg>
  162. </button>
  163. }
  164. const PauseResumeButton = (props) => {
  165. const { isAllPaused, i18n } = props
  166. const title = isAllPaused ? i18n('resume') : i18n('pause')
  167. return <button
  168. title={title}
  169. aria-label={title}
  170. class="uppy-u-reset uppy-StatusBar-actionCircleBtn"
  171. type="button"
  172. onclick={() => togglePauseResume(props)}>
  173. {isAllPaused
  174. ? <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  175. <g fill="none" fill-rule="evenodd">
  176. <circle fill="#888" cx="8" cy="8" r="8" />
  177. <path fill="#FFF" d="M6 4.25L11.5 8 6 11.75z" />
  178. </g>
  179. </svg>
  180. : <svg aria-hidden="true" class="UppyIcon" width="16" height="16" viewBox="0 0 16 16">
  181. <g fill="none" fill-rule="evenodd">
  182. <circle fill="#888" cx="8" cy="8" r="8" />
  183. <path d="M5 4.5h2v7H5v-7zm4 0h2v7H9v-7z" fill="#FFF" />
  184. </g>
  185. </svg>
  186. }
  187. </button>
  188. }
  189. const LoadingSpinner = (props) => {
  190. return <svg class="uppy-StatusBar-spinner" width="14" height="14">
  191. <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" />
  192. </svg>
  193. }
  194. const ProgressBarProcessing = (props) => {
  195. const value = Math.round(props.value * 100)
  196. return <div class="uppy-StatusBar-content">
  197. <LoadingSpinner {...props} />
  198. {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
  199. {props.message}
  200. </div>
  201. }
  202. const ProgressDetails = (props) => {
  203. return <div class="uppy-StatusBar-statusSecondary">
  204. { props.numUploads > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads }) + ' \u00B7 ' }
  205. { props.i18n('dataUploadedOfTotal', {
  206. complete: prettyBytes(props.totalUploadedSize),
  207. total: prettyBytes(props.totalSize)
  208. }) + ' \u00B7 ' }
  209. { props.i18n('xTimeLeft', { time: prettyETA(props.totalETA) }) }
  210. </div>
  211. }
  212. const UnknownProgressDetails = (props) => {
  213. return <div class="uppy-StatusBar-statusSecondary">
  214. { props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads }) }
  215. </div>
  216. }
  217. const UploadNewlyAddedFiles = (props) => {
  218. const uploadBtnClassNames = classNames(
  219. 'uppy-u-reset',
  220. 'uppy-c-btn',
  221. 'uppy-StatusBar-actionBtn'
  222. )
  223. return <div class="uppy-StatusBar-statusSecondary">
  224. <div class="uppy-StatusBar-statusSecondaryHint">
  225. { props.i18n('xMoreFilesAdded', { smart_count: props.newFiles }) }
  226. </div>
  227. <button type="button"
  228. class={uploadBtnClassNames}
  229. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  230. onclick={props.startUpload}>
  231. {props.i18n('upload')}
  232. </button>
  233. </div>
  234. }
  235. const ThrottledProgressDetails = throttle(ProgressDetails, 500, { leading: true, trailing: true })
  236. const ProgressBarUploading = (props) => {
  237. if (!props.isUploadStarted || props.isAllComplete) {
  238. return null
  239. }
  240. const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
  241. const showUploadNewlyAddedFiles = props.newFiles && props.isUploadStarted
  242. return (
  243. <div class="uppy-StatusBar-content" aria-label={title} title={title}>
  244. { !props.isAllPaused ? <LoadingSpinner {...props} /> : null }
  245. <div class="uppy-StatusBar-status">
  246. <div class="uppy-StatusBar-statusPrimary">
  247. {props.supportsUploadProgress ? `${title}: ${props.totalProgress}%` : title}
  248. </div>
  249. { !props.isAllPaused && !showUploadNewlyAddedFiles && props.showProgressDetails
  250. ? (props.supportsUploadProgress ? <ThrottledProgressDetails {...props} /> : <UnknownProgressDetails {...props} />)
  251. : null
  252. }
  253. { showUploadNewlyAddedFiles ? <UploadNewlyAddedFiles {...props} /> : null }
  254. </div>
  255. </div>
  256. )
  257. }
  258. const ProgressBarComplete = ({ totalProgress, i18n }) => {
  259. return (
  260. <div class="uppy-StatusBar-content" role="status" title={i18n('complete')}>
  261. <div class="uppy-StatusBar-status">
  262. <div class="uppy-StatusBar-statusPrimary">
  263. <svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="15" height="11" viewBox="0 0 15 11">
  264. <path d="M.414 5.843L1.627 4.63l3.472 3.472L13.202 0l1.212 1.213L5.1 10.528z" />
  265. </svg>
  266. {i18n('complete')}
  267. </div>
  268. </div>
  269. </div>
  270. )
  271. }
  272. const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
  273. return (
  274. <div class="uppy-StatusBar-content" role="alert" title={i18n('uploadFailed')}>
  275. <div class="uppy-StatusBar-status">
  276. <div class="uppy-StatusBar-statusPrimary">
  277. <svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="11" height="11" viewBox="0 0 11 11">
  278. <path d="M4.278 5.5L0 1.222 1.222 0 5.5 4.278 9.778 0 11 1.222 6.722 5.5 11 9.778 9.778 11 5.5 6.722 1.222 11 0 9.778z" />
  279. </svg>
  280. {i18n('uploadFailed')}
  281. </div>
  282. </div>
  283. {/* {!hideRetryButton &&
  284. <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span>
  285. } */}
  286. <span class="uppy-StatusBar-details"
  287. aria-label={error}
  288. data-microtip-position="top-right"
  289. data-microtip-size="medium"
  290. role="tooltip">?</span>
  291. </div>
  292. )
  293. }