StatusBar.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const throttle = require('lodash.throttle')
  2. const classNames = require('classnames')
  3. const statusBarStates = require('./StatusBarStates')
  4. const { h } = require('preact')
  5. function calculateProcessingProgress (files) {
  6. // Collect pre or postprocessing progress states.
  7. const progresses = []
  8. Object.keys(files).forEach((fileID) => {
  9. const { progress } = files[fileID]
  10. if (progress.preprocess) {
  11. progresses.push(progress.preprocess)
  12. }
  13. if (progress.postprocess) {
  14. progresses.push(progress.postprocess)
  15. }
  16. })
  17. // In the future we should probably do this differently. For now we'll take the
  18. // mode and message from the first file…
  19. const { mode, message } = progresses[0]
  20. const value = progresses.filter(isDeterminate).reduce((total, progress, index, all) => {
  21. return total + progress.value / all.length
  22. }, 0)
  23. function isDeterminate (progress) {
  24. return progress.mode === 'determinate'
  25. }
  26. return {
  27. mode,
  28. message,
  29. value
  30. }
  31. }
  32. function togglePauseResume (props) {
  33. if (props.isAllComplete) return
  34. if (!props.resumableUploads) {
  35. return props.cancelAll()
  36. }
  37. if (props.isAllPaused) {
  38. return props.resumeAll()
  39. }
  40. return props.pauseAll()
  41. }
  42. module.exports = (props) => {
  43. props = props || {}
  44. const uploadState = props.uploadState
  45. let progressValue = props.totalProgress
  46. let progressMode
  47. let progressBarContent
  48. if (uploadState === statusBarStates.STATE_PREPROCESSING || uploadState === statusBarStates.STATE_POSTPROCESSING) {
  49. const progress = calculateProcessingProgress(props.files)
  50. progressMode = progress.mode
  51. if (progressMode === 'determinate') {
  52. progressValue = progress.value * 100
  53. }
  54. progressBarContent = ProgressBarProcessing(progress)
  55. } else if (uploadState === statusBarStates.STATE_COMPLETE) {
  56. progressBarContent = ProgressBarComplete(props)
  57. } else if (uploadState === statusBarStates.STATE_UPLOADING) {
  58. progressBarContent = ProgressBarUploading(props)
  59. } else if (uploadState === statusBarStates.STATE_ERROR) {
  60. progressValue = undefined
  61. progressBarContent = ProgressBarError(props)
  62. }
  63. const width = typeof progressValue === 'number' ? progressValue : 100
  64. const isHidden = (uploadState === statusBarStates.STATE_WAITING && props.hideUploadButton) ||
  65. (uploadState === statusBarStates.STATE_WAITING && !props.newFiles > 0) ||
  66. (uploadState === statusBarStates.STATE_COMPLETE && props.hideAfterFinish) ||
  67. !props.allowNewUpload
  68. const progressClassNames = `uppy-StatusBar-progress
  69. ${progressMode ? 'is-' + progressMode : ''}`
  70. const statusBarClassNames = classNames(
  71. { 'uppy-Root': props.isTargetDOMEl },
  72. 'uppy-StatusBar',
  73. `is-${uploadState}`,
  74. { 'uppy-StatusBar--detailedProgress': props.showProgressDetails }
  75. )
  76. return (
  77. <div class={statusBarClassNames} aria-hidden={isHidden}>
  78. <div class={progressClassNames}
  79. style={{ width: width + '%' }}
  80. role="progressbar"
  81. aria-valuemin="0"
  82. aria-valuemax="100"
  83. aria-valuenow={progressValue} />
  84. {progressBarContent}
  85. <div class="uppy-StatusBar-actions">
  86. { props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} uploadState={uploadState} /> : null }
  87. { props.error && !props.hideRetryButton ? <RetryBtn {...props} /> : null }
  88. { !props.hidePauseResumeCancelButtons && uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
  89. ? <CancelBtn {...props} />
  90. : null
  91. }
  92. </div>
  93. </div>
  94. )
  95. }
  96. const UploadBtn = (props) => {
  97. const uploadBtnClassNames = classNames(
  98. 'uppy-u-reset',
  99. 'uppy-c-btn',
  100. 'uppy-StatusBar-actionBtn',
  101. 'uppy-StatusBar-actionBtn--upload',
  102. { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING }
  103. )
  104. return <button type="button"
  105. class={uploadBtnClassNames}
  106. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  107. onclick={props.startUpload}>
  108. {props.newFiles && props.uploadStarted
  109. ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
  110. : props.i18n('uploadXFiles', { smart_count: props.newFiles })
  111. }
  112. </button>
  113. }
  114. const RetryBtn = (props) => {
  115. return <button type="button"
  116. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
  117. aria-label={props.i18n('retryUpload')}
  118. onclick={props.retryAll}>{props.i18n('retry')}</button>
  119. }
  120. const CancelBtn = (props) => {
  121. return <button type="button"
  122. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--cancel"
  123. aria-label={props.i18n('cancel')}
  124. onclick={props.cancelAll}>{props.i18n('cancel')}</button>
  125. }
  126. const PauseResumeButtons = (props) => {
  127. const { resumableUploads, isAllPaused, i18n } = props
  128. const title = resumableUploads
  129. ? isAllPaused
  130. ? i18n('resumeUpload')
  131. : i18n('pauseUpload')
  132. : i18n('cancelUpload')
  133. return <button title={title} class="uppy-u-reset uppy-StatusBar-statusIndicator" type="button" onclick={() => togglePauseResume(props)}>
  134. {resumableUploads
  135. ? isAllPaused
  136. ? <svg aria-hidden="true" class="UppyIcon" width="15" height="17" viewBox="0 0 11 13">
  137. <path d="M1.26 12.534a.67.67 0 0 1-.674.012.67.67 0 0 1-.336-.583v-11C.25.724.38.5.586.382a.658.658 0 0 1 .673.012l9.165 5.5a.66.66 0 0 1 .325.57.66.66 0 0 1-.325.573l-9.166 5.5z" />
  138. </svg>
  139. : <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
  140. <path d="M4.888.81v11.38c0 .446-.324.81-.722.81H2.722C2.324 13 2 12.636 2 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81zM9.888.81v11.38c0 .446-.324.81-.722.81H7.722C7.324 13 7 12.636 7 12.19V.81c0-.446.324-.81.722-.81h1.444c.398 0 .722.364.722.81z" />
  141. </svg>
  142. : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
  143. <path d="M17.318 17.232L9.94 9.854 9.586 9.5l-.354.354-7.378 7.378h.707l-.62-.62v.706L9.318 9.94l.354-.354-.354-.354L1.94 1.854v.707l.62-.62h-.706l7.378 7.378.354.354.354-.354 7.378-7.378h-.707l.622.62v-.706L9.854 9.232l-.354.354.354.354 7.378 7.378.708-.707-7.38-7.378v.708l7.38-7.38.353-.353-.353-.353-.622-.622-.353-.353-.354.352-7.378 7.38h.708L2.56 1.23 2.208.88l-.353.353-.622.62-.353.355.352.353 7.38 7.38v-.708l-7.38 7.38-.353.353.352.353.622.622.353.353.354-.353 7.38-7.38h-.708l7.38 7.38z" />
  144. </svg>
  145. }
  146. </button>
  147. }
  148. const ProgressBarProcessing = (props) => {
  149. const value = Math.round(props.value * 100)
  150. return <div class="uppy-StatusBar-content">
  151. {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
  152. {props.message}
  153. </div>
  154. }
  155. const ProgressDetails = (props) => {
  156. return <div class="uppy-StatusBar-statusSecondary">
  157. { props.numUploads > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.numUploads }) + ' \u00B7 ' }
  158. { props.i18n('dataUploadedOfTotal', { complete: props.totalUploadedSize, total: props.totalSize }) + ' \u00B7 ' }
  159. { props.i18n('xTimeLeft', { time: props.totalETA }) }
  160. </div>
  161. }
  162. const ThrottledProgressDetails = throttle(ProgressDetails, 500, { leading: true, trailing: true })
  163. const ProgressBarUploading = (props) => {
  164. if (!props.isUploadStarted || props.isAllComplete) {
  165. return null
  166. }
  167. const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
  168. return (
  169. <div class="uppy-StatusBar-content" aria-label={title} title={title}>
  170. { !props.hidePauseResumeCancelButtons && <PauseResumeButtons {...props} /> }
  171. <div class="uppy-StatusBar-status">
  172. <div class="uppy-StatusBar-statusPrimary">{title}: {props.totalProgress}%</div>
  173. { !props.isAllPaused && <ThrottledProgressDetails {...props} /> }
  174. </div>
  175. </div>
  176. )
  177. }
  178. const ProgressBarComplete = ({ totalProgress, i18n }) => {
  179. return (
  180. <div class="uppy-StatusBar-content" role="status" title={i18n('complete')}>
  181. <svg aria-hidden="true" class="uppy-StatusBar-statusIndicator UppyIcon" width="18" height="17" viewBox="0 0 23 17">
  182. <path d="M8.944 17L0 7.865l2.555-2.61 6.39 6.525L20.41 0 23 2.645z" />
  183. </svg>
  184. {i18n('complete')}
  185. </div>
  186. )
  187. }
  188. const ProgressBarError = ({ error, retryAll, hideRetryButton, i18n }) => {
  189. return (
  190. <div class="uppy-StatusBar-content" role="alert">
  191. <span class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</span>
  192. { !hideRetryButton && <span class="uppy-StatusBar-contentPadding">{i18n('pleasePressRetry')}</span> }
  193. <span class="uppy-StatusBar-details"
  194. aria-label={error}
  195. data-microtip-position="top"
  196. data-microtip-size="large"
  197. role="tooltip">?</span>
  198. </div>
  199. )
  200. }