瀏覽代碼

Merge branch 'master' into feature/s3

Artur Paikin 7 年之前
父節點
當前提交
23caa332e9
共有 55 個文件被更改,包括 1172 次插入379 次删除
  1. 47 26
      CHANGELOG.md
  2. 1 0
      examples/bundled-example/main.js
  3. 109 29
      src/core/Core.js
  4. 11 4
      src/generic-provider-views/AuthView.js
  5. 0 9
      src/generic-provider-views/Error.js
  6. 52 20
      src/generic-provider-views/index.js
  7. 54 0
      src/locales/nb_NO.js
  8. 54 0
      src/locales/tr_TR.js
  9. 10 8
      src/plugins/Dashboard/Dashboard.js
  10. 9 9
      src/plugins/Dashboard/FileItem.js
  11. 1 1
      src/plugins/Dashboard/Tabs.js
  12. 12 14
      src/plugins/Dashboard/index.js
  13. 1 1
      src/plugins/DragDrop/index.js
  14. 1 5
      src/plugins/Dropbox/index.js
  15. 1 1
      src/plugins/FileInput.js
  16. 1 5
      src/plugins/GoogleDrive/index.js
  17. 6 62
      src/plugins/Informer.js
  18. 18 8
      src/plugins/Instagram/index.js
  19. 1 1
      src/plugins/Plugin.js
  20. 3 3
      src/plugins/StatusBar/index.js
  21. 26 24
      src/plugins/Tus10.js
  22. 6 6
      src/plugins/Webcam/index.js
  23. 10 10
      src/plugins/XHRUpload.js
  24. 29 29
      src/scss/_dashboard.scss
  25. 39 5
      src/uppy-base/src/plugins/Provider.js
  26. 1 1
      test/acceptance/tools.js
  27. 553 82
      website/package-lock.json
  28. 1 1
      website/package.json
  29. 102 0
      website/src/_posts/2017-07-golden-retriever.md
  30. 1 1
      website/src/api-usage-example.ejs
  31. 1 2
      website/src/examples/bundle/index.ejs
  32. 1 1
      website/src/examples/dashboard/app.es6
  33. 0 2
      website/src/examples/drive/index.ejs
  34. 2 3
      website/src/examples/i18n/app.html
  35. 2 2
      website/src/examples/multipart/index.ejs
  36. 二進制
      website/src/images/blog/golden-retriever/catch-fail-1.gif
  37. 二進制
      website/src/images/blog/golden-retriever/catch-fail-2.gif
  38. 二進制
      website/src/images/blog/golden-retriever/desktop-ghost.png
  39. 二進制
      website/src/images/blog/golden-retriever/desktop-restore.png
  40. 二進制
      website/src/images/blog/golden-retriever/just-a-little-turbulance-folks.jpg
  41. 二進制
      website/src/images/blog/golden-retriever/mobile.png
  42. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-1.gif
  43. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-2.gif
  44. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-3.gif
  45. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-4.jpg
  46. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-5.jpg
  47. 二進制
      website/src/images/blog/golden-retriever/no-idea-dog-6.jpg
  48. 二進制
      website/src/images/blog/golden-retriever/puppy-rescue.gif
  49. 二進制
      website/src/images/blog/golden-retriever/uppy-golden-retriever-crash-demo-2.mp4
  50. 二進制
      website/src/images/blog/golden-retriever/uppy-golden-retriever-crash-demo.mp4
  51. 二進制
      website/src/images/blog/golden-retriever/uppy-team-kong.jpg
  52. 二進制
      website/src/images/blog/golden-retriever/uppy-team.jpg
  53. 4 2
      website/themes/uppy/layout/layout.ejs
  54. 1 1
      website/themes/uppy/source/css/_common.scss
  55. 1 1
      website/themes/uppy/source/css/_page.scss

+ 47 - 26
CHANGELOG.md

@@ -18,12 +18,10 @@ last Friday of every new month.
 
 
 Ideas that will be planned and find their way into a release at one point
 Ideas that will be planned and find their way into a release at one point
 
 
-- [ ] build: go over `package.json` together and clean up npm run scripts (@arturi, @hedgerh, @kvz)
 - [ ] build: investigate Rollup someday, for tree-shaking and smaller dist https://github.com/substack/node-browserify/issues/1379#issuecomment-183383199, https://github.com/nolanlawson/rollupify, https://github.com/nolanlawson/rollup-comparison
 - [ ] build: investigate Rollup someday, for tree-shaking and smaller dist https://github.com/substack/node-browserify/issues/1379#issuecomment-183383199, https://github.com/nolanlawson/rollupify, https://github.com/nolanlawson/rollup-comparison
 - [ ] core: Decouple rendering from Plugins and try to make Uppy work with React (add basic example) to remain aware of possible issues (@hedgerh), look at https://github.com/akiran/react-slick, https://github.com/nosir/cleave.js
 - [ ] core: Decouple rendering from Plugins and try to make Uppy work with React (add basic example) to remain aware of possible issues (@hedgerh), look at https://github.com/akiran/react-slick, https://github.com/nosir/cleave.js
 - [ ] core: Have base styles, be explicit about fonts, etc
 - [ ] core: Have base styles, be explicit about fonts, etc
 - [ ] core: Make sure Uppy works well in VR
 - [ ] core: Make sure Uppy works well in VR
-- [ ] dashboard: add ability to minimize Modal/Dashboard, while long upload is in progress? Uppy then becomes just a tiny progress indicator
 - [ ] test: Human should check http://www.webpagetest.org and https://developers.google.com/web/tools/lighthouse/, use it sometimes to test website and Uppy. Will show response/loading times and other issues
 - [ ] test: Human should check http://www.webpagetest.org and https://developers.google.com/web/tools/lighthouse/, use it sometimes to test website and Uppy. Will show response/loading times and other issues
 - [ ] test: Human should test with real screen reader to identify accessibility problems
 - [ ] test: Human should test with real screen reader to identify accessibility problems
 - [ ] test: Make Edge and Safari work via the tunnel so we can test localhost instead of uppy.io, and test the current build, vs the previous deploy that way
 - [ ] test: Make Edge and Safari work via the tunnel so we can test localhost instead of uppy.io, and test the current build, vs the previous deploy that way
@@ -35,7 +33,6 @@ Ideas that will be planned and find their way into a release at one point
 - [ ] dashboard: maybe add perfect scrollbar https://github.com/noraesae/perfect-scrollbar (@arturi)
 - [ ] dashboard: maybe add perfect scrollbar https://github.com/noraesae/perfect-scrollbar (@arturi)
 - [ ] ui: do we want https://github.com/kazzkiq/balloon.css ?
 - [ ] ui: do we want https://github.com/kazzkiq/balloon.css ?
 - [ ] core: consider adding presets, see https://github.com/cssinjs/jss-preset-default/blob/master/src/index.js (@arturi)
 - [ ] core: consider adding presets, see https://github.com/cssinjs/jss-preset-default/blob/master/src/index.js (@arturi)
-- [ ] dashboard: see if transitions can be fixed in Firefox — seem to be working fine, let’s check again someday (@arturi)
 - [ ] uppy/uppy-server: Transfer files between providers (from instagram to Google drive for example).
 - [ ] uppy/uppy-server: Transfer files between providers (from instagram to Google drive for example).
 - [ ] uppy/uppy-server: review websocket connection and throttling progress events (@arturi, @ifedapoolarewaju)
 - [ ] uppy/uppy-server: review websocket connection and throttling progress events (@arturi, @ifedapoolarewaju)
 - [ ] uploaders: consider not showing progress updates from the server after an upload’s been paused (@arturi, @ifedapoolarewaju)
 - [ ] uploaders: consider not showing progress updates from the server after an upload’s been paused (@arturi, @ifedapoolarewaju)
@@ -43,15 +40,18 @@ Ideas that will be planned and find their way into a release at one point
 - [ ] maybe restrict system file picking dialog too https://github.com/transloadit/uppy/issues/253
 - [ ] maybe restrict system file picking dialog too https://github.com/transloadit/uppy/issues/253
 - [ ] uppy-server: what happens if access token expires amid an upload/download process.
 - [ ] uppy-server: what happens if access token expires amid an upload/download process.
 - [ ] s3+transloadit: upload to S3, then import into :tl: assembly using `/add_file?s3url=${url}` (@goto-bus-stop)
 - [ ] s3+transloadit: upload to S3, then import into :tl: assembly using `/add_file?s3url=${url}` (@goto-bus-stop)
+- [ ] good way to change plugin options at runtime—maybe `this.state.options`?
+- [ ] s3: multipart/"resumable" uploads for large files (@goto-bus-stop)
 
 
 ## 1.0 Goals
 ## 1.0 Goals
 
 
 What we need to do to release Uppy 1.0
 What we need to do to release Uppy 1.0
 
 
 - [x] feature: restrictions: by size, number of files, file type
 - [x] feature: restrictions: by size, number of files, file type
+- [ ] feature: beta file recovering after closed tab / browser crash
 - [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
 - [ ] feature: improved UI for Provider, Google Drive and Instagram, grid/list views
 - [ ] feature: finish the direct-to-s3 upload plugin and test it with the flow to then upload to :transloadit: afterwards. This is because this might influence the inner flow of the plugin architecture quite a bit
 - [ ] feature: finish the direct-to-s3 upload plugin and test it with the flow to then upload to :transloadit: afterwards. This is because this might influence the inner flow of the plugin architecture quite a bit
-- [ ] feature: Uppy should work well with React/Redux. React (Native)
+- [ ] feature: Uppy should work well with React/Redux and React Native
 - [ ] feature: preset for Transloadit that mimics jQuery SDK
 - [ ] feature: preset for Transloadit that mimics jQuery SDK
 - [ ] QA: test how everything works together: user experience from `npm install` to production build with Webpack, using in React/Redux environment (npm pack)
 - [ ] QA: test how everything works together: user experience from `npm install` to production build with Webpack, using in React/Redux environment (npm pack)
 - [ ] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files. add metrics to Librato
 - [ ] QA: test uppy server. benchmarks / stress test. multiple connections, different setups, large files. add metrics to Librato
@@ -61,7 +61,7 @@ What we need to do to release Uppy 1.0
 - [ ] ui: refine UI, neat things up (if that’s even a word)
 - [ ] ui: refine UI, neat things up (if that’s even a word)
 - [ ] refactoring: reduce size where possible, like, socket.io --> websockets (saves 20KB)
 - [ ] refactoring: reduce size where possible, like, socket.io --> websockets (saves 20KB)
 - [ ] refactoring: possibly add CSS-in-JS
 - [ ] refactoring: possibly add CSS-in-JS
-- [ ] refactoring: possibly switch from Yo-Yo to Preact, because its more stable, solves a few issues we are struggling with (like onload/onunload being weird in yo-yo) and mature, “new standard”, larger community
+- [ ] refactoring: possibly switch from Yo-Yo to Preact, because its more stable, solves a few issues we are struggling with (like onload/onunload being weird in yo-yo) and mature, “new standard”, larger community
 - [ ] refactoring: possibly differentiate UI plugins from logic plugins, so that, say Tus plugin doesn’t include rendering stuff
 - [ ] refactoring: possibly differentiate UI plugins from logic plugins, so that, say Tus plugin doesn’t include rendering stuff
 - [ ] refactoring: webcam plugin
 - [ ] refactoring: webcam plugin
 - [ ] refactoring: clean up code everywhere
 - [ ] refactoring: clean up code everywhere
@@ -71,38 +71,59 @@ What we need to do to release Uppy 1.0
 
 
 ## 0.19.0
 ## 0.19.0
 
 
-- [ ] allow minimizing the Dashboard during upload (@arturi)
+- [ ] allow minimizing the Dashboard during upload (Uppy then becomes just a tiny progress indicator) (@arturi)
 - [ ] webcam: look into simplifying / improving webcam plugin (@arturi, @goto-bus-stop)
 - [ ] webcam: look into simplifying / improving webcam plugin (@arturi, @goto-bus-stop)
+- [ ] DnD Bar ? (@arturi)
+- [ ] provider: improve UI: add icons for file types? (@arturi)
+- [ ] core: see if we can figure out css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
+- [ ] dashboard: error UI, question mark button, `core:error` (@arturi)
+- [ ] core: add error in file progress state? (@arturi)
+- [ ] core: calling `upload` immediately after `addFile` does not upload all files (#249 @goto-bus-stop)
+- [ ] core: research !important styles to be immune to any environment/page. Maybe use smth like `postcss-safe-important`. Or increase specificity (with .Uppy) (@arturi)
+- [ ] s3+transloadit: upload to S3, then import into :tl: assembly using `/add_file?s3url=${url}` (@goto-bus-stop)
 
 
 # next
 # next
 
 
 ## 0.18.0
 ## 0.18.0
 
 
-To be released: 2017-07-28.
+To be released: 2017-08-14.
 Theme: Dogumentation.
 Theme: Dogumentation.
 
 
+- [ ] goldenretriver: add file limits, figure out what restores from where, add “ghost” files, add throttling for localStorage state sync (@goto-bus-stop @arturi)
+- [x] dashboard: flag to hide the upload button, for cases when you want to manually stat the upload (@arturi)
+- [x] dashboard: place close btn inside the Dashboard, don’t close on click outside, place source icon near the file size
+- [x] core: informer becomes a core API, `uppy.info('Smile! 📸', 'warning', 5000)` so its more concise with `uppy.log('my msg')` and supports different UI implementations (@arturi, #271)
+- [ ] docs: first stage — on using plugins, all options, list of plugins, i18n (@arturi, @goto-bus-stop, @ifedapoolarewaju)
+- [ ] core: retry or show error when upload can’t start (offline, wrong endpoint) — now it just sits there (@arturi @goto-bus-stop)
+- [ ] informer: support “explanations”, a (?) button that shows more info on hover / click
+- [x] provider: file size sorting (@ifedapoolarewaju)
+- [x] provider: show loading screen when checking auth too (@arturi)
 - [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing (@arturi)
 - [ ] test: add https://github.com/pa11y/pa11y for automated accessibility testing (@arturi)
-- [ ] core: add error in file progress state? (@arturi)
-- [ ] core: research !important styles to be immune to any environment/page. Maybe use smth like `postcss-safe-important`. Or increase specificity (with .Uppy) (@arturi)
-- [ ] uppy-server: add uppy-server metrics to Librato (@ifedapoolarewaju)
-- [ ] dashboard: error UI, question mark button, `core:error` (@arturi)
-- [x] uploaders: add direct-to-s3 upload plugin (@goto-bus-stop)
-- [ ] provider: file size sorting (@ifedapoolarewaju)
-- [ ] core: see if we can figure out css-in-js, while keeping non-random classnames (ideally prefixed) and useful preprocessor features. also see simple https://github.com/codemirror/CodeMirror/blob/master/lib/codemirror.css (@arturi, @goto-bus-stop)
 - [ ] test: add tests for `npm install uppy` and running in different browsers, the real world use case (@arturi)
 - [ ] test: add tests for `npm install uppy` and running in different browsers, the real world use case (@arturi)
-- [ ] provider: improve UI: add icons for file types? (@arturi)
-- [ ] uppy: flag to upload all files, even `uploadComplete` ones (@arturi)
+- [x] uploaders: add direct-to-s3 upload plugin (@goto-bus-stop)
+- [x] core: ability to re-upload all files, even `uploadComplete` ones, reset progress (@arturi)
+- [x] goldenretriver: recover selected or in progress files after a browser crash or closed tab: alpha-version, add LocalStorage, Service Worker and IndexedDB (@arturi @goto-bus-stop @nqst #268)
+- [x] xhrupload: add XHRUpload a more flexible successor to Multipart, so that S3 plugin can depend on it (@goto-bus-stop #242)
+- [x] core: add getFile method (@goto-bus-stop, #263)
+- [x] provider: use informer to display errors (@ifedapoolarewaju)
+- [x] provider: flatten instagram carousels #234 (@ifedapoolarewaju)
+- [x] server: add uppy-server url as `i-am` header (@ifedapoolarewaju)
+- [x] server: disable socket channel from restarting an already completed file download (@ifedapoolarewaju)
+- [x] server: make uppy client whitelisting optional. You may use wildcard instead (@ifedapoolarewaju)
+- [x] server: master oauth redirect uri for multiple uppy-server instances
+- [x] server: options support for redis session storage on standalone server (@ifedapoolarewaju)
+- [x] server: start uppy-server as binary `uppy-server` (@ifedapoolarewaju)
+- [x] server: store downloaded files based on uuids (@ifedapoolarewaju)
+- [x] server: store upload state on redis (@ifedapoolarewaju)
+- [x] server: use uppy informer for server errors (@ifedapoolarewaju, #272)
+- [x] server: whitelist multiple uppy clients (@ifedapoolarewaju)
 - [x] transloadit: emit an event when an assembly is created (@goto-bus-stop / #244)
 - [x] transloadit: emit an event when an assembly is created (@goto-bus-stop / #244)
-- [ ] dashboard: flag to hide the upload button, for cases when you want to manually stat the upload (@arturi)
-- [x] webcam: add 1, 2, 3, smile! to webcam (@arturi #187)
-- [ ] transloadit: function option for file-dependent `params` (@goto-bus-stop)
-- [ ] docs: on using plugins, all options, list of plugins, i18n (@arturi, @goto-bus-stop, @ifedapoolarewaju)
-- [ ] core: calling `upload` immediately after `addFile` does not upload all files (#249 @goto-bus-stop)
-- [x] website: live example on the homepage, “try me” (@arturi)
-- [ ] DnD Bar (@arturi)
-- [ ] handle error when upload can’t start (offline, wrong endpoint) — now it just sits there (@arturi @goto-bus-stop)
-- [ ] improve docs (@arturi @goto-bus-stop)
-- [ ] GoldenRetriver: recover selected or in progress files after a browser crash or closed tab (@arturi @goto-bus-stop @nqst #268)
+- [x] transloadit: function option for file-dependent `params` (@goto-bus-stop / #250)
+- [x] tus: Save upload URL early on (@goto-bus-stop #261)
+- [x] tus: return immediately if no files are selected (@goto-bus-stop #245)
+- [x] uppy-server: add uppy-server metrics to Librato (@ifedapoolarewaju @kiloreux)
+- [x] webcam: add 1, 2, 3, smile! to webcam, onBeforeSnapshothook (@arturi, #187, #248)
+- [x] website: live example on the homepage, “try me” button, improve /examples (@arturi)
 
 
 ## 0.17.0
 ## 0.17.0
 
 

+ 1 - 0
examples/bundled-example/main.js

@@ -55,6 +55,7 @@ const uppy = Uppy({
     setMetaFromTargetForm: true,
     setMetaFromTargetForm: true,
     // replaceTargetContent: true,
     // replaceTargetContent: true,
     target: '.MyForm',
     target: '.MyForm',
+    hideUploadButton: true,
     locale: {
     locale: {
       strings: {browse: 'wow'}
       strings: {browse: 'wow'}
     },
     },

+ 109 - 29
src/core/Core.js

@@ -26,14 +26,13 @@ class Uppy {
           1: 'You have to select at least %{smart_count} files'
           1: 'You have to select at least %{smart_count} files'
         },
         },
         exceedsSize: 'This file exceeds maximum allowed size of',
         exceedsSize: 'This file exceeds maximum allowed size of',
-        youCanOnlyUploadFileTypes: 'You can only upload:'
+        youCanOnlyUploadFileTypes: 'You can only upload:',
+        uppyServerError: 'Connection with Uppy server failed'
       }
       }
     }
     }
 
 
     // set default options
     // set default options
     const defaultOptions = {
     const defaultOptions = {
-      // load English as the default locale
-      // locale: en_US,
       autoProceed: true,
       autoProceed: true,
       debug: false,
       debug: false,
       restrictions: {
       restrictions: {
@@ -64,6 +63,7 @@ class Uppy {
     // Container for different types of plugins
     // Container for different types of plugins
     this.plugins = {}
     this.plugins = {}
 
 
+    // @TODO maybe bindall
     this.translator = new Translator({locale: this.opts.locale})
     this.translator = new Translator({locale: this.opts.locale})
     this.i18n = this.translator.translate.bind(this.translator)
     this.i18n = this.translator.translate.bind(this.translator)
     this.getState = this.getState.bind(this)
     this.getState = this.getState.bind(this)
@@ -72,10 +72,14 @@ class Uppy {
     this.log = this.log.bind(this)
     this.log = this.log.bind(this)
     this.addFile = this.addFile.bind(this)
     this.addFile = this.addFile.bind(this)
     this.calculateProgress = this.calculateProgress.bind(this)
     this.calculateProgress = this.calculateProgress.bind(this)
+    this.resetProgress = this.resetProgress.bind(this)
 
 
-    this.bus = this.emitter = ee()
-    this.on = this.bus.on.bind(this.bus)
-    this.emit = this.bus.emit.bind(this.bus)
+    // this.bus = this.emitter = ee()
+    this.emitter = ee()
+    this.on = this.emitter.on.bind(this.emitter)
+    this.off = this.emitter.off.bind(this.emitter)
+    this.once = this.emitter.once.bind(this.emitter)
+    this.emit = this.emitter.emit.bind(this.emitter)
 
 
     this.preProcessors = []
     this.preProcessors = []
     this.uploaders = []
     this.uploaders = []
@@ -87,7 +91,12 @@ class Uppy {
         resumableUploads: false
         resumableUploads: false
       },
       },
       totalProgress: 0,
       totalProgress: 0,
-      meta: Object.assign({}, this.opts.meta)
+      meta: Object.assign({}, this.opts.meta),
+      info: {
+        isHidden: true,
+        type: '',
+        msg: ''
+      }
     }
     }
 
 
     // for debugging and testing
     // for debugging and testing
@@ -95,8 +104,8 @@ class Uppy {
     if (this.opts.debug) {
     if (this.opts.debug) {
       global.UppyState = this.state
       global.UppyState = this.state
       global.uppyLog = ''
       global.uppyLog = ''
-      global.UppyAddFile = this.addFile.bind(this)
-      global._Uppy = this
+      // global.UppyAddFile = this.addFile.bind(this)
+      global._uppy = this
     }
     }
   }
   }
 
 
@@ -143,6 +152,27 @@ class Uppy {
     })
     })
   }
   }
 
 
+  resetProgress () {
+    const defaultProgress = {
+      percentage: 0,
+      bytesUploaded: 0,
+      uploadComplete: false,
+      uploadStarted: false
+    }
+    const files = Object.assign({}, this.state.files)
+    const updatedFiles = {}
+    Object.keys(files).forEach(fileID => {
+      const updatedFile = Object.assign({}, files[fileID])
+      updatedFile.progress = Object.assign({}, updatedFile.progress, defaultProgress)
+      updatedFiles[fileID] = updatedFile
+    })
+    console.log(updatedFiles)
+    this.setState({
+      files: updatedFiles,
+      totalProgress: 0
+    })
+  }
+
   addPreProcessor (fn) {
   addPreProcessor (fn) {
     this.preProcessors.push(fn)
     this.preProcessors.push(fn)
   }
   }
@@ -197,7 +227,7 @@ class Uppy {
 
 
     if (checkMinNumberOfFiles && minNumberOfFiles) {
     if (checkMinNumberOfFiles && minNumberOfFiles) {
       if (Object.keys(this.state.files).length < minNumberOfFiles) {
       if (Object.keys(this.state.files).length < minNumberOfFiles) {
-        this.emit('informer', `${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
+        this.info(`${this.i18n('youHaveToAtLeastSelectX', {smart_count: minNumberOfFiles})}`, 'error', 5000)
         return false
         return false
       }
       }
       return true
       return true
@@ -205,7 +235,7 @@ class Uppy {
 
 
     if (maxNumberOfFiles) {
     if (maxNumberOfFiles) {
       if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
       if (Object.keys(this.state.files).length + 1 > maxNumberOfFiles) {
-        this.emit('informer', `${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
+        this.info(`${this.i18n('youCanOnlyUploadX', {smart_count: maxNumberOfFiles})}`, 'error', 5000)
         return false
         return false
       }
       }
     }
     }
@@ -214,14 +244,14 @@ class Uppy {
       const isCorrectFileType = allowedFileTypes.filter(match(fileType.join('/'))).length > 0
       const isCorrectFileType = allowedFileTypes.filter(match(fileType.join('/'))).length > 0
       if (!isCorrectFileType) {
       if (!isCorrectFileType) {
         const allowedFileTypesString = allowedFileTypes.join(', ')
         const allowedFileTypesString = allowedFileTypes.join(', ')
-        this.emit('informer', `${this.i18n('youCanOnlyUploadFileTypes')} ${allowedFileTypesString}`, 'error', 5000)
+        this.info(`${this.i18n('youCanOnlyUploadFileTypes')} ${allowedFileTypesString}`, 'error', 5000)
         return false
         return false
       }
       }
     }
     }
 
 
     if (maxFileSize) {
     if (maxFileSize) {
       if (file.data.size > maxFileSize) {
       if (file.data.size > maxFileSize) {
-        this.emit('informer', `${this.i18n('exceedsSize')} ${prettyBytes(maxFileSize)}`, 'error', 5000)
+        this.info(`${this.i18n('exceedsSize')} ${prettyBytes(maxFileSize)}`, 'error', 5000)
         return false
         return false
       }
       }
     }
     }
@@ -231,7 +261,7 @@ class Uppy {
 
 
   addFile (file) {
   addFile (file) {
     return this.opts.onBeforeFileAdded(file, this.getState().files).catch((err) => {
     return this.opts.onBeforeFileAdded(file, this.getState().files).catch((err) => {
-      this.emit('informer', err, 'error', 5000)
+      this.info(err, 'error', 5000)
       return Promise.reject(`onBeforeFileAdded: ${err}`)
       return Promise.reject(`onBeforeFileAdded: ${err}`)
     }).then(() => {
     }).then(() => {
       return Utils.getFileType(file).then((fileType) => {
       return Utils.getFileType(file).then((fileType) => {
@@ -273,7 +303,7 @@ class Uppy {
         }
         }
 
 
         const isFileAllowed = this.checkRestrictions(false, newFile, fileType)
         const isFileAllowed = this.checkRestrictions(false, newFile, fileType)
-        if (!isFileAllowed) return
+        if (!isFileAllowed) return Promise.reject('File not allowed')
 
 
         updatedFiles[fileID] = newFile
         updatedFiles[fileID] = newFile
         this.setState({files: updatedFiles})
         this.setState({files: updatedFiles})
@@ -393,9 +423,9 @@ class Uppy {
     })
     })
 
 
     this.on('core:cancel-all', () => {
     this.on('core:cancel-all', () => {
-      let updatedFiles = this.getState().files
-      updatedFiles = {}
-      this.setState({files: updatedFiles})
+      // let updatedFiles = this.getState().files
+      // updatedFiles = {}
+      this.setState({files: {}})
     })
     })
 
 
     this.on('core:upload-started', (fileID, upload) => {
     this.on('core:upload-started', (fileID, upload) => {
@@ -508,13 +538,13 @@ class Uppy {
     const online = status || window.navigator.onLine
     const online = status || window.navigator.onLine
     if (!online) {
     if (!online) {
       this.emit('is-offline')
       this.emit('is-offline')
-      this.emit('informer', 'No internet connection', 'error', 0)
+      this.info('No internet connection', 'error', 0)
       this.wasOffline = true
       this.wasOffline = true
     } else {
     } else {
       this.emit('is-online')
       this.emit('is-online')
       if (this.wasOffline) {
       if (this.wasOffline) {
         this.emit('back-online')
         this.emit('back-online')
-        this.emit('informer', 'Connected!', 'success', 3000)
+        this.info('Connected!', 'success', 3000)
         this.wasOffline = false
         this.wasOffline = false
       }
       }
     }
     }
@@ -619,11 +649,57 @@ class Uppy {
     }
     }
   }
   }
 
 
-/**
- * Logs stuff to console, only if `debug` is set to true. Silent in production.
- *
- * @return {String|Object} to log
- */
+  /**
+  * Set info message in `state.info`, so that UI plugins like `Informer`
+  * can display the message
+  *
+  * @param {string} msg Message to be displayed by the informer
+  */
+
+  info (msg, type, duration) {
+    this.setState({
+      info: {
+        isHidden: false,
+        type: type,
+        msg: msg
+      }
+    })
+
+    this.emit('core:info-visible')
+
+    window.clearTimeout(this.infoTimeoutID)
+    if (duration === 0) {
+      this.infoTimeoutID = undefined
+      return
+    }
+
+    // hide the informer after `duration` milliseconds
+    this.infoTimeoutID = setTimeout(() => {
+      const newInformer = Object.assign({}, this.state.info, {
+        isHidden: true
+      })
+      this.setState({
+        info: newInformer
+      })
+      this.emit('core:info-hidden')
+    }, duration)
+  }
+
+  hideInfo () {
+    const newInfo = Object.assign({}, this.core.state.info, {
+      isHidden: true
+    })
+    this.setState({
+      info: newInfo
+    })
+    this.emit('core:info-hidden')
+  }
+
+  /**
+   * Logs stuff to console, only if `debug` is set to true. Silent in production.
+   *
+   * @return {String|Object} to log
+   */
   log (msg, type) {
   log (msg, type) {
     if (!this.opts.debug) {
     if (!this.opts.debug) {
       return
       return
@@ -671,14 +747,14 @@ class Uppy {
     return this
     return this
   }
   }
 
 
-  upload () {
+  upload (forceUpload) {
     const isMinNumberOfFilesReached = this.checkRestrictions(true)
     const isMinNumberOfFilesReached = this.checkRestrictions(true)
     if (!isMinNumberOfFilesReached) {
     if (!isMinNumberOfFilesReached) {
       return Promise.reject('Minimum number of files has not been reached')
       return Promise.reject('Minimum number of files has not been reached')
     }
     }
 
 
-    return this.opts.onBeforeUpload(this.getState().files).catch((err) => {
-      this.emit('informer', err, 'error', 5000)
+    return this.opts.onBeforeUpload(this.state.files).catch((err) => {
+      this.info(err, 'error', 5000)
       return Promise.reject(`onBeforeUpload: ${err}`)
       return Promise.reject(`onBeforeUpload: ${err}`)
     }).then(() => {
     }).then(() => {
       this.emit('core:upload')
       this.emit('core:upload')
@@ -690,7 +766,11 @@ class Uppy {
         //
         //
         // filter files that are now yet being uploaded / haven’t been uploaded
         // filter files that are now yet being uploaded / haven’t been uploaded
         // and remote too
         // and remote too
-        if (!file.progress.uploadStarted || file.isRemote) {
+
+        if (forceUpload) {
+          this.resetProgress()
+          waitingFileIDs.push(file.id)
+        } else if (!file.progress.uploadStarted || file.isRemote) {
           waitingFileIDs.push(file.id)
           waitingFileIDs.push(file.id)
         }
         }
       })
       })

+ 11 - 4
src/generic-provider-views/AuthView.js

@@ -1,14 +1,21 @@
 const html = require('yo-yo')
 const html = require('yo-yo')
+const onload = require('on-load')
+const LoaderView = require('./Loader')
 
 
 module.exports = (props) => {
 module.exports = (props) => {
   const demoLink = props.demo ? html`<button class="UppyProvider-authBtnDemo" onclick=${props.handleDemoAuth}>Proceed with Demo Account</button>` : null
   const demoLink = props.demo ? html`<button class="UppyProvider-authBtnDemo" onclick=${props.handleDemoAuth}>Proceed with Demo Account</button>` : null
-  return html`
+  const AuthBlock = () => html`
     <div class="UppyProvider-auth">
     <div class="UppyProvider-auth">
-      <h1 class="UppyProvider-authTitle">
-        Please authenticate with <span class="UppyProvider-authTitleName">${props.pluginName}</span><br> to select files
-      </h1>
+      <h1 class="UppyProvider-authTitle">Please authenticate with <span class="UppyProvider-authTitleName">${props.pluginName}</span><br> to select files</h1>
       <button type="button" class="UppyProvider-authBtn" onclick=${props.handleAuth}>Authenticate</button>
       <button type="button" class="UppyProvider-authBtn" onclick=${props.handleAuth}>Authenticate</button>
       ${demoLink}
       ${demoLink}
     </div>
     </div>
   `
   `
+  return onload(html`
+    <div style="height: 100%;">
+      ${props.checkAuthInProgress
+        ? LoaderView()
+        : AuthBlock()
+      }
+    </div>`, props.checkAuth, null, `auth${props.pluginName}`)
 }
 }

+ 0 - 9
src/generic-provider-views/Error.js

@@ -1,9 +0,0 @@
-const html = require('yo-yo')
-
-module.exports = (props) => {
-  return html`
-    <div class="UppyProvider-error">
-      <span>Something went wrong. Probably our fault. ${props.error}</span>
-    </div>
-  `
-}

+ 52 - 20
src/generic-provider-views/index.js

@@ -1,6 +1,5 @@
 const AuthView = require('./AuthView')
 const AuthView = require('./AuthView')
 const Browser = require('./Browser')
 const Browser = require('./Browser')
-const ErrorView = require('./Error')
 const LoaderView = require('./Loader')
 const LoaderView = require('./Loader')
 const Utils = require('../core/Utils')
 const Utils = require('../core/Utils')
 
 
@@ -61,6 +60,7 @@ module.exports = class View {
     this.getFolder = this.getFolder.bind(this)
     this.getFolder = this.getFolder.bind(this)
     this.getNextFolder = this.getNextFolder.bind(this)
     this.getNextFolder = this.getNextFolder.bind(this)
     this.logout = this.logout.bind(this)
     this.logout = this.logout.bind(this)
+    this.checkAuth = this.checkAuth.bind(this)
     this.handleAuth = this.handleAuth.bind(this)
     this.handleAuth = this.handleAuth.bind(this)
     this.handleDemoAuth = this.handleDemoAuth.bind(this)
     this.handleDemoAuth = this.handleDemoAuth.bind(this)
     this.sortByTitle = this.sortByTitle.bind(this)
     this.sortByTitle = this.sortByTitle.bind(this)
@@ -95,6 +95,19 @@ module.exports = class View {
     this.updateState({ folders, files })
     this.updateState({ folders, files })
   }
   }
 
 
+  checkAuth () {
+    this.updateState({ checkAuthInProgress: true })
+    this.Provider.checkAuth()
+      .then((authenticated) => {
+        this.updateState({ checkAuthInProgress: false })
+        this.plugin.onAuth(authenticated)
+      })
+      .catch((err) => {
+        this.updateState({ checkAuthInProgress: false })
+        this.handleError(err)
+      })
+  }
+
   /**
   /**
    * Based on folder ID, fetch a new folder and update it to state
    * Based on folder ID, fetch a new folder and update it to state
    * @param  {String} id Folder id
    * @param  {String} id Folder id
@@ -145,7 +158,7 @@ module.exports = class View {
       },
       },
       remote: {
       remote: {
         host: this.plugin.opts.host,
         host: this.plugin.opts.host,
-        url: `${this.plugin.opts.host}/${this.Provider.id}/get/${this.plugin.getItemRequestPath(file)}`,
+        url: `${this.Provider.fileUrl(this.plugin.getItemRequestPath(file))}`,
         body: {
         body: {
           fileId: this.plugin.getItemId(file)
           fileId: this.plugin.getItemId(file)
         }
         }
@@ -157,14 +170,8 @@ module.exports = class View {
         tagFile.preview = this.plugin.getItemThumbnailUrl(file)
         tagFile.preview = this.plugin.getItemThumbnailUrl(file)
       }
       }
       this.plugin.core.log('Adding remote file')
       this.plugin.core.log('Adding remote file')
-      this.plugin.core.emitter.emit('core:file-add', tagFile)
+      this.plugin.core.addFile(tagFile)
     })
     })
-
-    // feels like a hack
-    // this.updateState({
-    //   filterInput: '',
-    //   isSearchVisible: false
-    // })
   }
   }
 
 
   /**
   /**
@@ -272,6 +279,31 @@ module.exports = class View {
     }))
     }))
   }
   }
 
 
+  sortBySize () {
+    const state = Object.assign({}, this.plugin.core.getState()[this.plugin.stateId])
+    const {files, sorting} = state
+
+    // check that plugin supports file sizes
+    if (!files.length || !this.plugin.getItemData(files[0]).size) {
+      return
+    }
+
+    let sortedFiles = files.sort((fileA, fileB) => {
+      let a = this.plugin.getItemData(fileA).size
+      let b = this.plugin.getItemData(fileB).size
+
+      if (sorting === 'sizeDescending') {
+        return a > b ? -1 : a < b ? 1 : 0
+      }
+      return a > b ? 1 : a < b ? -1 : 0
+    })
+
+    this.updateState(Object.assign({}, state, {
+      files: sortedFiles,
+      sorting: (sorting === 'sizeDescending') ? 'sizeAscending' : 'sizeDescending'
+    }))
+  }
+
   isActiveRow (file) {
   isActiveRow (file) {
     return this.plugin.core.getState()[this.plugin.stateId].activeRow === this.plugin.getItemId(file)
     return this.plugin.core.getState()[this.plugin.stateId].activeRow === this.plugin.getItemId(file)
   }
   }
@@ -288,7 +320,7 @@ module.exports = class View {
     const redirect = `${location.href}${location.search ? '&' : '?'}id=${urlId}`
     const redirect = `${location.href}${location.search ? '&' : '?'}id=${urlId}`
 
 
     const authState = btoa(JSON.stringify({ redirect }))
     const authState = btoa(JSON.stringify({ redirect }))
-    const link = `${this.plugin.opts.host}/connect/${this.Provider.authProvider}?state=${authState}`
+    const link = `${this.Provider.authUrl()}?state=${authState}`
 
 
     const authWindow = window.open(link, '_blank')
     const authWindow = window.open(link, '_blank')
     const checkAuth = () => {
     const checkAuth = () => {
@@ -303,9 +335,9 @@ module.exports = class View {
       }
       }
 
 
       // split url because chrome adds '#' to redirects
       // split url because chrome adds '#' to redirects
-      if (authWindowUrl.split('#')[0] === redirect) {
+      if (authWindowUrl && authWindowUrl.split('#')[0] === redirect) {
         authWindow.close()
         authWindow.close()
-        this._loaderWrapper(this.Provider.auth(), this.plugin.onAuth, this.handleError)
+        this._loaderWrapper(this.Provider.checkAuth(), this.plugin.onAuth, this.handleError)
       } else {
       } else {
         setTimeout(checkAuth, 100)
         setTimeout(checkAuth, 100)
       }
       }
@@ -315,7 +347,10 @@ module.exports = class View {
   }
   }
 
 
   handleError (error) {
   handleError (error) {
-    this.updateState({ error })
+    const core = this.plugin.core
+    const message = core.i18n('uppyServerError')
+    core.log(error.toString())
+    core.emit('informer', message, 'error', 5000)
   }
   }
 
 
   handleScroll (e) {
   handleScroll (e) {
@@ -343,12 +378,7 @@ module.exports = class View {
   }
   }
 
 
   render (state) {
   render (state) {
-    const { authenticated, error, loading } = state[this.plugin.stateId]
-
-    if (error) {
-      this.updateState({ error: undefined })
-      return ErrorView({ error: error })
-    }
+    const { authenticated, checkAuthInProgress, loading } = state[this.plugin.stateId]
 
 
     if (loading) {
     if (loading) {
       return LoaderView()
       return LoaderView()
@@ -358,8 +388,10 @@ module.exports = class View {
       return AuthView({
       return AuthView({
         pluginName: this.plugin.title,
         pluginName: this.plugin.title,
         demo: this.plugin.opts.demo,
         demo: this.plugin.opts.demo,
+        checkAuth: this.checkAuth,
         handleAuth: this.handleAuth,
         handleAuth: this.handleAuth,
-        handleDemoAuth: this.handleDemoAuth
+        handleDemoAuth: this.handleDemoAuth,
+        checkAuthInProgress: checkAuthInProgress
       })
       })
     }
     }
 
 

+ 54 - 0
src/locales/nb_NO.js

@@ -0,0 +1,54 @@
+/* eslint camelcase: 0 */
+
+const nb_NO = {}
+
+nb_NO.strings = {
+  chooseFile: 'Velg en fil',
+  youHaveChosen: 'Du har valgt: %{fileName}',
+  orDragDrop: 'eller slipp den her',
+  filesChosen: {
+    0: '%{smart_count} fil valgt',
+    1: '%{smart_count} filer valgt'
+  },
+  filesUploaded: {
+    0: '%{smart_count} fil lastet opp',
+    1: '%{smart_count} filer lastet opp'
+  },
+  files: {
+    0: '%{smart_count} fil',
+    1: '%{smart_count} filer'
+  },
+  uploadFiles: {
+    0: 'Lastet opp %{smart_count} fil',
+    1: 'Lastet opp %{smart_count} filer'
+  },
+  selectToUpload: 'Velg filer å laste opp',
+  closeModal: 'Lukk dialogboksen',
+  upload: 'Last opp',
+  importFrom: 'Importer filer fra',
+  dashboardWindowTitle: 'Uppy Dashboard-vindu (Trykk escape for å lukke)',
+  dashboardTitle: 'Uppy Dashboard',
+  copyLinkToClipboardSuccess: 'Lenken ble kopiert til utklippstavla.',
+  copyLinkToClipboardFallback: 'Kopier URL-en under',
+  done: 'Ferdig',
+  localDisk: 'Lokal disk',
+  dropPasteImport: 'Du kan slippe eller lime inn filer her, importere fra en en av plasseringene ovenfor eller',
+  dropPaste: 'Du kan slippe eller lime inn filer her eller',
+  browse: 'velge dem',
+  fileProgress: 'Filstatus: Opplastingshastighet og ETA',
+  numberOfSelectedFiles: 'Antall valgte filer',
+  uploadAllNewFiles: 'Last opp alle nye filer'
+}
+
+nb_NO.pluralize = function (n) {
+  if (n === 1) {
+    return 0
+  }
+  return 1
+}
+
+if (typeof window !== 'undefined' && typeof window.Uppy !== 'undefined') {
+  window.Uppy.locales.nb_NO = nb_NO
+}
+
+module.exports = nb_NO

+ 54 - 0
src/locales/tr_TR.js

@@ -0,0 +1,54 @@
+/* eslint camelcase: 0 */
+
+const tr_TR = {}
+
+tr_TR.strings = {
+  chooseFile: 'Dosya Seçin',
+  youHaveChosen: 'Seçmiş olduğun dosya: %{fileName}',
+  orDragDrop: 'yada bırakın',
+  filesChosen: {
+    0: '%{smart_count} adet dosya seçili',
+    1: '%{smart_count} adet dosyalar seçili'
+  },
+  filesUploaded: {
+    0: '%{smart_count} adet dosya yüklendi',
+    1: '%{smart_count} adet dosyalar yüklendi'
+  },
+  files: {
+    0: '%{smart_count} dosya',
+    1: '%{smart_count} dosyalar'
+  },
+  uploadFiles: {
+    0: 'Yüklenen %{smart_count} dosya',
+    1: 'Yüklenen %{smart_count} dosyalar'
+  },
+  selectToUpload: 'Yüklemek için dosyaları seçin',
+  closeModal: 'Pencereyi Kapat',
+  upload: 'Yükle',
+  importFrom: 'Dosyaları içeri aktar',
+  dashboardWindowTitle: 'Uppy Panel Pencerisi (kapatmak için esc kullanın)',
+  dashboardTitle: 'Uppy Panel',
+  copyLinkToClipboardSuccess: 'Bağlantı kopyalandı.',
+  copyLinkToClipboardFallback: 'Bağlantıyı kopyala.',
+  done: 'Bitti',
+  localDisk: 'Lokal Dosyalar',
+  dropPasteImport: 'Dosyaları buraya bırakın, yukarıdaki konumlardan birinden yapıştırın, içeri aktarın veya',
+  dropPaste: 'Dosyaları buraya bırak, yapıştır veya',
+  browse: 'Gözat',
+  fileProgress: 'Dosya ilerlemesi: yükleme hızı ve süresi',
+  numberOfSelectedFiles: 'Seçilen dosya sayısı',
+  uploadAllNewFiles: 'Tüm yeni dosyaları yükle'
+}
+
+tr_TR.pluralize = function (n) {
+  if (n === 1) {
+    return 0
+  }
+  return 1
+}
+
+if (typeof window !== 'undefined' && typeof window.Uppy !== 'undefined') {
+  window.Uppy.locales.tr_TR = tr_TR
+}
+
+module.exports = tr_TR

+ 10 - 8
src/plugins/Dashboard/Dashboard.js

@@ -57,13 +57,9 @@ module.exports = function Dashboard (props) {
           onpaste=${handlePaste}
           onpaste=${handlePaste}
           onload=${() => props.updateDashboardElWidth()}>
           onload=${() => props.updateDashboardElWidth()}>
 
 
-    <button class="UppyDashboard-close"
-            type="button"
-            aria-label="${props.i18n('closeModal')}"
-            title="${props.i18n('closeModal')}"
-            onclick=${props.hideModal}>${closeIcon()}</button>
-
-    <div class="UppyDashboard-overlay" onclick=${props.hideModal}></div>
+    <div class="UppyDashboard-overlay" onclick=${() => {
+      // props.hideModal
+    }}></div>
 
 
     <div class="UppyDashboard-inner"
     <div class="UppyDashboard-inner"
          tabindex="0"
          tabindex="0"
@@ -71,6 +67,12 @@ module.exports = function Dashboard (props) {
           ${props.inline && props.maxWidth ? `max-width: ${props.maxWidth}px;` : ''}
           ${props.inline && props.maxWidth ? `max-width: ${props.maxWidth}px;` : ''}
           ${props.inline && props.maxHeight ? `max-height: ${props.maxHeight}px;` : ''}
           ${props.inline && props.maxHeight ? `max-height: ${props.maxHeight}px;` : ''}
          ">
          ">
+      <button class="UppyDashboard-close"
+              type="button"
+              aria-label="${props.i18n('closeModal')}"
+              title="${props.i18n('closeModal')}"
+              onclick=${props.hideModal}>${closeIcon()}</button>
+
       <div class="UppyDashboard-innerWrap">
       <div class="UppyDashboard-innerWrap">
 
 
         ${Tabs({
         ${Tabs({
@@ -116,7 +118,7 @@ module.exports = function Dashboard (props) {
           })}
           })}
 
 
           <div class="UppyDashboard-actions">
           <div class="UppyDashboard-actions">
-            ${!props.autoProceed && props.newFiles.length > 0
+            ${!props.hideUploadButton && !props.autoProceed && props.newFiles.length > 0
               ? UploadBtn({
               ? UploadBtn({
                 i18n: props.i18n,
                 i18n: props.i18n,
                 startUpload: props.startUpload,
                 startUpload: props.startUpload,

+ 9 - 9
src/plugins/Dashboard/FileItem.js

@@ -30,14 +30,6 @@ module.exports = function fileItem (props) {
                   id="uppy_${file.id}"
                   id="uppy_${file.id}"
                   title="${file.meta.name}">
                   title="${file.meta.name}">
       <div class="UppyDashboardItem-preview">
       <div class="UppyDashboardItem-preview">
-        ${file.source
-          ? html`<div class="UppyDashboardItem-sourceIcon">
-            ${acquirers.map(acquirer => {
-              if (acquirer.id === file.source) return html`<span title="${acquirer.name}">${acquirer.icon()}</span>`
-            })}
-          </div>`
-          : ''
-        }
         <div class="UppyDashboardItem-previewInnerWrap" style="background-color: ${getFileTypeIcon(file.type.general, file.type.specific).color}">
         <div class="UppyDashboardItem-previewInnerWrap" style="background-color: ${getFileTypeIcon(file.type.general, file.type.specific).color}">
           ${file.preview
           ${file.preview
             ? html`<img alt="${file.name}" src="${file.preview}">`
             ? html`<img alt="${file.name}" src="${file.preview}">`
@@ -94,7 +86,15 @@ module.exports = function fileItem (props) {
         }
         }
       </h4>
       </h4>
       <div class="UppyDashboardItem-status">
       <div class="UppyDashboardItem-status">
-        <span class="UppyDashboardItem-statusSize">${file.data.size ? prettyBytes(file.data.size) : ''}</span>
+        <div class="UppyDashboardItem-statusSize">${file.data.size ? prettyBytes(file.data.size) : ''}</div>
+        ${file.source
+          ? html`<div class="UppyDashboardItem-sourceIcon">
+            ${acquirers.map(acquirer => {
+              if (acquirer.id === file.source) return html`<span title="${acquirer.name}">${acquirer.icon()}</span>`
+            })}
+          </div>`
+          : ''
+        }
       </div>
       </div>
       ${!uploadInProgressOrComplete
       ${!uploadInProgressOrComplete
         ? html`<button class="UppyDashboardItem-edit"
         ? html`<button class="UppyDashboardItem-edit"

+ 1 - 1
src/plugins/Dashboard/Tabs.js

@@ -35,7 +35,7 @@ module.exports = (props) => {
                     input.click()
                     input.click()
                   }}>
                   }}>
             ${localIcon()}
             ${localIcon()}
-            <h5 class="UppyDashboardTab-name">${props.i18n('localDisk')}</h5>
+            <h5 class="UppyDashboardTab-name">${props.i18n('myDevice')}</h5>
           </button>
           </button>
           ${input}
           ${input}
         </li>
         </li>

+ 12 - 14
src/plugins/Dashboard/index.js

@@ -30,6 +30,7 @@ module.exports = class DashboardUI extends Plugin {
         copyLinkToClipboardFallback: 'Copy the URL below',
         copyLinkToClipboardFallback: 'Copy the URL below',
         done: 'Done',
         done: 'Done',
         localDisk: 'Local Disk',
         localDisk: 'Local Disk',
+        myDevice: 'My Device',
         dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
         dropPasteImport: 'Drop files here, paste, import from one of the locations above or',
         dropPaste: 'Drop files here, paste or',
         dropPaste: 'Drop files here, paste or',
         browse: 'browse',
         browse: 'browse',
@@ -49,6 +50,7 @@ module.exports = class DashboardUI extends Plugin {
       semiTransparent: false,
       semiTransparent: false,
       defaultTabIcon: defaultTabIcon(),
       defaultTabIcon: defaultTabIcon(),
       showProgressDetails: false,
       showProgressDetails: false,
+      hideUploadButton: false,
       note: false,
       note: false,
       locale: defaultLocale
       locale: defaultLocale
     }
     }
@@ -214,22 +216,17 @@ module.exports = class DashboardUI extends Plugin {
   }
   }
 
 
   actions () {
   actions () {
-    const bus = this.core.bus
-
-    // bus.on('core:file-add', this.hideAllPanels)
-    bus.on('core:file-added', this.hideAllPanels)
-    bus.on('dashboard:file-card', this.handleFileCard)
+    this.core.on('core:file-added', this.hideAllPanels)
+    this.core.on('dashboard:file-card', this.handleFileCard)
 
 
     window.addEventListener('resize', this.updateDashboardElWidth)
     window.addEventListener('resize', this.updateDashboardElWidth)
   }
   }
 
 
   removeActions () {
   removeActions () {
-    const bus = this.core.bus
-
     window.removeEventListener('resize', this.updateDashboardElWidth)
     window.removeEventListener('resize', this.updateDashboardElWidth)
 
 
-    bus.off('core:file-add', this.hideAllPanels)
-    bus.off('dashboard:file-card', this.handleFileCard)
+    this.core.off('core:file-added', this.hideAllPanels)
+    this.core.off('dashboard:file-card', this.handleFileCard)
   }
   }
 
 
   updateDashboardElWidth () {
   updateDashboardElWidth () {
@@ -258,7 +255,7 @@ module.exports = class DashboardUI extends Plugin {
     this.core.log('All right, someone dropped something...')
     this.core.log('All right, someone dropped something...')
 
 
     files.forEach((file) => {
     files.forEach((file) => {
-      this.core.bus.emit('core:file-add', {
+      this.core.addFile({
         source: this.id,
         source: this.id,
         name: file.name,
         name: file.name,
         type: file.type,
         type: file.type,
@@ -268,15 +265,15 @@ module.exports = class DashboardUI extends Plugin {
   }
   }
 
 
   cancelAll () {
   cancelAll () {
-    this.core.bus.emit('core:cancel-all')
+    this.core.emit('core:cancel-all')
   }
   }
 
 
   pauseAll () {
   pauseAll () {
-    this.core.bus.emit('core:pause-all')
+    this.core.emit('core:pause-all')
   }
   }
 
 
   resumeAll () {
   resumeAll () {
-    this.core.bus.emit('core:resume-all')
+    this.core.emit('core:resume-all')
   }
   }
 
 
   render (state) {
   render (state) {
@@ -348,7 +345,7 @@ module.exports = class DashboardUI extends Plugin {
     }
     }
 
 
     const info = (text, type, duration) => {
     const info = (text, type, duration) => {
-      this.core.emitter.emit('informer', text, type, duration)
+      this.core.info(text, type, duration)
     }
     }
 
 
     const resumableUploads = this.core.getState().capabilities.resumableUploads || false
     const resumableUploads = this.core.getState().capabilities.resumableUploads || false
@@ -364,6 +361,7 @@ module.exports = class DashboardUI extends Plugin {
       activePanel: state.modal.activePanel,
       activePanel: state.modal.activePanel,
       progressindicators: progressindicators,
       progressindicators: progressindicators,
       autoProceed: this.core.opts.autoProceed,
       autoProceed: this.core.opts.autoProceed,
+      hideUploadButton: this.opts.hideUploadButton,
       id: this.id,
       id: this.id,
       hideModal: this.hideModal,
       hideModal: this.hideModal,
       showProgressDetails: this.opts.showProgressDetails,
       showProgressDetails: this.opts.showProgressDetails,

+ 1 - 1
src/plugins/DragDrop/index.js

@@ -102,7 +102,7 @@ module.exports = class DragDrop extends Plugin {
     const files = toArray(ev.target.files)
     const files = toArray(ev.target.files)
 
 
     files.forEach((file) => {
     files.forEach((file) => {
-      this.core.emitter.emit('core:file-add', {
+      this.core.addFile({
         source: this.id,
         source: this.id,
         name: file.name,
         name: file.name,
         type: file.type,
         type: file.type,

+ 1 - 5
src/plugins/Dropbox/index.js

@@ -23,7 +23,7 @@ module.exports = class Dropbox extends Plugin {
 
 
     // writing out the key explicitly for readability the key used to store
     // writing out the key explicitly for readability the key used to store
     // the provider instance must be equal to this.id.
     // the provider instance must be equal to this.id.
-    this.Dropbox = new Provider({
+    this.Dropbox = new Provider(core, {
       host: this.opts.host,
       host: this.opts.host,
       provider: 'dropbox'
       provider: 'dropbox'
     })
     })
@@ -61,10 +61,6 @@ module.exports = class Dropbox extends Plugin {
     const target = this.opts.target
     const target = this.opts.target
     const plugin = this
     const plugin = this
     this.target = this.mount(target, plugin)
     this.target = this.mount(target, plugin)
-
-    this[this.id].auth().then(this.onAuth).catch(this.view.handleError)
-
-    return
   }
   }
 
 
   uninstall () {
   uninstall () {

+ 1 - 1
src/plugins/FileInput.js

@@ -46,7 +46,7 @@ module.exports = class FileInput extends Plugin {
     const files = toArray(ev.target.files)
     const files = toArray(ev.target.files)
 
 
     files.forEach((file) => {
     files.forEach((file) => {
-      this.core.emitter.emit('core:file-add', {
+      this.core.addFile({
         source: this.id,
         source: this.id,
         name: file.name,
         name: file.name,
         type: file.type,
         type: file.type,

+ 1 - 5
src/plugins/GoogleDrive/index.js

@@ -20,7 +20,7 @@ module.exports = class Google extends Plugin {
 
 
     // writing out the key explicitly for readability the key used to store
     // writing out the key explicitly for readability the key used to store
     // the provider instance must be equal to this.id.
     // the provider instance must be equal to this.id.
-    this.GoogleDrive = new Provider({
+    this.GoogleDrive = new Provider(core, {
       host: this.opts.host,
       host: this.opts.host,
       provider: 'drive',
       provider: 'drive',
       authProvider: 'google'
       authProvider: 'google'
@@ -59,10 +59,6 @@ module.exports = class Google extends Plugin {
     const target = this.opts.target
     const target = this.opts.target
     const plugin = this
     const plugin = this
     this.target = this.mount(target, plugin)
     this.target = this.mount(target, plugin)
-
-    // catch error here.
-    this[this.id].auth().then(this.onAuth).catch(this.view.handleError)
-    return
   }
   }
 
 
   uninstall () {
   uninstall () {

+ 6 - 62
src/plugins/Informer.js

@@ -4,8 +4,8 @@ const html = require('yo-yo')
 /**
 /**
  * Informer
  * Informer
  * Shows rad message bubbles
  * Shows rad message bubbles
- * used like this: `bus.emit('informer', 'hello world', 'info', 5000)`
- * or for errors: `bus.emit('informer', 'Error uploading img.jpg', 'error', 5000)`
+ * used like this: `core.emit('informer', 'hello world', 'info', 5000)`
+ * or for errors: `core.emit('informer', 'Error uploading img.jpg', 'error', 5000)`
  *
  *
  */
  */
 module.exports = class Informer extends Plugin {
 module.exports = class Informer extends Plugin {
@@ -14,7 +14,7 @@ module.exports = class Informer extends Plugin {
     this.type = 'progressindicator'
     this.type = 'progressindicator'
     this.id = 'Informer'
     this.id = 'Informer'
     this.title = 'Informer'
     this.title = 'Informer'
-    this.timeoutID = undefined
+    // this.timeoutID = undefined
 
 
     // set default options
     // set default options
     const defaultOptions = {
     const defaultOptions = {
@@ -44,45 +44,10 @@ module.exports = class Informer extends Plugin {
     this.render = this.render.bind(this)
     this.render = this.render.bind(this)
   }
   }
 
 
-  showInformer (msg, type, duration) {
-    this.core.setState({
-      informer: {
-        isHidden: false,
-        type: type,
-        msg: msg
-      }
-    })
-
-    window.clearTimeout(this.timeoutID)
-    if (duration === 0) {
-      this.timeoutID = undefined
-      return
-    }
-
-    // hide the informer after `duration` milliseconds
-    this.timeoutID = setTimeout(() => {
-      const newInformer = Object.assign({}, this.core.getState().informer, {
-        isHidden: true
-      })
-      this.core.setState({
-        informer: newInformer
-      })
-    }, duration)
-  }
-
-  hideInformer () {
-    const newInformer = Object.assign({}, this.core.getState().informer, {
-      isHidden: true
-    })
-    this.core.setState({
-      informer: newInformer
-    })
-  }
-
   render (state) {
   render (state) {
-    const isHidden = state.informer.isHidden
-    const msg = state.informer.msg
-    const type = state.informer.type || 'info'
+    const isHidden = state.info.isHidden
+    const msg = state.info.msg
+    const type = state.info.type || 'info'
     const style = `background-color: ${this.opts.typeColors[type].bg}; color: ${this.opts.typeColors[type].text};`
     const style = `background-color: ${this.opts.typeColors[type].bg}; color: ${this.opts.typeColors[type].text};`
 
 
     // @TODO add aria-live for screen-readers
     // @TODO add aria-live for screen-readers
@@ -92,29 +57,8 @@ module.exports = class Informer extends Plugin {
   }
   }
 
 
   install () {
   install () {
-    // Set default state for Google Drive
-    this.core.setState({
-      informer: {
-        isHidden: true,
-        type: '',
-        msg: ''
-      }
-    })
-
-    this.core.on('informer', (msg, type, duration) => {
-      this.showInformer(msg, type, duration)
-    })
-
-    this.core.on('informer:hide', () => {
-      this.hideInformer()
-    })
-
     const target = this.opts.target
     const target = this.opts.target
     const plugin = this
     const plugin = this
     this.target = this.mount(target, plugin)
     this.target = this.mount(target, plugin)
   }
   }
-
-  uninstall () {
-    this.unmount()
-  }
 }
 }

+ 18 - 8
src/plugins/Instagram/index.js

@@ -24,7 +24,7 @@ module.exports = class Instagram extends Plugin {
 
 
     // writing out the key explicitly for readability the key used to store
     // writing out the key explicitly for readability the key used to store
     // the provider instance must be equal to this.id.
     // the provider instance must be equal to this.id.
-    this.Instagram = new Provider({
+    this.Instagram = new Provider(core, {
       host: this.opts.host,
       host: this.opts.host,
       provider: 'instagram',
       provider: 'instagram',
       authProvider: 'instagram'
       authProvider: 'instagram'
@@ -65,10 +65,6 @@ module.exports = class Instagram extends Plugin {
     const target = this.opts.target
     const target = this.opts.target
     const plugin = this
     const plugin = this
     this.target = this.mount(target, plugin)
     this.target = this.mount(target, plugin)
-
-    // catch error here.
-    this[this.id].auth().then(this.onAuth).catch(this.view.handleError)
-    return
   }
   }
 
 
   uninstall () {
   uninstall () {
@@ -95,7 +91,20 @@ module.exports = class Instagram extends Plugin {
   }
   }
 
 
   getItemSubList (item) {
   getItemSubList (item) {
-    return item.data
+    const subItems = []
+    item.data.forEach((subItem) => {
+      if (subItem.carousel_media) {
+        subItem.carousel_media.forEach((i, index) => {
+          const { id, created_time } = subItem
+          const newSubItem = Object.assign({}, i, { id, created_time })
+          newSubItem.carousel_id = index
+          subItems.push(newSubItem)
+        })
+      } else {
+        subItems.push(subItem)
+      }
+    })
+    return subItems
   }
   }
 
 
   getItemName (item) {
   getItemName (item) {
@@ -107,11 +116,12 @@ module.exports = class Instagram extends Plugin {
   }
   }
 
 
   getItemId (item) {
   getItemId (item) {
-    return item.id
+    return `${item.id}${item.carousel_id || ''}`
   }
   }
 
 
   getItemRequestPath (item) {
   getItemRequestPath (item) {
-    return this.getItemId(item)
+    const suffix = isNaN(item.carousel_id) ? '' : `?carousel_id=${item.carousel_id}`
+    return `${item.id}${suffix}`
   }
   }
 
 
   getItemModifiedDate (item) {
   getItemModifiedDate (item) {

+ 1 - 1
src/plugins/Plugin.js

@@ -100,6 +100,6 @@ module.exports = class Plugin {
   }
   }
 
 
   uninstall () {
   uninstall () {
-    return
+    this.unmount()
   }
   }
 }
 }

+ 3 - 3
src/plugins/StatusBar/index.js

@@ -32,15 +32,15 @@ module.exports = class StatusBarUI extends Plugin {
   }
   }
 
 
   cancelAll () {
   cancelAll () {
-    this.core.bus.emit('core:cancel-all')
+    this.core.emit('core:cancel-all')
   }
   }
 
 
   pauseAll () {
   pauseAll () {
-    this.core.bus.emit('core:pause-all')
+    this.core.emit('core:pause-all')
   }
   }
 
 
   resumeAll () {
   resumeAll () {
-    this.core.bus.emit('core:resume-all')
+    this.core.emit('core:resume-all')
   }
   }
 
 
   getTotalSpeed (files) {
   getTotalSpeed (files) {

+ 26 - 24
src/plugins/Tus10.js

@@ -37,7 +37,8 @@ module.exports = class Tus10 extends Plugin {
     const defaultOptions = {
     const defaultOptions = {
       resume: true,
       resume: true,
       allowPause: true,
       allowPause: true,
-      autoRetry: true
+      autoRetry: true,
+      retryDelays: [0, 1000, 3000, 5000]
     }
     }
 
 
     // merge default options with the ones set by user
     // merge default options with the ones set by user
@@ -126,13 +127,13 @@ module.exports = class Tus10 extends Plugin {
 
 
       optsTus.onError = (err) => {
       optsTus.onError = (err) => {
         this.core.log(err)
         this.core.log(err)
-        this.core.emitter.emit('core:upload-error', file.id, err)
+        this.core.emit('core:upload-error', file.id, err)
         reject('Failed because: ' + err)
         reject('Failed because: ' + err)
       }
       }
 
 
       optsTus.onProgress = (bytesUploaded, bytesTotal) => {
       optsTus.onProgress = (bytesUploaded, bytesTotal) => {
         this.onReceiveUploadUrl(file, upload.url)
         this.onReceiveUploadUrl(file, upload.url)
-        this.core.emitter.emit('core:upload-progress', {
+        this.core.emit('core:upload-progress', {
           uploader: this,
           uploader: this,
           id: file.id,
           id: file.id,
           bytesUploaded: bytesUploaded,
           bytesUploaded: bytesUploaded,
@@ -141,7 +142,7 @@ module.exports = class Tus10 extends Plugin {
       }
       }
 
 
       optsTus.onSuccess = () => {
       optsTus.onSuccess = () => {
-        this.core.emitter.emit('core:upload-success', file.id, upload, upload.url)
+        this.core.emit('core:upload-success', file.id, upload, upload.url)
 
 
         if (upload.url) {
         if (upload.url) {
           this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
           this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
@@ -153,10 +154,10 @@ module.exports = class Tus10 extends Plugin {
 
 
       const upload = new tus.Upload(file.data, optsTus)
       const upload = new tus.Upload(file.data, optsTus)
 
 
-      this.onFileRemove(file.id, () => {
-        this.core.log('removing file:', file.id)
+      this.onFileRemove(file.id, (targetFileID) => {
+        // this.core.log(`removing file: ${targetFileID}`)
         upload.abort()
         upload.abort()
-        resolve(`upload ${file.id} was removed`)
+        resolve(`upload ${targetFileID} was removed`)
       })
       })
 
 
       this.onPause(file.id, (isPaused) => {
       this.onPause(file.id, (isPaused) => {
@@ -183,7 +184,7 @@ module.exports = class Tus10 extends Plugin {
       })
       })
 
 
       upload.start()
       upload.start()
-      this.core.emitter.emit('core:upload-started', file.id, upload)
+      this.core.emit('core:upload-started', file.id, upload)
     })
     })
   }
   }
 
 
@@ -195,7 +196,7 @@ module.exports = class Tus10 extends Plugin {
         endpoint = file.tus.endpoint
         endpoint = file.tus.endpoint
       }
       }
 
 
-      this.core.emitter.emit('core:upload-started', file.id)
+      this.core.emit('core:upload-started', file.id)
 
 
       fetch(file.remote.url, {
       fetch(file.remote.url, {
         method: 'post',
         method: 'post',
@@ -221,9 +222,9 @@ module.exports = class Tus10 extends Plugin {
           const host = Utils.getSocketHost(file.remote.host)
           const host = Utils.getSocketHost(file.remote.host)
           const socket = new UppySocket({ target: `${host}/api/${token}` })
           const socket = new UppySocket({ target: `${host}/api/${token}` })
 
 
-          this.onFileRemove(file.id, () => {
+          this.onFileRemove(file.id, (targetFileID) => {
             socket.send('pause', {})
             socket.send('pause', {})
-            resolve(`upload ${file.id} was removed`)
+            resolve(`upload ${targetFileID} was removed`)
           })
           })
 
 
           this.onPause(file.id, (isPaused) => {
           this.onPause(file.id, (isPaused) => {
@@ -241,7 +242,7 @@ module.exports = class Tus10 extends Plugin {
           socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
           socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
 
 
           socket.on('success', (data) => {
           socket.on('success', (data) => {
-            this.core.emitter.emit('core:upload-success', file.id, data, data.url)
+            this.core.emit('core:upload-success', file.id, data, data.url)
             socket.close()
             socket.close()
             return resolve()
             return resolve()
           })
           })
@@ -256,6 +257,7 @@ module.exports = class Tus10 extends Plugin {
 
 
   onReceiveUploadUrl (file, uploadURL) {
   onReceiveUploadUrl (file, uploadURL) {
     const currentFile = this.getFile(file.id)
     const currentFile = this.getFile(file.id)
+    if (!currentFile) return
     // Only do the update if we didn't have an upload URL yet.
     // Only do the update if we didn't have an upload URL yet.
     if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
     if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
       const newFile = Object.assign({}, currentFile, {
       const newFile = Object.assign({}, currentFile, {
@@ -271,13 +273,13 @@ module.exports = class Tus10 extends Plugin {
   }
   }
 
 
   onFileRemove (fileID, cb) {
   onFileRemove (fileID, cb) {
-    this.core.emitter.on('core:file-remove', (targetFileID) => {
-      if (fileID === targetFileID) cb()
+    this.core.on('core:file-removed', (targetFileID) => {
+      if (fileID === targetFileID) cb(targetFileID)
     })
     })
   }
   }
 
 
   onPause (fileID, cb) {
   onPause (fileID, cb) {
-    this.core.emitter.on('core:upload-pause', (targetFileID) => {
+    this.core.on('core:upload-pause', (targetFileID) => {
       if (fileID === targetFileID) {
       if (fileID === targetFileID) {
         const isPaused = this.pauseResume('toggle', fileID)
         const isPaused = this.pauseResume('toggle', fileID)
         cb(isPaused)
         cb(isPaused)
@@ -286,14 +288,14 @@ module.exports = class Tus10 extends Plugin {
   }
   }
 
 
   onPauseAll (fileID, cb) {
   onPauseAll (fileID, cb) {
-    this.core.emitter.on('core:pause-all', () => {
+    this.core.on('core:pause-all', () => {
       if (!this.core.getFile(fileID)) return
       if (!this.core.getFile(fileID)) return
       cb()
       cb()
     })
     })
   }
   }
 
 
   onResumeAll (fileID, cb) {
   onResumeAll (fileID, cb) {
-    this.core.emitter.on('core:resume-all', () => {
+    this.core.on('core:resume-all', () => {
       if (!this.core.getFile(fileID)) return
       if (!this.core.getFile(fileID)) return
       cb()
       cb()
     })
     })
@@ -324,17 +326,17 @@ module.exports = class Tus10 extends Plugin {
     this.uploadFiles(filesToUpload)
     this.uploadFiles(filesToUpload)
 
 
     return new Promise((resolve) => {
     return new Promise((resolve) => {
-      this.core.bus.once('core:upload-complete', resolve)
+      this.core.once('core:upload-complete', resolve)
     })
     })
   }
   }
 
 
   actions () {
   actions () {
-    this.core.emitter.on('core:pause-all', this.handlePauseAll)
-    this.core.emitter.on('core:resume-all', this.handleResumeAll)
+    this.core.on('core:pause-all', this.handlePauseAll)
+    this.core.on('core:resume-all', this.handleResumeAll)
 
 
     if (this.opts.autoRetry) {
     if (this.opts.autoRetry) {
-      this.core.emitter.on('back-online', () => {
-        this.core.emitter.emit('core:retry-started')
+      this.core.on('back-online', () => {
+        this.core.emit('core:retry-started')
       })
       })
     }
     }
   }
   }
@@ -355,7 +357,7 @@ module.exports = class Tus10 extends Plugin {
 
 
   uninstall () {
   uninstall () {
     this.core.removeUploader(this.handleUpload)
     this.core.removeUploader(this.handleUpload)
-    this.core.emitter.off('core:pause-all', this.handlePauseAll)
-    this.core.emitter.off('core:resume-all', this.handleResumeAll)
+    this.core.off('core:pause-all', this.handlePauseAll)
+    this.core.off('core:resume-all', this.handleResumeAll)
   }
   }
 }
 }

+ 6 - 6
src/plugins/Webcam/index.js

@@ -168,7 +168,7 @@ module.exports = class Webcam extends Plugin {
           data: new Blob(this.recordingChunks, { type: mimeType })
           data: new Blob(this.recordingChunks, { type: mimeType })
         }
         }
 
 
-        this.core.emitter.emit('core:file-add', file)
+        this.core.addfile(file)
 
 
         this.recordingChunks = null
         this.recordingChunks = null
         this.recorder = null
         this.recorder = null
@@ -204,11 +204,11 @@ module.exports = class Webcam extends Plugin {
         }
         }
 
 
         if (count > 0) {
         if (count > 0) {
-          this.core.emit('informer', `${count}...`, 'warning', 800)
+          this.core.info(`${count}...`, 'warning', 800)
           count--
           count--
         } else {
         } else {
           clearInterval(countDown)
           clearInterval(countDown)
-          this.core.emit('informer', this.i18n('smile'), 'success', 1500)
+          this.core.info(this.i18n('smile'), 'success', 1500)
           setTimeout(() => resolve(), 1500)
           setTimeout(() => resolve(), 1500)
         }
         }
       }, 1000)
       }, 1000)
@@ -217,7 +217,7 @@ module.exports = class Webcam extends Plugin {
 
 
   // justSmile () {
   // justSmile () {
   //   return new Promise((resolve, reject) => {
   //   return new Promise((resolve, reject) => {
-  //     setTimeout(() => this.core.emit('informer', this.i18n('smile'), 'success', 1000), 1500)
+  //     setTimeout(() => this.core.info(this.i18n('smile'), 'success', 1000), 1500)
   //     setTimeout(() => resolve(), 2000)
   //     setTimeout(() => resolve(), 2000)
   //   })
   //   })
   // }
   // }
@@ -234,7 +234,7 @@ module.exports = class Webcam extends Plugin {
     this.captureInProgress = true
     this.captureInProgress = true
 
 
     this.opts.onBeforeSnapshot().catch((err) => {
     this.opts.onBeforeSnapshot().catch((err) => {
-      this.emit('informer', err, 'error', 5000)
+      this.core.info(err, 'error', 5000)
       return Promise.reject(`onBeforeSnapshot: ${err}`)
       return Promise.reject(`onBeforeSnapshot: ${err}`)
     }).then(() => {
     }).then(() => {
       const video = this.target.querySelector('.UppyWebcam-video')
       const video = this.target.querySelector('.UppyWebcam-video')
@@ -260,7 +260,7 @@ module.exports = class Webcam extends Plugin {
   focus () {
   focus () {
     if (this.opts.countdown) return
     if (this.opts.countdown) return
     setTimeout(() => {
     setTimeout(() => {
-      this.core.emit('informer', this.i18n('smile'), 'success', 1500)
+      this.core.info(this.i18n('smile'), 'success', 1500)
     }, 1000)
     }, 1000)
   }
   }
 
 

+ 10 - 10
src/plugins/XHRUpload.js

@@ -66,7 +66,7 @@ module.exports = class XHRUpload extends Plugin {
 
 
       xhr.upload.addEventListener('progress', (ev) => {
       xhr.upload.addEventListener('progress', (ev) => {
         if (ev.lengthComputable) {
         if (ev.lengthComputable) {
-          this.core.emitter.emit('core:upload-progress', {
+          this.core.emit('core:upload-progress', {
             uploader: this,
             uploader: this,
             id: file.id,
             id: file.id,
             bytesUploaded: ev.loaded,
             bytesUploaded: ev.loaded,
@@ -80,7 +80,7 @@ module.exports = class XHRUpload extends Plugin {
           const resp = opts.getResponseData(xhr)
           const resp = opts.getResponseData(xhr)
           const uploadURL = resp[opts.responseUrlFieldName]
           const uploadURL = resp[opts.responseUrlFieldName]
 
 
-          this.core.emitter.emit('core:upload-success', file.id, resp, uploadURL)
+          this.core.emit('core:upload-success', file.id, resp, uploadURL)
 
 
           if (uploadURL) {
           if (uploadURL) {
             this.core.log(`Download ${file.name} from ${file.uploadURL}`)
             this.core.log(`Download ${file.name} from ${file.uploadURL}`)
@@ -88,7 +88,7 @@ module.exports = class XHRUpload extends Plugin {
 
 
           return resolve(file)
           return resolve(file)
         } else {
         } else {
-          this.core.emitter.emit('core:upload-error', file.id, xhr)
+          this.core.emit('core:upload-error', file.id, xhr)
           return reject('Upload error')
           return reject('Upload error')
         }
         }
 
 
@@ -102,7 +102,7 @@ module.exports = class XHRUpload extends Plugin {
       })
       })
 
 
       xhr.addEventListener('error', (ev) => {
       xhr.addEventListener('error', (ev) => {
-        this.core.emitter.emit('core:upload-error', file.id)
+        this.core.emit('core:upload-error', file.id)
         return reject('Upload error')
         return reject('Upload error')
       })
       })
 
 
@@ -114,26 +114,26 @@ module.exports = class XHRUpload extends Plugin {
 
 
       xhr.send(data)
       xhr.send(data)
 
 
-      this.core.emitter.on('core:upload-cancel', (fileID) => {
+      this.core.on('core:upload-cancel', (fileID) => {
         if (fileID === file.id) {
         if (fileID === file.id) {
           xhr.abort()
           xhr.abort()
         }
         }
       })
       })
 
 
-      this.core.emitter.on('core:cancel-all', () => {
+      this.core.on('core:cancel-all', () => {
         // const files = this.core.getState().files
         // const files = this.core.getState().files
         // if (!files[file.id]) return
         // if (!files[file.id]) return
         xhr.abort()
         xhr.abort()
       })
       })
 
 
-      this.core.emitter.emit('core:upload-started', file.id)
+      this.core.emit('core:upload-started', file.id)
     })
     })
   }
   }
 
 
   uploadRemote (file, current, total) {
   uploadRemote (file, current, total) {
     const opts = Object.assign({}, this.opts, file.xhrUpload || {})
     const opts = Object.assign({}, this.opts, file.xhrUpload || {})
     return new Promise((resolve, reject) => {
     return new Promise((resolve, reject) => {
-      this.core.emitter.emit('core:upload-started', file.id)
+      this.core.emit('core:upload-started', file.id)
 
 
       fetch(file.remote.url, {
       fetch(file.remote.url, {
         method: 'post',
         method: 'post',
@@ -161,7 +161,7 @@ module.exports = class XHRUpload extends Plugin {
           socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
           socket.on('progress', (progressData) => Utils.emitSocketProgress(this, progressData, file))
 
 
           socket.on('success', (data) => {
           socket.on('success', (data) => {
-            this.core.emitter.emit('core:upload-success', file.id, data, data.url)
+            this.core.emit('core:upload-success', file.id, data, data.url)
             socket.close()
             socket.close()
             return resolve()
             return resolve()
           })
           })
@@ -206,7 +206,7 @@ module.exports = class XHRUpload extends Plugin {
     this.selectForUpload(files)
     this.selectForUpload(files)
 
 
     return new Promise((resolve) => {
     return new Promise((resolve) => {
-      this.core.bus.once('core:upload-complete', resolve)
+      this.core.once('core:upload-complete', resolve)
     })
     })
   }
   }
 
 

+ 29 - 29
src/scss/_dashboard.scss

@@ -78,29 +78,28 @@
 .UppyDashboard-close {
 .UppyDashboard-close {
   @include reset-button;
   @include reset-button;
   display: none;
   display: none;
-  position: fixed;
-  top: 12px;
-  right: 12px;
+  position: absolute;
+  top: 7px;
+  right: 7px;
   cursor: pointer;
   cursor: pointer;
-  color: lighten($color-asphalt-gray, 25%);
+  color: lighten($color-asphalt-gray, 20%);
   z-index: $zIndex-4;
   z-index: $zIndex-4;
   transition: all 0.2s;
   transition: all 0.2s;
 
 
-  &:hover { color: $color-black; }
+  &:hover { color: $color-cornflower-blue; }
 
 
   .UppyIcon {
   .UppyIcon {
-    width: 14px;
-    height: 14px;
+    width: 12px;
+    height: 12px;
   }
   }
 
 
   .UppyDashboard--wide & {
   .UppyDashboard--wide & {
-    color: $color-asphalt-gray;
-    top: 16px;
-    right: 16px;
+    top: 10px;
+    right: 10px;
 
 
     .UppyIcon {
     .UppyIcon {
-      width: 17px;
-      height: 17px;
+      width: 14px;
+      height: 14px;
     }
     }
   }
   }
 
 
@@ -152,7 +151,7 @@
 
 
 .UppyDashboardTab {
 .UppyDashboardTab {
   width: 70px;
   width: 70px;
-  margin: 0 2px;
+  margin: 0;
   display: inline-block;
   display: inline-block;
   text-align: center;
   text-align: center;
 
 
@@ -195,8 +194,8 @@
 
 
 // On SVG sizing: https://sarasoueidan.com/blog/svg-style-inheritance-and-FOUSVG/
 // On SVG sizing: https://sarasoueidan.com/blog/svg-style-inheritance-and-FOUSVG/
 .UppyDashboardTab .UppyIcon {
 .UppyDashboardTab .UppyIcon {
-  width: 20px;
-  height: 20px;
+  width: 18px;
+  height: 18px;
   vertical-align: middle;
   vertical-align: middle;
 
 
   .UppyDashboard--wide & {
   .UppyDashboard--wide & {
@@ -475,25 +474,24 @@
 }
 }
 
 
 .UppyDashboardItem-sourceIcon {
 .UppyDashboardItem-sourceIcon {
-  color: rgba($color-white, 0.95);
-  position: absolute;
-  top: 4px;
-  left: 4px;
-  width: 10px;
-  height: 10px;
-  z-index: $zIndex-3;
+  // color: rgba($color-white, 0.95);
+  position: relative;
+  left: 3px;
+  display: inline-block;
+  vertical-align: middle;
+  width: 7px;
+  height: 7px;
+  opacity: 0.7;
 
 
   .UppyDashboard--wide & {
   .UppyDashboard--wide & {
-    top: 8px;
-    left: 8px;
-    width: 15px;
-    height: 15px;
+    width: 10px;
+    height: 10px;
   }
   }
 }
 }
 
 
-.UppyDashboardItem-sourceIcon .UppyIcon {
-  filter: drop-shadow(0 0 1px rgba(0,0,0,0.3));
-}
+// .UppyDashboardItem-sourceIcon .UppyIcon {
+//   filter: drop-shadow(0 0 1px rgba(0,0,0,0.3));
+// }
 
 
 .UppyDashboardItem-previewInnerWrap {
 .UppyDashboardItem-previewInnerWrap {
   width: 100%;
   width: 100%;
@@ -606,6 +604,8 @@
 }
 }
 
 
 .UppyDashboardItem-statusSize {
 .UppyDashboardItem-statusSize {
+  display: inline-block;
+  vertical-align: middle;
   text-transform: uppercase;
   text-transform: uppercase;
 }
 }
 
 

+ 39 - 5
src/uppy-base/src/plugins/Provider.js

@@ -7,16 +7,40 @@ const _getName = (id) => {
 }
 }
 
 
 module.exports = class Provider {
 module.exports = class Provider {
-  constructor (opts) {
+  constructor (core, opts) {
+    this.core = core
     this.opts = opts
     this.opts = opts
     this.provider = opts.provider
     this.provider = opts.provider
     this.id = this.provider
     this.id = this.provider
     this.authProvider = opts.authProvider || this.provider
     this.authProvider = opts.authProvider || this.provider
     this.name = this.opts.name || _getName(this.id)
     this.name = this.opts.name || _getName(this.id)
+
+    this.onReceiveResponse = this.onReceiveResponse.bind(this)
+  }
+
+  get hostname () {
+    const uppyServer = this.core.state.uppyServer || {}
+    const host = this.opts.host
+    return uppyServer[host] || host
+  }
+
+  onReceiveResponse (response) {
+    const uppyServer = this.core.state.uppyServer || {}
+    const host = this.opts.host
+    const headers = response.headers
+    // Store the self-identified domain name for the uppy-server we just hit.
+    if (headers.has('i-am') && headers.get('i-am') !== uppyServer[host]) {
+      this.core.setState({
+        uppyServer: Object.assign({}, uppyServer, {
+          [host]: headers.get('i-am')
+        })
+      })
+    }
+    return response
   }
   }
 
 
-  auth () {
-    return fetch(`${this.opts.host}/${this.id}/auth`, {
+  checkAuth () {
+    return fetch(`${this.hostname}/${this.id}/authorized`, {
       method: 'get',
       method: 'get',
       credentials: 'include',
       credentials: 'include',
       headers: {
       headers: {
@@ -24,6 +48,7 @@ module.exports = class Provider {
         'Content-Type': 'application/json'
         'Content-Type': 'application/json'
       }
       }
     })
     })
+    .then(this.onReceiveResponse)
     .then((res) => {
     .then((res) => {
       return res.json()
       return res.json()
       .then((payload) => {
       .then((payload) => {
@@ -32,8 +57,16 @@ module.exports = class Provider {
     })
     })
   }
   }
 
 
+  authUrl () {
+    return `${this.opts.host}/${this.id}/connect`
+  }
+
+  fileUrl (id) {
+    return `${this.opts.host}/${this.id}/get/${id}`
+  }
+
   list (directory) {
   list (directory) {
-    return fetch(`${this.opts.host}/${this.id}/list/${directory || ''}`, {
+    return fetch(`${this.hostname}/${this.id}/list/${directory || ''}`, {
       method: 'get',
       method: 'get',
       credentials: 'include',
       credentials: 'include',
       headers: {
       headers: {
@@ -41,11 +74,12 @@ module.exports = class Provider {
         'Content-Type': 'application/json'
         'Content-Type': 'application/json'
       }
       }
     })
     })
+    .then(this.onReceiveResponse)
     .then((res) => res.json())
     .then((res) => res.json())
   }
   }
 
 
   logout (redirect = location.href) {
   logout (redirect = location.href) {
-    return fetch(`${this.opts.host}/${this.id}/logout?redirect=${redirect}`, {
+    return fetch(`${this.hostname}/${this.id}/logout?redirect=${redirect}`, {
       method: 'get',
       method: 'get',
       credentials: 'include',
       credentials: 'include',
       headers: {
       headers: {

+ 1 - 1
test/acceptance/tools.js

@@ -9,7 +9,7 @@ function uppySelectFakeFile () {
     ['data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='],
     ['data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='],
     {type: 'image/svg+xml'}
     {type: 'image/svg+xml'}
   )
   )
-  window.UppyAddFile({
+  window._uppy.addFile({
     source: 'acceptance-test',
     source: 'acceptance-test',
     name: 'test-file',
     name: 'test-file',
     type: 'image/svg+xml',
     type: 'image/svg+xml',

File diff suppressed because it is too large
+ 553 - 82
website/package-lock.json


+ 1 - 1
website/package.json

@@ -44,4 +44,4 @@
     "remark": "5.0.1",
     "remark": "5.0.1",
     "watchify": "3.7.0"
     "watchify": "3.7.0"
   }
   }
-}
+}

+ 102 - 0
website/src/_posts/2017-07-golden-retriever.md

@@ -0,0 +1,102 @@
+---
+title: "The Golden Retriever: Making uploads survive browser crashes"
+date: 2017-07-31
+author: arturi
+image: "http://uppy.io/images/blog/golden-retriever/uppy-team-kong.jpg"
+published: true
+---
+
+**TL;DR:** We're on a mission to improve file uploading on the web. We released [tus](https://tus.io): the open protocol for resumable file uploads, as well as Uppy: the next open source file uploader for web browsers. Uppy uses tus, which makes it resilient to poor network conditions and crashing servers. Today we’re launching an Uppy feature that also makes it resilient to browser crashes, which we believe is an industry first. We’re sharing a quick [demo](/blog/2017/07/golden-retriever/#demo) video, a bit of [background](/blog/2017/07/golden-retriever/#uppy), [how](/blog/2017/07/golden-retriever/#how) exactly we achieved this, and how you can [try](/blog/2017/07/golden-retriever/#try) it yourself.
+
+\***
+
+Don’t you just hate it when you’re about to share the perfect photos from your trip to Iceland, and halfway through, your cat jumps on the keyboard and trashes your browser? Or the battery in your laptop dies? Or you accidentally close the tab or navigate away? We hate that too!
+
+If action games have had checkpoints since 1687 — why can’t file uploaders? Well, as it turns out, they can! We found a way to get those Iceland pics into the hands of your loved ones with near-zero levels of frustration, even after a dreaded Blue Screen of Death! (if that is still a thing ;)
+
+<!-- more -->
+
+<a name="demo"></a>
+
+## Demo
+
+First off, let’s show you a demo 📹 of Uppy surviving a browser crash and picking up right where we left it:
+
+<figure class="wide"><video alt="Demo video showing the Golden Retriever file restoring plugin in action" controls><source src="/images/blog/golden-retriever/uppy-golden-retriever-crash-demo-2.mp4" type="video/mp4">Your browser does not support the video tag, you can <a href="/images/blog/golden-retriever/uppy-golden-retriever-crash-demo-2.mp4">download the video</a> to watch it.</video></figure>
+
+<a name="uppy"></a>
+
+## Uppy?
+
+For those of you who are new here, Uppy is the next-gen open source file uploader for the web. It is made by Transloadit and thus it works great with their uploading & encoding platform — but it also works great without! Simply add Uppy JavaScript to your website, deploy your own tusd/Node.js/Apache/Nginx server, and be on your way. Add [uppy-server](https://github.com/transloadit/uppy-server), and your users will be able to pick files from remote sources like Dropbox and Instagram. Uppy’s focus is on the modern web, and we go through extreme lengths to achieve the smoothest of user experiences, and the most durable of reliabilities. 🙃
+
+## Hacking trip
+
+Our core team is spread across three continents and five cities, and most of us have never met in person, with the majority of communication happening in GitHub and Slack. Just last week, we got together in Berlin for a crazy week of pink limo rides, Indian food and Mario Kart 64. More on that coming soon on the [Transloadit blog](https://transloadit.com/blog/).
+
+<figure class="wide"><img src="/images/blog/golden-retriever/uppy-team-kong.jpg"></figure>
+
+While enjoying some world-famous-in-Germany “Flammkuchen”, we were thinking about even more ways to make file uploading better (yes, we really can’t stop thinking about that). We then sat together in one room for a few days of hacking and came up with something neat. 
+
+## The Golden Retriever
+
+Uppy has a new friend to play with. Meet the Golden Retriever, our file recovery plugin:
+
+<center><img src="/images/blog/golden-retriever/catch-fail-2.gif" alt="Golden Retriever failing to catch something" title="Good try, girl!"></center>
+
+As you can see, we’re not yet fully done with training her, but we’re getting there! 😄
+
+But wait, we can hear you think, didn't [tus.io](https://tus.io) already make resumable uploads possible? Yes indeed, and it does an awesome job at recovering from poor network conditions. However, if your browser suddenly decided to crash, Uppy would have no idea about what it was doing before, and you would have to re-select and edit your files all over. 
+
+<center><img src="/images/blog/golden-retriever/no-idea-dog-3.gif" alt="Dog has no idea what he is doing" title="Keep trying, buddy!"></center>
+
+For those cases, our Golden Retriever now comes to the rescue! It saves Uppy’s memory (state) in browser cache with every move you make. This means that when Uppy suddenly crashes for whatever reason, our plugin will be able to retrieve this memory upon restart, and offer to resume where you left off. Sounds simple enough right? So why hasn't anybody attempted this before?
+
+As it turns out, it’s tricky. For one thing, no other competing file uploader uses tus, and resuming uploads without standardized and scrutinized components is really leaving you with more problems than you’re trying to solve in the first place. But with tus, we are standing on the shoulders of a giant and need not worry about the resumability aspect of the transmission.
+
+So then it becomes all about remembering what was going on with file selection and uploading right before the crash. One of the big issues here is that because of security reasons, Uppy is no longer allowed to access the selected files on your disk after a crash. Reasonable of course, but this meant that we had to deploy a number of workarounds that — while it may cause our inner purist some upset - combined, now amount to a pretty sweet user experience for the majority of cases. And in the end, that is what Uppy is all about: pleasing and delighting its users.
+
+<a name="how"></a>
+
+## 👻 How it works
+
+If you really want to know...
+
+Because we cannot access the files that we were uploading from disk, we cache them inside the browser.
+
+It all started with [a prototype](https://github.com/transloadit/uppy/issues/237) by [Richard Willars](https://github.com/richardwillars), which used a Service Worker to store files and states. Service Workers are great for when you close a tab, but when the browser dies, so does the Service Worker (in most cases). Also: iOS does not support it yet. So, we looked at Local Storage, which is almost universally available and _can_ survive a browser crash, but can't be used to store blobs. We also considered IndexedDB, which _can_ store blobs, but is less available and has severe limits on how much you can or should store in it.
+
+Since all of these technologies came with specific drawbacks, which one should we pick?
+
+Why, all of them, of course! By combining the three, they cover each other’s disadvantages with their own advantages. Here's what goes where: 
+
+- Local Storage stores all files state, without blobs (the actual data of the file), and restores this meta information on boot.
+- Service Worker stores references to all file blobs in memory. This should persist when navigating away from a page or closing the browser tab, but will likely get destroyed after a browser crash / quit.
+- IndexedDB stores all files that can reasonably be stored there, up to 10 MB per file and 300 MB in total (we are still debating reasonable limits). This persists until either the browser or Uppy decides to do a cleanup.
+
+Now when Uppy starts, we restore all meta information from Local Storage to get an idea of what was going on. For the blobs, we try to recover data from both the Service Worker and IndexedDB. This goes a long way into supporting many disastrous scenarios out there. 
+
+In some cases (very large files or a complete browser crash), we won’t be able to recover the file, but we do have valuable information about it, such as the name and a preview.
+
+Our current idea is that we could present the user with “ghost files” for these edge cases, and ask them to re-add such files. Here’s an early mockup, but we would love more feedback on this:
+
+<img src="/images/blog/golden-retriever/desktop-ghost.png" alt="Design mockup with ghosts" title="Design mockup with ghosts">
+
+For the remaining cases, if an upload was already in progress before the crash/refresh, and especially if it was resumable (via [tus](https://tus.io), for example), Golden Retriever just picks up from where it all went south. Our Golden Retriever will also clean up after herself: when files are successfully uploaded, or you decide to delete them, they will be removed from all “permanent” storages.
+
+<a name="try"></a>
+
+## 🚦 Give it a try in alpha
+
+Golden Retriever already works — tail awagging — and feels like magic :sparkles:, but it is also unstable, and hasn’t been tested on all the different devices yet. We encourage you to try it out though:
+
+```sh
+git clone https://github.com/transloadit/uppy.git
+git checkout feature/restore-files
+npm install
+npm run dev
+```
+
+A new browser tab with Uppy + Golden Retriever should open in a moment after the last command from above. The app entry point is in `examples/bundled-example/main.js`, it rebuilds on change. Enjoy! And please give your feedback in the [#268](https://github.com/transloadit/uppy/pull/268) PR 🎉
+
+The Uppy Team

+ 1 - 1
website/src/api-usage-example.ejs

@@ -6,7 +6,7 @@ import MetaData from 'uppy/lib/plugins/MetaData'
 const uppy = Uppy({autoProceed: false})
 const uppy = Uppy({autoProceed: false})
   .use(Dashboard, {trigger: '#select-files', target: '#upload-form', replaceTargetContent: true})
   .use(Dashboard, {trigger: '#select-files', target: '#upload-form', replaceTargetContent: true})
   .use(Tus10, {endpoint: '://master.tus.io/files/'})
   .use(Tus10, {endpoint: '://master.tus.io/files/'})
-  uppy.use(MetaData, {
+  .use(MetaData, {
     fields: [
     fields: [
       { id: 'resizeTo', name: 'Resize to', value: 1200, placeholder: 'specify future image size' },
       { id: 'resizeTo', name: 'Resize to', value: 1200, placeholder: 'specify future image size' },
       { id: 'description', name: 'Description', value: 'none', placeholder: 'describe what the file is for' }
       { id: 'description', name: 'Description', value: 'none', placeholder: 'describe what the file is for' }

+ 1 - 2
website/src/examples/bundle/index.ejs

@@ -6,8 +6,7 @@ order: 1
 ---
 ---
 
 
 {% blockquote %}
 {% blockquote %}
-This example showcases sourcing a pre-built bundle, that a browser can request
-straight from a CDN.
+This example showcases sourcing a pre-built bundle, that a browser can request straight from a CDN.
 {% endblockquote %}
 {% endblockquote %}
 
 
 <% include app.html %>
 <% include app.html %>

+ 1 - 1
website/src/examples/dashboard/app.es6

@@ -37,7 +37,7 @@ function uppyInit () {
     trigger: '.UppyModalOpenerBtn',
     trigger: '.UppyModalOpenerBtn',
     inline: opts.DashboardInline,
     inline: opts.DashboardInline,
     target: opts.DashboardInline ? '.DashboardContainer' : 'body',
     target: opts.DashboardInline ? '.DashboardContainer' : 'body',
-    note: opts.restrictions ? 'Images and video only, 300kb or less' : ''
+    note: opts.restrictions ? 'Images and video only, 2–3 files, up to 1 MB' : ''
   })
   })
 
 
   if (opts.GoogleDrive) {
   if (opts.GoogleDrive) {

+ 0 - 2
website/src/examples/drive/index.ejs

@@ -1,8 +1,6 @@
 ---
 ---
 title: Google Drive
 title: Google Drive
 layout: example
 layout: example
-type: examples
-order: 2
 ---
 ---
 
 
 {% blockquote %}
 {% blockquote %}

+ 2 - 3
website/src/examples/i18n/app.html

@@ -5,7 +5,6 @@
 
 
 <!-- Load Uppy pre-built bundled version and Russian language pack -->
 <!-- Load Uppy pre-built bundled version and Russian language pack -->
 <script src="/uppy/uppy.min.js"></script>
 <script src="/uppy/uppy.min.js"></script>
-<!--script src="/uppy/locales/ru_RU.js"></script-->
 <script>
 <script>
   var uppy = new Uppy.Core({debug: true});
   var uppy = new Uppy.Core({debug: true});
   uppy.use(Uppy.DragDrop, {
   uppy.use(Uppy.DragDrop, {
@@ -13,11 +12,11 @@
     locale: {
     locale: {
       strings: {
       strings: {
         chooseFile: 'Выберите файл',
         chooseFile: 'Выберите файл',
-        orDragDrop: 'или перенесите его сюда',
-        upload: 'Загрузить'
+        orDragDrop: 'или перенесите его сюда'      
       }
       }
     }
     }
   });
   });
+  uppy.use(Uppy.ProgressBar, {target: 'body', fixed: true})
   uppy.use(Uppy.Tus10, {endpoint: '//master.tus.io/files/'});
   uppy.use(Uppy.Tus10, {endpoint: '//master.tus.io/files/'});
   uppy.run();
   uppy.run();
 
 

+ 2 - 2
website/src/examples/multipart/index.ejs

@@ -1,12 +1,12 @@
 ---
 ---
-title: Multipart Uploads
+title: XHRUpload (Multipart)
 layout: example
 layout: example
 type: examples
 type: examples
 order: 3
 order: 3
 ---
 ---
 
 
 {% blockquote %}
 {% blockquote %}
-Uppy recommends using tus resumable file uploads, but if you want you can also use Multipart (Form) uploads
+Uppy recommends using tus resumable file uploads, but if you want you can also use XHRUpload (Multipart Form) uploads
 {% endblockquote %}
 {% endblockquote %}
 
 
 <p>
 <p>

二進制
website/src/images/blog/golden-retriever/catch-fail-1.gif


二進制
website/src/images/blog/golden-retriever/catch-fail-2.gif


二進制
website/src/images/blog/golden-retriever/desktop-ghost.png


二進制
website/src/images/blog/golden-retriever/desktop-restore.png


二進制
website/src/images/blog/golden-retriever/just-a-little-turbulance-folks.jpg


二進制
website/src/images/blog/golden-retriever/mobile.png


二進制
website/src/images/blog/golden-retriever/no-idea-dog-1.gif


二進制
website/src/images/blog/golden-retriever/no-idea-dog-2.gif


二進制
website/src/images/blog/golden-retriever/no-idea-dog-3.gif


二進制
website/src/images/blog/golden-retriever/no-idea-dog-4.jpg


二進制
website/src/images/blog/golden-retriever/no-idea-dog-5.jpg


二進制
website/src/images/blog/golden-retriever/no-idea-dog-6.jpg


二進制
website/src/images/blog/golden-retriever/puppy-rescue.gif


二進制
website/src/images/blog/golden-retriever/uppy-golden-retriever-crash-demo-2.mp4


二進制
website/src/images/blog/golden-retriever/uppy-golden-retriever-crash-demo.mp4


二進制
website/src/images/blog/golden-retriever/uppy-team-kong.jpg


二進制
website/src/images/blog/golden-retriever/uppy-team.jpg


+ 4 - 2
website/themes/uppy/layout/layout.ejs

@@ -1,6 +1,8 @@
 <% var isIndex = page.path === 'index.html' %>
 <% var isIndex = page.path === 'index.html' %>
 <% var title = page.title ? page.title + ' — ' + config.title : config.title %>
 <% var title = page.title ? page.title + ' — ' + config.title : config.title %>
 <% var excerpt = page.excerpt ? page.excerpt.replace(/(<([^>]+)>)/ig, '') : config.description %>
 <% var excerpt = page.excerpt ? page.excerpt.replace(/(<([^>]+)>)/ig, '') : config.description %>
+<% var image = page.image ? page.image : 'http://uppy.io/images/uppy-social.jpg' %>
+
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
   <head>
   <head>
@@ -16,8 +18,8 @@
     <meta property="og:title" content="<%- title %>">
     <meta property="og:title" content="<%- title %>">
     <meta name="twitter:title" content="<%- title %>">
     <meta name="twitter:title" content="<%- title %>">
     <meta name="twitter:description" content="<%- excerpt %>">
     <meta name="twitter:description" content="<%- excerpt %>">
-    <meta property="og:image" content="http://uppy.io/images/uppy-social.jpg">
-    <meta name="twitter:image" content="http://uppy.io/images/uppy-social.jpg">
+    <meta property="og:image" content="<%- image %>">
+    <meta name="twitter:image" content="<%- image %>">
 
 
     <link rel="icon" href="<%- config.logo_icon %>" type="image/x-icon">
     <link rel="icon" href="<%- config.logo_icon %>" type="image/x-icon">
     <link rel="alternate" type="application/rss+xml" title="<%= config.title %>" href="/atom.xml">
     <link rel="alternate" type="application/rss+xml" title="<%= config.title %>" href="/atom.xml">

+ 1 - 1
website/themes/uppy/source/css/_common.scss

@@ -49,7 +49,7 @@ code {
   // white-space: nowrap;
   // white-space: nowrap;
 }
 }
 
 
-em { color: $color-light; }
+// em { color: $color-light; }
 
 
 hr {
 hr {
   border: 0;
   border: 0;

+ 1 - 1
website/themes/uppy/source/css/_page.scss

@@ -135,7 +135,7 @@
   figure.wide {
   figure.wide {
     @media #{$screen-large} {
     @media #{$screen-large} {
       max-width: 135%;
       max-width: 135%;
-      img { max-width: 135%; }
+      img, video { max-width: 135%; }
     }
     }
   }
   }
 
 

Some files were not shown because too many files changed in this diff