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. const progressClassNames = `uppy-StatusBar-progress
  68. ${progressMode ? 'is-' + progressMode : ''}`
  69. const statusBarClassNames = classNames(
  70. 'uppy',
  71. 'uppy-StatusBar',
  72. `is-${uploadState}`,
  73. { 'uppy-StatusBar--detailedProgress': props.showProgressDetails }
  74. )
  75. return (
  76. <div class={statusBarClassNames} aria-hidden={isHidden}>
  77. <div class={progressClassNames}
  78. style={{ width: width + '%' }}
  79. role="progressbar"
  80. aria-valuemin="0"
  81. aria-valuemax="100"
  82. aria-valuenow={progressValue} />
  83. {progressBarContent}
  84. <div class="uppy-StatusBar-actions">
  85. { props.newFiles && !props.hideUploadButton ? <UploadBtn {...props} uploadState={uploadState} /> : null }
  86. { props.error && !props.hideRetryButton ? <RetryBtn {...props} /> : null }
  87. { !props.hidePauseResumeCancelButtons && uploadState !== statusBarStates.STATE_WAITING && uploadState !== statusBarStates.STATE_COMPLETE
  88. ? <CancelBtn {...props} />
  89. : null
  90. }
  91. </div>
  92. </div>
  93. )
  94. }
  95. const UploadBtn = (props) => {
  96. const uploadBtnClassNames = classNames(
  97. 'uppy-u-reset',
  98. 'uppy-c-btn',
  99. 'uppy-StatusBar-actionBtn',
  100. 'uppy-StatusBar-actionBtn--upload',
  101. { 'uppy-c-btn-primary': props.uploadState === statusBarStates.STATE_WAITING }
  102. )
  103. return <button type="button"
  104. class={uploadBtnClassNames}
  105. aria-label={props.i18n('uploadXFiles', { smart_count: props.newFiles })}
  106. onclick={props.startUpload}>
  107. {props.newFiles && props.uploadStarted
  108. ? props.i18n('uploadXNewFiles', { smart_count: props.newFiles })
  109. : props.i18n('uploadXFiles', { smart_count: props.newFiles })
  110. }
  111. </button>
  112. }
  113. const RetryBtn = (props) => {
  114. return <button type="button"
  115. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--retry"
  116. aria-label={props.i18n('retryUpload')}
  117. onclick={props.retryAll}>{props.i18n('retry')}</button>
  118. }
  119. const CancelBtn = (props) => {
  120. return <button type="button"
  121. class="uppy-u-reset uppy-c-btn uppy-StatusBar-actionBtn uppy-StatusBar-actionBtn--cancel"
  122. aria-label={props.i18n('cancel')}
  123. onclick={props.cancelAll}>{props.i18n('cancel')}</button>
  124. }
  125. const PauseResumeButtons = (props) => {
  126. const { resumableUploads, isAllPaused, i18n } = props
  127. const title = resumableUploads
  128. ? isAllPaused
  129. ? i18n('resumeUpload')
  130. : i18n('pauseUpload')
  131. : i18n('cancelUpload')
  132. return <button title={title} class="uppy-u-reset uppy-StatusBar-statusIndicator" type="button" onclick={() => togglePauseResume(props)}>
  133. {resumableUploads
  134. ? isAllPaused
  135. ? <svg aria-hidden="true" class="UppyIcon" width="15" height="17" viewBox="0 0 11 13">
  136. <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" />
  137. </svg>
  138. : <svg aria-hidden="true" class="UppyIcon" width="16" height="17" viewBox="0 0 12 13">
  139. <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" />
  140. </svg>
  141. : <svg aria-hidden="true" class="UppyIcon" width="16px" height="16px" viewBox="0 0 19 19">
  142. <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" />
  143. </svg>
  144. }
  145. </button>
  146. }
  147. const ProgressBarProcessing = (props) => {
  148. const value = Math.round(props.value * 100)
  149. return <div class="uppy-StatusBar-content">
  150. {props.mode === 'determinate' ? `${value}% \u00B7 ` : ''}
  151. {props.message}
  152. </div>
  153. }
  154. const progressDetails = (props) => {
  155. return <span class="uppy-StatusBar-statusSecondary">
  156. { props.inProgress > 1 && props.i18n('filesUploadedOfTotal', { complete: props.complete, smart_count: props.inProgress }) + ' \u00B7 ' }
  157. { props.i18n('dataUploadedOfTotal', { complete: props.totalUploadedSize, total: props.totalSize }) + ' \u00B7 ' }
  158. { props.i18n('xTimeLeft', { time: props.totalETA }) }
  159. </span>
  160. }
  161. const ThrottledProgressDetails = throttle(progressDetails, 500, { leading: true, trailing: true })
  162. const ProgressBarUploading = (props) => {
  163. if (!props.isUploadStarted || props.isAllComplete) {
  164. return null
  165. }
  166. const title = props.isAllPaused ? props.i18n('paused') : props.i18n('uploading')
  167. return (
  168. <div class="uppy-StatusBar-content" aria-label={title} title={title}>
  169. { !props.hidePauseResumeCancelButtons && <PauseResumeButtons {...props} /> }
  170. <div class="uppy-StatusBar-status">
  171. <span class="uppy-StatusBar-statusPrimary">{title}: {props.totalProgress}%</span>
  172. <br />
  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. <strong class="uppy-StatusBar-contentPadding">{i18n('uploadFailed')}.</strong>
  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. }