Browse Source

Pause/resume all uploads at once #65

Artur Paikin 8 years ago
parent
commit
baf8e361f1

+ 17 - 8
src/core/Core.js

@@ -126,6 +126,7 @@ export default class Core {
       data: file.data,
       progress: {
         percentage: 0,
+        uploadComplete: false,
         uploadStarted: false
       },
       size: file.data.size,
@@ -180,15 +181,17 @@ export default class Core {
     // `remove-file` removes a file from `state.files`, for example when
     // a user decides not to upload particular file and clicks a button to remove it
     this.emitter.on('file-remove', (fileID) => {
-      const updatedFiles = Object.assign({}, this.state.files)
+      const updatedFiles = Object.assign({}, this.getState().files)
       delete updatedFiles[fileID]
       this.setState({files: updatedFiles})
     })
 
-    this.emitter.on('core:file-upload-started', (fileID) => {
+    this.emitter.on('core:file-upload-started', (fileID, upload) => {
       const updatedFiles = Object.assign({}, this.getState().files)
       const updatedFile = Object.assign({}, updatedFiles[fileID],
         Object.assign({}, {
+          // can’t do that, because immutability. ??
+          // upload: upload,
           progress: Object.assign({}, updatedFiles[fileID].progress, {
             uploadStarted: Date.now()
           })
@@ -196,14 +199,16 @@ export default class Core {
       ))
       updatedFiles[fileID] = updatedFile
 
-      this.setState({
-        files: updatedFiles
-      })
+      this.setState({files: updatedFiles})
     })
 
     this.emitter.on('upload-progress', (data) => {
       const fileID = data.id
       const updatedFiles = Object.assign({}, this.getState().files)
+      if (!updatedFiles[fileID]) {
+        console.error('Trying to set progress for a file that’s not with us anymore: ', fileID)
+        return
+      }
 
       const updatedFile = Object.assign({}, updatedFiles[fileID],
         Object.assign({}, {
@@ -218,12 +223,13 @@ export default class Core {
 
       // calculate total progress, using the number of files currently uploading,
       // multiplied by 100 and the summ of individual progress of each file
-      const inProgress = Object.keys(updatedFiles).map((file) => {
-        return file.progress !== 0
+      const inProgress = Object.keys(updatedFiles).filter((file) => {
+        return !updatedFiles[file].progress.uploadComplete &&
+               updatedFiles[file].progress.uploadStarted
       })
       const progressMax = Object.keys(inProgress).length * 100
       let progressAll = 0
-      Object.keys(updatedFiles).forEach((file) => {
+      inProgress.forEach((file) => {
         progressAll = progressAll + updatedFiles[file].progress.percentage
       })
 
@@ -238,6 +244,9 @@ export default class Core {
     this.emitter.on('upload-success', (fileID, uploadURL) => {
       const updatedFiles = Object.assign({}, this.getState().files)
       const updatedFile = Object.assign({}, updatedFiles[fileID], {
+        progress: Object.assign({}, updatedFiles[fileID].progress, {
+          uploadComplete: true
+        }),
         uploadURL: uploadURL
       })
       updatedFiles[fileID] = updatedFile

+ 55 - 19
src/plugins/Dashboard/Dashboard.js

@@ -2,7 +2,7 @@ import html from '../../core/html'
 import Utils from '../../core/Utils'
 import FileItem from './FileItem'
 import FileCard from './FileCard'
-import { closeIcon, localIcon, uploadIcon, dashboardBgIcon } from './icons'
+import { closeIcon, localIcon, uploadIcon, dashboardBgIcon, iconPause, iconResume } from './icons'
 
 export default function Dashboard (props, bus) {
   // http://dev.edenspiekermann.com/2016/02/11/introducing-accessible-modal-dialog
@@ -12,12 +12,12 @@ export default function Dashboard (props, bus) {
   const modal = state.modal
   const container = props.container
   const showFileCard = modal.showFileCard
+  const showProgressDetails = props.showProgressDetails
 
   const hideModal = props.hideModal
   const hideAllPanels = props.hideAllPanels
   const showPanel = props.showPanel
   const log = props.log
-  // const handleInputChange = props.handleInputChange
 
   const acquirers = modal.targets.filter((target) => {
     return target.type === 'acquirer'
@@ -55,13 +55,40 @@ export default function Dashboard (props, bus) {
     })
   }
 
-  const selectedFiles = Object.keys(files).filter((file) => {
-    return files[file].progress.percentage === 0
-    // return files[file].progress !== 100
+  const newFiles = Object.keys(files).filter((file) => {
+    return !files[file].progress.uploadStarted
   })
+  const uploadStartedFiles = Object.keys(files).filter((file) => {
+    return files[file].progress.uploadStarted
+  })
+  const completeFiles = Object.keys(files).filter((file) => {
+    return files[file].progress.uploadComplete
+  })
+  const inProgressFiles = Object.keys(files).filter((file) => {
+    return !files[file].progress.uploadComplete &&
+           files[file].progress.uploadStarted &&
+           !files[file].isPaused
+  })
+
+  const uploadStartedFilesCount = Object.keys(uploadStartedFiles).length
+  const completeFilesCount = Object.keys(completeFiles).length
+  const inProgressFilesCount = Object.keys(inProgressFiles).length
   const totalFileCount = Object.keys(files).length
-  const selectedFileCount = Object.keys(selectedFiles).length
-  const isSomethingSelected = selectedFileCount > 0
+  const newFileCount = Object.keys(newFiles).length
+
+  function renderPauseResume () {
+    if (uploadStartedFilesCount > 0) {
+      if (inProgressFilesCount > 0) {
+        return html`<button class="UppyDashboard-pauseResume UppyButton--circular UppyButton--yellow UppyButton--sizeS"
+                            onclick=${() => bus.emit('core:pause-all')}>${iconPause()}</button>`
+      }
+
+      if (uploadStartedFilesCount !== completeFilesCount) {
+        return html`<button class="UppyDashboard-pauseResume UppyButton--circular UppyButton--green UppyButton--sizeS"
+                            onclick=${() => bus.emit('core:resume-all')}>${iconResume()}</button>`
+      }
+    }
+  }
 
   return html`<div class="Uppy UppyTheme--default UppyDashboard ${isTouchDevice ? 'Uppy--isTouchDevice' : ''}"
                  aria-hidden="${modal.isHidden}"
@@ -119,22 +146,31 @@ export default function Dashboard (props, bus) {
           <ul class="UppyDashboard-filesInner">
             ${totalFileCount === 0
               ? html`<div class="UppyDashboard-bgIcon">${dashboardBgIcon()}</div>`
-              : ''
+              : null
             }
             ${Object.keys(files).map((fileID) => {
-              return FileItem(files[fileID], bus)
+              return FileItem(
+                {file: files[fileID], showProgressDetails}, bus
+              )
             })}
           </ul>
-          ${!props.autoProceed && isSomethingSelected
-            ? html`<button class="UppyButton--circular UppyDashboard-upload"
-                           type="button"
-                           title="Upload"
-                           onclick=${next}>
-                      ${uploadIcon()}
-                      <sup class="UppyDashboard-uploadCount">${selectedFileCount}</sup>
-                   </button>`
-            : null
-          }
+          <div class="UppyDashboard-actions">
+            ${renderPauseResume()}
+            ${!props.autoProceed && newFileCount > 0
+              ? html`<button class="UppyButton--circular UppyButton--blue UppyButton--sizeM UppyDashboard-upload"
+                             type="button"
+                             title="Upload all files"
+                             aria-label="Upload all files"
+                             onclick=${next}>
+                        ${uploadIcon()}
+                        <sup class="UppyDashboard-uploadCount"
+                             title="Number of selected files"
+                             aria-label="Number of selected files">
+                              ${newFileCount}</sup>
+                     </button>`
+              : null
+            }
+          </div>
         </div>
 
         ${acquirers.map((target) => {

+ 34 - 14
src/plugins/Dashboard/FileItem.js

@@ -41,10 +41,13 @@ function getSpeed (fileProgress) {
   return uploadSpeed
 }
 
-export default function fileItem (file, bus) {
-  const isUploaded = file.progress.percentage === 100
-  const uploadInProgressOrComplete = file.progress.percentage > 0
-  const uploadInProgress = file.progress.percentage > 0 && file.progress.percentage < 100
+export default function fileItem (props, bus) {
+  const file = props.file
+  const showProgressDetails = props.showProgressDetails
+
+  const isUploaded = file.progress.uploadComplete
+  const uploadInProgressOrComplete = file.progress.uploadStarted
+  const uploadInProgress = file.progress.uploadStarted && !file.progress.uploadComplete
   const isPaused = file.isPaused || false
 
   const fileName = Utils.getFileNameAndExtension(file.meta.name)[0]
@@ -72,19 +75,31 @@ export default function fileItem (file, bus) {
           : getIconByMime(file.type.general)
         }
         <div class="UppyDashboardItem-progress">
-          <button class="UppyDashboardItem-progressBtn" onclick=${(e) => {
-            if (file.progress.percentage === 100) return
-            bus.emit('core:upload-pause', file.id)
-          }}>
+          <button class="UppyDashboardItem-progressBtn"
+                  title="${isUploaded
+                          ? 'upload complete'
+                          : file.isPaused ? 'resume upload' : 'pause upload'}"
+                  onclick=${(e) => {
+                    if (isUploaded) return
+                    bus.emit('core:upload-pause', file.id)
+                  }}>
             ${FileItemProgress({progress: file.progress.percentage, fileID: file.id}, bus)}
           </button>
-          <div class="UppyDashboardItem-progressInfo">
-            ${getETA(file.progress)} ・ ↑ ${prettyBytes(getSpeed(file.progress))} / s
-          </div>
+          ${showProgressDetails
+            ? html`<div class="UppyDashboardItem-progressInfo"
+                   title="File progress: upload speed and ETA"
+                   aria-label="File progress: upload speed and ETA">
+                ${!file.isPaused && !isUploaded
+                  ? html`<span>${getETA(file.progress)} ・ ↑ ${prettyBytes(getSpeed(file.progress))}/s</span>`
+                  : null
+                }
+              </div>`
+            : null
+          }
         </div>
       </div>
     <div class="UppyDashboardItem-info">
-      <h4 class="UppyDashboardItem-name">
+      <h4 class="UppyDashboardItem-name" title="${fileName}">
         ${file.uploadURL
           ? html`<a href="${file.uploadURL}" target="_blank">${file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName}</a>`
           : file.extension ? truncatedFileName + '.' + file.extension : truncatedFileName
@@ -95,14 +110,19 @@ export default function fileItem (file, bus) {
       </div>
       ${!uploadInProgressOrComplete
         ? html`<button class="UppyDashboardItem-edit"
-              onclick=${(e) => bus.emit('dashboard:file-card', file.id)}>${iconEdit()}</button>`
+                       aria-label="Edit file"
+                       title="Edit file"
+                       onclick=${(e) => bus.emit('dashboard:file-card', file.id)}>
+                        ${iconEdit()}
+                      </button>`
         : null
       }
     </div>
     <div class="UppyDashboardItem-action">
       ${!isUploaded
         ? html`<button class="UppyDashboardItem-remove"
-                       aria-label="Remove this file"
+                       aria-label="Remove file"
+                       title="Remove file"
                        onclick=${remove}>
                   ${removeIcon()}
                </button>`

+ 15 - 6
src/plugins/Dashboard/icons.js

@@ -9,6 +9,21 @@ export function defaultTabIcon () {
   </svg>`
 }
 
+export function iconResume () {
+  return html`<svg class="UppyIcon" width="25" height="25" viewBox="0 0 44 44">
+    <polygon class="play" transform="translate(6, 5.5)" points="13 21.6666667 13 11 21 16.3333333" />
+  </svg>`
+}
+
+export function iconPause () {
+  return html`<svg class="UppyIcon" width="25px" height="25px" viewBox="0 0 44 44">
+    <g transform="translate(18, 17)" class="pause">
+      <rect x="0" y="0" width="2" height="10" rx="0" />
+      <rect x="6" y="0" width="2" height="10" rx="0" />
+    </g>
+  </svg>`
+}
+
 export function iconEdit () {
   return html`<svg class="UppyIcon UppyIcon-edit" width="28" height="28" viewBox="0 0 28 28">
     <path d="M25.436 2.566a7.98 7.98 0 0 0-2.078-1.51C22.638.703 21.906.5 21.198.5a3 3 0 0 0-1.023.17 2.436 2.436 0 0 0-.893.562L2.292 18.217.5 27.5l9.28-1.796 16.99-16.99c.255-.254.444-.56.562-.888a3 3 0 0 0 .17-1.023c0-.708-.205-1.44-.555-2.16a8 8 0 0 0-1.51-2.077zM9.01 24.252l-4.313.834c0-.03.008-.06.012-.09.007-.944-.74-1.715-1.67-1.723-.04 0-.078.007-.118.01l.83-4.29L17.72 5.024l5.264 5.264L9.01 24.252zm16.84-16.96a.818.818 0 0 1-.194.31l-1.57 1.57-5.26-5.26 1.57-1.57a.82.82 0 0 1 .31-.194 1.45 1.45 0 0 1 .492-.074c.397 0 .917.126 1.468.397.55.27 1.13.678 1.656 1.21.53.53.94 1.11 1.208 1.655.272.55.397 1.07.393 1.468.004.193-.027.358-.074.488z" />
@@ -22,12 +37,6 @@ export function localIcon () {
   </svg>`
 }
 
-// export function backIcon () {
-//   return html`<svg class="UppyIcon" width="7px" height="10px" viewBox="0 0 23 45">
-//     <polygon points="21.2678133 0.318896058 1.26781326 21.8188961 0.634228199 22.5 1.26781326 23.1811039 21.2678133 44.6811039 22.7321867 43.3188961 2.73218674 21.8188961 2.73218674 23.1811039 22.7321867 1.68110394"></polygon>
-//   </svg>`
-// }
-
 export function closeIcon () {
   return html`<svg class="UppyIcon" width="14px" height="14px" viewBox="0 0 19 19">
     <polygon points="17.3182539 17.2324466 9.93955339 9.85374611 9.586 9.50019272 9.23244661 9.85374611 1.85374611 17.2324466 2.56085289 17.2324466 1.93955339 16.6111471 1.93955865 17.3182486 9.31803946 9.93954813 9.67158232 9.58599474 9.31803419 9.23244661 1.93955339 1.85396581 1.93961588 2.56101008 2.56091538 1.93949089 1.85375137 1.93955865 9.23245187 9.31803946 9.586 9.67157706 9.93954813 9.31803946 17.3182486 1.93955865 16.6111471 1.93955339 17.2324466 2.56085289 17.2324466 1.85374611 9.85374611 9.23244661 9.50019272 9.586 9.85374611 9.93955339 17.2324466 17.3182539 17.9395534 16.6111471 10.5608529 9.23244661 10.5608529 9.93955339 17.9395534 2.56085289 18.2931068 2.2072995 17.9395534 1.85374611 17.3182539 1.23244661 16.9647058 0.878898482 16.6111524 1.23244135 9.23245187 8.61092215 9.93954813 8.61092215 2.56084763 1.23244135 2.20723173 0.87883598 1.85368362 1.23250911 1.23238412 1.85402831 0.878955712 2.20758169 1.23244661 2.56107259 8.61092741 9.93955339 8.61092215 9.23245187 1.23244135 16.6111524 0.878898482 16.9647058 1.23244661 17.3182539 1.85374611 17.9395534 2.2072995 18.2931068 2.56085289 17.9395534 9.93955339 10.5608529 9.23244661 10.5608529 16.6111471 17.9395534"></polygon>

+ 81 - 19
src/plugins/Tus10.js

@@ -15,33 +15,69 @@ export default class Tus10 extends Plugin {
 
     // set default options
     const defaultOptions = {
-      resume: true
+      resume: true,
+      allowPause: true
     }
 
     // merge default options with the ones set by user
     this.opts = Object.assign({}, defaultOptions, opts)
   }
 
-  pauseUpload (fileID) {
+  pauseResume (action, fileID) {
     const updatedFiles = Object.assign({}, this.core.getState().files)
-    const wasPaused = updatedFiles[fileID].isPaused || false
-    const isPaused = !wasPaused
-    let updatedFile
-    if (wasPaused) {
-      updatedFile = Object.assign({}, updatedFiles[fileID], {
-        isPaused: false
-      })
-    } else {
-      updatedFile = Object.assign({}, updatedFiles[fileID], {
-        isPaused: true
-      })
-    }
-    updatedFiles[fileID] = updatedFile
-    this.core.setState({files: updatedFiles})
+    const inProgressUpdatedFiles = Object.keys(updatedFiles).filter((file) => {
+      return !updatedFiles[file].progress.uploadComplete &&
+             updatedFiles[file].progress.uploadStarted
+    })
 
-    return isPaused
+    switch (action) {
+      case 'toggle':
+        const wasPaused = updatedFiles[fileID].isPaused || false
+        const isPaused = !wasPaused
+        let updatedFile
+        if (wasPaused) {
+          updatedFile = Object.assign({}, updatedFiles[fileID], {
+            isPaused: false
+          })
+        } else {
+          updatedFile = Object.assign({}, updatedFiles[fileID], {
+            isPaused: true
+          })
+        }
+        updatedFiles[fileID] = updatedFile
+        this.core.setState({files: updatedFiles})
+        return isPaused
+      case 'pauseAll':
+        inProgressUpdatedFiles.forEach((file) => {
+          const updatedFile = Object.assign({}, updatedFiles[file], {
+            isPaused: true
+          })
+          updatedFiles[file] = updatedFile
+        })
+        this.core.setState({files: updatedFiles})
+        return
+      case 'resumeAll':
+        inProgressUpdatedFiles.forEach((file) => {
+          const updatedFile = Object.assign({}, updatedFiles[file], {
+            isPaused: false
+          })
+          updatedFiles[file] = updatedFile
+        })
+        this.core.setState({files: updatedFiles})
+        return
+    }
   }
 
+  // pauseResumeAll (action) {
+  //   let updatedFiles = Object.assign({}, this.core.getState().files)
+  //   updatedFiles = Object.keys(updatedFiles).map((file) => {
+  //     return Object.assign({}, updatedFiles[file], {
+  //       isPaused: action
+  //     })
+  //   })
+  //   this.core.setState({files: updatedFiles})
+  // }
+
 /**
  * Create a new Tus upload
  *
@@ -85,19 +121,33 @@ export default class Tus10 extends Plugin {
 
       this.core.emitter.on('file-remove', (fileID) => {
         if (fileID === file.id) {
+          console.log('removing file: ', fileID)
           upload.abort()
+          resolve(`upload ${fileID} was removed`)
         }
       })
 
       this.core.emitter.on('core:upload-pause', (fileID) => {
         if (fileID === file.id) {
-          const isPaused = this.pauseUpload(fileID)
+          const isPaused = this.pauseResume('toggle', fileID)
           isPaused ? upload.abort() : upload.start()
         }
       })
 
+      this.core.emitter.on('core:pause-all', () => {
+        const files = this.core.getState().files
+        if (!files[file.id]) return
+        upload.abort()
+      })
+
+      this.core.emitter.on('core:resume-all', () => {
+        const files = this.core.getState().files
+        if (!files[file.id]) return
+        upload.start()
+      })
+
       upload.start()
-      this.core.emitter.emit('core:file-upload-started', file.id)
+      this.core.emitter.emit('core:file-upload-started', file.id, upload)
     })
   }
 
@@ -194,7 +244,19 @@ export default class Tus10 extends Plugin {
     this.uploadFiles(filesForUpload)
   }
 
+  actions () {
+    this.core.emitter.on('core:pause-all', () => {
+      this.pauseResume('pauseAll')
+    })
+
+    this.core.emitter.on('core:resume-all', () => {
+      this.pauseResume('resumeAll')
+    })
+  }
+
   install () {
+    this.actions()
+
     const bus = this.core.emitter
     bus.on('core:upload', () => {
       this.core.log('Tus is uploading...')