Переглянути джерело

webcam: Add support for mobileNativeCamera option to Webcam and Dashboard (#3844)

* Add support for mobileNativeCamera option to Webcam and Dashboard

* update types

* missing comma

* Set mobileNativeCamera to isMobile() by default

* Update website/src/docs/webcam.md

Co-authored-by: Merlijn Vos <merlijn@soverin.net>

* Update packages/@uppy/dashboard/src/components/AddFiles.jsx

Co-authored-by: Merlijn Vos <merlijn@soverin.net>

* Respect modes like video-only and picture when rendering mobile camera btns

* Update packages/@uppy/webcam/src/Webcam.jsx

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>

* Use includes

* Update locale

* Add tablet support

* Update website/src/docs/webcam.md

Co-authored-by: Merlijn Vos <merlijn@soverin.net>

Co-authored-by: Merlijn Vos <merlijn@soverin.net>
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Artur Paikin 2 роки тому
батько
коміт
a7a4bd0ef0

+ 4 - 0
packages/@uppy/dashboard/src/Dashboard.jsx

@@ -90,6 +90,8 @@ export default class Dashboard extends UIPlugin {
       showSelectedFiles: true,
       showRemoveButtonAfterComplete: false,
       browserBackButtonClose: false,
+      showNativePhotoCameraButton: false,
+      showNativeVideoCameraButton: false,
       theme: 'light',
       autoOpenFileEditor: false,
       disabled: false,
@@ -967,6 +969,8 @@ export default class Dashboard extends UIPlugin {
       maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
       requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
       showSelectedFiles: this.opts.showSelectedFiles,
+      showNativePhotoCameraButton: this.opts.showNativePhotoCameraButton,
+      showNativeVideoCameraButton: this.opts.showNativeVideoCameraButton,
       handleCancelRestore: this.handleCancelRestore,
       handleRequestThumbnail: this.handleRequestThumbnail,
       handleCancelThumbnail: this.handleCancelThumbnail,

+ 101 - 5
packages/@uppy/dashboard/src/components/AddFiles.jsx

@@ -1,4 +1,4 @@
-import {  h, Component  } from 'preact'
+import { h, Component, Fragment } from 'preact'
 
 class AddFiles extends Component {
   triggerFileInputClick = () => {
@@ -9,6 +9,14 @@ class AddFiles extends Component {
     this.folderInput.click()
   }
 
+  triggerVideoCameraInputClick = () => {
+    this.mobileVideoFileInput.click()
+  }
+
+  triggerPhotoCameraInputClick = () => {
+    this.mobilePhotoFileInput.click()
+  }
+
   onFileInputChange = (event) => {
     this.props.handleInputChange(event)
 
@@ -39,6 +47,26 @@ class AddFiles extends Component {
     )
   }
 
+  renderHiddenCameraInput = (type, refCallback) => {
+    const typeToAccept = { photo: 'image/*', video: 'video/*' }
+    const accept = typeToAccept[type]
+
+    return (
+      <input
+        className="uppy-Dashboard-input"
+        hidden
+        aria-hidden="true"
+        tabIndex={-1}
+        type="file"
+        name={`camera-${type}`}
+        onChange={this.onFileInputChange}
+        capture="user"
+        accept={accept}
+        ref={refCallback}
+      />
+    )
+  }
+
   renderMyDeviceAcquirer = () => {
     return (
       <div
@@ -66,6 +94,58 @@ class AddFiles extends Component {
     )
   }
 
+  renderPhotoCamera = () => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id="MobilePhotoCamera"
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-uppy-super-focusable
+          onClick={this.triggerPhotoCameraInputClick}
+        >
+          <svg aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
+            <g fill="none" fillRule="evenodd">
+              <rect className="uppy-ProviderIconBg" fill="#03BFEF" width="32" height="32" rx="16" />
+              <path d="M22 11c1.133 0 2 .867 2 2v7.333c0 1.134-.867 2-2 2H10c-1.133 0-2-.866-2-2V13c0-1.133.867-2 2-2h2.333l1.134-1.733C13.6 9.133 13.8 9 14 9h4c.2 0 .4.133.533.267L19.667 11H22zm-6 1.533a3.764 3.764 0 0 0-3.8 3.8c0 2.129 1.672 3.801 3.8 3.801s3.8-1.672 3.8-3.8c0-2.13-1.672-3.801-3.8-3.801zm0 6.261c-1.395 0-2.46-1.066-2.46-2.46 0-1.395 1.065-2.461 2.46-2.461s2.46 1.066 2.46 2.46c0 1.395-1.065 2.461-2.46 2.461z" fill="#FFF" fillRule="nonzero" />
+            </g>
+          </svg>
+          <div className="uppy-DashboardTab-name">{this.props.i18n('takePictureBtn')}</div>
+        </button>
+      </div>
+    )
+  }
+
+  renderVideoCamera = () => {
+    return (
+      <div
+        className="uppy-DashboardTab"
+        role="presentation"
+        data-uppy-acquirer-id="MobileVideoCamera"
+      >
+        <button
+          type="button"
+          className="uppy-u-reset uppy-c-btn uppy-DashboardTab-btn"
+          role="tab"
+          tabIndex={0}
+          data-uppy-super-focusable
+          onClick={this.triggerVideoCameraInputClick}
+        >
+          <svg aria-hidden="true" width="32" height="32" viewBox="0 0 32 32">
+            <rect fill="#1abc9c" width="32" height="32" rx="16" />
+            <path fill="#FFF" fillRule="nonzero" d="m21.254 14.277 2.941-2.588c.797-.313 1.243.818 1.09 1.554-.01 2.094.02 4.189-.017 6.282-.126.915-1.145 1.08-1.58.34l-2.434-2.142c-.192.287-.504 1.305-.738.468-.104-1.293-.028-2.596-.05-3.894.047-.312.381.823.426 1.069.063-.384.206-.744.362-1.09zm-12.939-3.73c3.858.013 7.717-.025 11.574.02.912.129 1.492 1.237 1.351 2.217-.019 2.412.04 4.83-.03 7.239-.17 1.025-1.166 1.59-2.029 1.429-3.705-.012-7.41.025-11.114-.019-.913-.129-1.492-1.237-1.352-2.217.018-2.404-.036-4.813.029-7.214.136-.82.83-1.473 1.571-1.454z " />
+          </svg>
+          <div className="uppy-DashboardTab-name">{this.props.i18n('recordVideoBtn')}</div>
+        </button>
+      </div>
+    )
+  }
+
   renderBrowseButton = (text, onClickFn) => {
     const numberOfAcquirers = this.props.acquirers.length
     return (
@@ -138,19 +218,31 @@ class AddFiles extends Component {
     )
   }
 
-  renderAcquirers = (acquirers, disableLocalFiles) => {
+  renderAcquirers = (acquirers) => {
     // Group last two buttons, so we don’t end up with
     // just one button on a new line
     const acquirersWithoutLastTwo = [...acquirers]
     const lastTwoAcquirers = acquirersWithoutLastTwo.splice(acquirers.length - 2, acquirers.length)
 
     return (
-      <div className="uppy-Dashboard-AddFiles-list" role="tablist">
-        {!disableLocalFiles && this.renderMyDeviceAcquirer()}
+      <Fragment>
         {acquirersWithoutLastTwo.map((acquirer) => this.renderAcquirer(acquirer))}
         <span role="presentation" style={{ 'white-space': 'nowrap' }}>
           {lastTwoAcquirers.map((acquirer) => this.renderAcquirer(acquirer))}
         </span>
+      </Fragment>
+    )
+  }
+
+  renderSourcesList = (acquirers, disableLocalFiles) => {
+    const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props
+
+    return (
+      <div className="uppy-Dashboard-AddFiles-list" role="tablist">
+        {!disableLocalFiles && this.renderMyDeviceAcquirer()}
+        {!disableLocalFiles && showNativePhotoCameraButton && this.renderPhotoCamera()}
+        {!disableLocalFiles && showNativeVideoCameraButton && this.renderVideoCamera()}
+        {acquirers.length > 0 && this.renderAcquirers(acquirers)}
       </div>
     )
   }
@@ -183,12 +275,16 @@ class AddFiles extends Component {
   }
 
   render () {
+    const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props
+
     return (
       <div className="uppy-Dashboard-AddFiles">
         {this.renderHiddenInput(false, (ref) => { this.fileInput = ref })}
         {this.renderHiddenInput(true, (ref) => { this.folderInput = ref })}
+        {showNativePhotoCameraButton && this.renderHiddenCameraInput('photo', (ref) => { this.mobilePhotoFileInput = ref })}
+        {showNativeVideoCameraButton && this.renderHiddenCameraInput('video', (ref) => { this.mobileVideoFileInput = ref })}
         {this.renderDropPasteBrowseTagline()}
-        {this.props.acquirers.length > 0 && this.renderAcquirers(this.props.acquirers, this.props.disableLocalFiles)}
+        {this.renderSourcesList(this.props.acquirers, this.props.disableLocalFiles)}
         <div className="uppy-Dashboard-AddFiles-info">
           {this.props.note && <div className="uppy-Dashboard-note">{this.props.note}</div>}
           {this.props.proudlyDisplayPoweredByUppy && this.renderPoweredByUppy(this.props)}

+ 3 - 0
packages/@uppy/dashboard/src/locale.js

@@ -84,5 +84,8 @@ export default {
       0: 'Missing required meta field: %{fields}.',
       1: 'Missing required meta fields: %{fields}.',
     },
+    // Used for native device camera buttons on mobile
+    takePictureBtn: 'Take Picture',
+    recordVideoBtn: 'Record Video',
   },
 }

+ 2 - 0
packages/@uppy/dashboard/types/index.d.ts

@@ -48,6 +48,8 @@ export interface DashboardOptions extends Options {
   showProgressDetails?: boolean
   showSelectedFiles?: boolean
   showRemoveButtonAfterComplete?: boolean
+  showNativePhotoCameraButton?: boolean
+  showNativeVideoCameraButton?: boolean
   target?: PluginTarget
   theme?: 'auto' | 'dark' | 'light'
   trigger?: string

+ 2 - 0
packages/@uppy/locales/src/en_US.js

@@ -125,6 +125,7 @@ en_US.strings = {
   recording: 'Recording',
   recordingLength: 'Recording length %{recording_length}',
   recordingStoppedMaxSize: 'Recording stopped because the file size is about to exceed the limit',
+  recordVideoBtn: 'Record Video',
   recoveredAllFiles: 'We restored all files. You can now resume the upload.',
   recoveredXFiles: {
     '0': 'We could not fully recover 1 file. Please re-select it and resume the upload.',
@@ -161,6 +162,7 @@ en_US.strings = {
   streamPassive: 'Stream passive',
   submitRecordedFile: 'Submit recorded file',
   takePicture: 'Take a picture',
+  takePictureBtn: 'Take Picture',
   timedOut: 'Upload stalled for %{seconds} seconds, aborting.',
   upload: 'Upload',
   uploadComplete: 'Upload complete',

+ 1 - 0
packages/@uppy/webcam/package.json

@@ -28,6 +28,7 @@
   },
   "dependencies": {
     "@uppy/utils": "workspace:^",
+    "is-mobile": "^3.1.1",
     "preact": "^10.5.13"
   },
   "devDependencies": {

+ 1 - 1
packages/@uppy/webcam/src/CameraScreen.jsx

@@ -8,7 +8,7 @@ import SubmitButton from './SubmitButton.jsx'
 import DiscardButton from './DiscardButton.jsx'
 
 function isModeAvailable (modes, mode) {
-  return modes.indexOf(mode) !== -1
+  return modes.includes(mode)
 }
 
 class CameraScreen extends Component {

+ 17 - 0
packages/@uppy/webcam/src/Webcam.jsx

@@ -3,6 +3,7 @@ import { h } from 'preact'
 import { UIPlugin } from '@uppy/core'
 import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension'
 import mimeTypes from '@uppy/utils/lib/mimeTypes'
+import isMobile from 'is-mobile'
 import canvasToBlob from '@uppy/utils/lib/canvasToBlob'
 import supportsMediaRecorder from './supportsMediaRecorder.js'
 import CameraIcon from './CameraIcon.jsx'
@@ -50,6 +51,11 @@ function getMediaDevices () {
   // eslint-disable-next-line compat/compat
   return navigator.mediaDevices
 }
+
+function isModeAvailable (modes, mode) {
+  return modes.includes(mode)
+}
+
 /**
  * Webcam
  */
@@ -96,6 +102,7 @@ export default class Webcam extends UIPlugin {
       preferredImageMimeType: null,
       preferredVideoMimeType: null,
       showRecordingLength: false,
+      mobileNativeCamera: isMobile({ tablet: true }),
     }
 
     this.opts = { ...defaultOptions, ...opts }
@@ -588,6 +595,16 @@ export default class Webcam extends UIPlugin {
   }
 
   install () {
+    const { mobileNativeCamera, modes } = this.opts
+
+    if (mobileNativeCamera) {
+      this.uppy.getPlugin('Dashboard').setOptions({
+        showNativeVideoCameraButton: isModeAvailable(modes, 'video-only') || isModeAvailable(modes, 'video-audio'),
+        showNativePhotoCameraButton: isModeAvailable(modes, 'picture'),
+      })
+      return
+    }
+
     this.setPluginState({
       cameraReady: false,
       recordingLengthSeconds: 0,

+ 1 - 0
packages/@uppy/webcam/types/index.d.ts

@@ -21,6 +21,7 @@ export interface WebcamOptions extends PluginOptions {
     showRecordingLength?: boolean
     preferredImageMimeType?: string
     preferredVideoMimeType?: string
+    mobileNativeCamera?: boolean
 }
 
 declare class Webcam extends UIPlugin<WebcamOptions> {}

+ 5 - 0
website/src/docs/dashboard.md

@@ -97,6 +97,8 @@ uppy.use(Dashboard, {
   onRequestCloseModal: () => this.closeModal(),
   showSelectedFiles: true,
   showRemoveButtonAfterComplete: false,
+  showNativePhotoCameraButton: false,
+  showNativeVideoCameraButton: false,
   locale: defaultLocale,
   browserBackButtonClose: false,
   theme: 'light',
@@ -421,6 +423,9 @@ export default {
       0: 'Missing required meta field: %{fields}.',
       1: 'Missing required meta fields: %{fields}.',
     },
+    // Used for native device camera buttons on mobile
+    takePictureBtn: 'Take Picture',
+    recordVideoBtn: 'Record Video',
   },
 }
 ```

+ 9 - 0
website/src/docs/webcam.md

@@ -72,6 +72,7 @@ uppy.use(Webcam, {
   showRecordingLength: false,
   preferredVideoMimeType: null,
   preferredImageMimeType: null,
+  mobileNativeCamera: isMobile(),
   locale: {},
 })
 ```
@@ -156,6 +157,14 @@ Set the preferred mime type for images, for example `'image/png'`. If the browse
 
 If no preferred image mime type is given, the Webcam plugin will prefer types listed in the [`allowedFileTypes` restriction](/docs/uppy/#restrictions), if any.
 
+### `mobileNativeCamera`
+
+Replaces Uppy’s custom camera UI on mobile and tablet with the native device camera (`Function: boolean` || `boolean`, default: `isMobile()`).
+
+This will show the “Take Picture” and / or “Record Video” buttons, which ones show depends on the [`modes`](#modes) option.
+
+You can set a boolean to forcefully enable / disable this feature, or a function which returns a boolean. By default we use the [`is-mobile`](https://github.com/juliangruber/is-mobile) package.
+
 ### `locale: {}`
 
 ```js

+ 8 - 0
yarn.lock

@@ -10685,6 +10685,7 @@ __metadata:
   dependencies:
     "@jest/globals": ^27.4.2
     "@uppy/utils": "workspace:^"
+    is-mobile: ^3.1.1
     preact: ^10.5.13
   peerDependencies:
     "@uppy/core": "workspace:^"
@@ -23834,6 +23835,13 @@ hexo-filter-github-emojis@arturi/hexo-filter-github-emojis:
   languageName: node
   linkType: hard
 
+"is-mobile@npm:^3.1.1":
+  version: 3.1.1
+  resolution: "is-mobile@npm:3.1.1"
+  checksum: b7c549020ac4674520378623afc4976694ff686eb3761cfad12da936ba9c2d675687bdc3c82eadf5a25147ce51c682800679bf835e31de272f05c026cd2b2f14
+  languageName: node
+  linkType: hard
+
 "is-module@npm:^1.0.0":
   version: 1.0.0
   resolution: "is-module@npm:1.0.0"