StatusBar.jsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { UIPlugin } from '@uppy/core'
  2. import emaFilter from '@uppy/utils/lib/emaFilter'
  3. import getTextDirection from '@uppy/utils/lib/getTextDirection'
  4. import statusBarStates from './StatusBarStates.js'
  5. import StatusBarUI from './StatusBarUI.jsx'
  6. import packageJson from '../package.json'
  7. import locale from './locale.js'
  8. const speedFilterHalfLife = 2000
  9. const ETAFilterHalfLife = 2000
  10. function getUploadingState (error, isAllComplete, recoveredState, files) {
  11. if (error) {
  12. return statusBarStates.STATE_ERROR
  13. }
  14. if (isAllComplete) {
  15. return statusBarStates.STATE_COMPLETE
  16. }
  17. if (recoveredState) {
  18. return statusBarStates.STATE_WAITING
  19. }
  20. let state = statusBarStates.STATE_WAITING
  21. const fileIDs = Object.keys(files)
  22. for (let i = 0; i < fileIDs.length; i++) {
  23. const { progress } = files[fileIDs[i]]
  24. // If ANY files are being uploaded right now, show the uploading state.
  25. if (progress.uploadStarted && !progress.uploadComplete) {
  26. return statusBarStates.STATE_UPLOADING
  27. }
  28. // If files are being preprocessed AND postprocessed at this time, we show the
  29. // preprocess state. If any files are being uploaded we show uploading.
  30. if (progress.preprocess && state !== statusBarStates.STATE_UPLOADING) {
  31. state = statusBarStates.STATE_PREPROCESSING
  32. }
  33. // If NO files are being preprocessed or uploaded right now, but some files are
  34. // being postprocessed, show the postprocess state.
  35. if (
  36. progress.postprocess
  37. && state !== statusBarStates.STATE_UPLOADING
  38. && state !== statusBarStates.STATE_PREPROCESSING
  39. ) {
  40. state = statusBarStates.STATE_POSTPROCESSING
  41. }
  42. }
  43. return state
  44. }
  45. /**
  46. * StatusBar: renders a status bar with upload/pause/resume/cancel/retry buttons,
  47. * progress percentage and time remaining.
  48. */
  49. export default class StatusBar extends UIPlugin {
  50. static VERSION = packageJson.version
  51. #lastUpdateTime
  52. #previousUploadedBytes
  53. #previousSpeed
  54. #previousETA
  55. constructor (uppy, opts) {
  56. super(uppy, opts)
  57. this.id = this.opts.id || 'StatusBar'
  58. this.title = 'StatusBar'
  59. this.type = 'progressindicator'
  60. this.defaultLocale = locale
  61. // set default options, must be kept in sync with @uppy/react/src/StatusBar.js
  62. const defaultOptions = {
  63. target: 'body',
  64. hideUploadButton: false,
  65. hideRetryButton: false,
  66. hidePauseResumeButton: false,
  67. hideCancelButton: false,
  68. showProgressDetails: false,
  69. hideAfterFinish: true,
  70. doneButtonHandler: null,
  71. }
  72. this.opts = { ...defaultOptions, ...opts }
  73. this.i18nInit()
  74. this.render = this.render.bind(this)
  75. this.install = this.install.bind(this)
  76. }
  77. #computeSmoothETA (totalBytes) {
  78. if (totalBytes.total === 0 || totalBytes.remaining === 0) {
  79. return 0
  80. }
  81. // When state is restored, lastUpdateTime is still nullish at this point.
  82. this.#lastUpdateTime ??= performance.now()
  83. const dt = performance.now() - this.#lastUpdateTime
  84. if (dt === 0) {
  85. return Math.round((this.#previousETA ?? 0) / 100) / 10
  86. }
  87. const uploadedBytesSinceLastTick = totalBytes.uploaded - this.#previousUploadedBytes
  88. this.#previousUploadedBytes = totalBytes.uploaded
  89. // uploadedBytesSinceLastTick can be negative in some cases (packet loss?)
  90. // in which case, we wait for next tick to update ETA.
  91. if (uploadedBytesSinceLastTick <= 0) {
  92. return Math.round((this.#previousETA ?? 0) / 100) / 10
  93. }
  94. const currentSpeed = uploadedBytesSinceLastTick / dt
  95. const filteredSpeed = this.#previousSpeed == null
  96. ? currentSpeed
  97. : emaFilter(currentSpeed, this.#previousSpeed, speedFilterHalfLife, dt)
  98. this.#previousSpeed = filteredSpeed
  99. const instantETA = totalBytes.remaining / filteredSpeed
  100. const updatedPreviousETA = Math.max(this.#previousETA - dt, 0)
  101. const filteredETA = this.#previousETA == null
  102. ? instantETA
  103. : emaFilter(instantETA, updatedPreviousETA, ETAFilterHalfLife, dt)
  104. this.#previousETA = filteredETA
  105. this.#lastUpdateTime = performance.now()
  106. return Math.round(filteredETA / 100) / 10
  107. }
  108. startUpload = () => {
  109. const { recoveredState } = this.uppy.getState()
  110. this.#previousSpeed = null
  111. this.#previousETA = null
  112. if (recoveredState) {
  113. this.#previousUploadedBytes = Object.values(recoveredState.files)
  114. .reduce((pv, { progress }) => pv + progress.bytesUploaded, 0)
  115. // We don't set `#lastUpdateTime` at this point because the upload won't
  116. // actually resume until the user asks for it.
  117. this.uppy.emit('restore-confirmed')
  118. return undefined
  119. }
  120. this.#lastUpdateTime = performance.now()
  121. this.#previousUploadedBytes = 0
  122. return this.uppy.upload().catch(() => {
  123. // Error logged in Core
  124. })
  125. }
  126. render (state) {
  127. const {
  128. capabilities,
  129. files,
  130. allowNewUpload,
  131. totalProgress,
  132. error,
  133. recoveredState,
  134. } = state
  135. const {
  136. newFiles,
  137. startedFiles,
  138. completeFiles,
  139. isUploadStarted,
  140. isAllComplete,
  141. isAllErrored,
  142. isAllPaused,
  143. isUploadInProgress,
  144. isSomeGhost,
  145. } = this.uppy.getObjectOfFilesPerState()
  146. // If some state was recovered, we want to show Upload button/counter
  147. // for all the files, because in this case it’s not an Upload button,
  148. // but “Confirm Restore Button”
  149. const newFilesOrRecovered = recoveredState
  150. ? Object.values(files)
  151. : newFiles
  152. const resumableUploads = !!capabilities.resumableUploads
  153. const supportsUploadProgress = capabilities.uploadProgress !== false
  154. let totalSize = 0
  155. let totalUploadedSize = 0
  156. startedFiles.forEach((file) => {
  157. totalSize += file.progress.bytesTotal || 0
  158. totalUploadedSize += file.progress.bytesUploaded || 0
  159. })
  160. const totalETA = this.#computeSmoothETA({
  161. uploaded: totalUploadedSize,
  162. total: totalSize,
  163. remaining: totalSize - totalUploadedSize,
  164. })
  165. return StatusBarUI({
  166. error,
  167. uploadState: getUploadingState(
  168. error,
  169. isAllComplete,
  170. recoveredState,
  171. state.files || {},
  172. ),
  173. allowNewUpload,
  174. totalProgress,
  175. totalSize,
  176. totalUploadedSize,
  177. isAllComplete: false,
  178. isAllPaused,
  179. isAllErrored,
  180. isUploadStarted,
  181. isUploadInProgress,
  182. isSomeGhost,
  183. recoveredState,
  184. complete: completeFiles.length,
  185. newFiles: newFilesOrRecovered.length,
  186. numUploads: startedFiles.length,
  187. totalETA,
  188. files,
  189. i18n: this.i18n,
  190. uppy: this.uppy,
  191. startUpload: this.startUpload,
  192. doneButtonHandler: this.opts.doneButtonHandler,
  193. resumableUploads,
  194. supportsUploadProgress,
  195. showProgressDetails: this.opts.showProgressDetails,
  196. hideUploadButton: this.opts.hideUploadButton,
  197. hideRetryButton: this.opts.hideRetryButton,
  198. hidePauseResumeButton: this.opts.hidePauseResumeButton,
  199. hideCancelButton: this.opts.hideCancelButton,
  200. hideAfterFinish: this.opts.hideAfterFinish,
  201. isTargetDOMEl: this.isTargetDOMEl,
  202. })
  203. }
  204. onMount () {
  205. // Set the text direction if the page has not defined one.
  206. const element = this.el
  207. const direction = getTextDirection(element)
  208. if (!direction) {
  209. element.dir = 'ltr'
  210. }
  211. }
  212. install () {
  213. const { target } = this.opts
  214. if (target) {
  215. this.mount(target, this)
  216. }
  217. }
  218. uninstall () {
  219. this.unmount()
  220. }
  221. }