From a7a4bd0ef0ef75bf4dadab51b3bef4f7e7e120bf Mon Sep 17 00:00:00 2001 From: Artur Paikin Date: Wed, 27 Jul 2022 10:52:19 +0100 Subject: [PATCH] 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 * Update packages/@uppy/dashboard/src/components/AddFiles.jsx Co-authored-by: Merlijn Vos * 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 * Use includes * Update locale * Add tablet support * Update website/src/docs/webcam.md Co-authored-by: Merlijn Vos Co-authored-by: Merlijn Vos Co-authored-by: Antoine du Hamel --- packages/@uppy/dashboard/src/Dashboard.jsx | 4 + .../dashboard/src/components/AddFiles.jsx | 106 +++++++++++++++++- packages/@uppy/dashboard/src/locale.js | 3 + packages/@uppy/dashboard/types/index.d.ts | 2 + packages/@uppy/locales/src/en_US.js | 2 + packages/@uppy/webcam/package.json | 1 + packages/@uppy/webcam/src/CameraScreen.jsx | 2 +- packages/@uppy/webcam/src/Webcam.jsx | 17 +++ packages/@uppy/webcam/types/index.d.ts | 1 + website/src/docs/dashboard.md | 5 + website/src/docs/webcam.md | 9 ++ yarn.lock | 8 ++ 12 files changed, 154 insertions(+), 6 deletions(-) diff --git a/packages/@uppy/dashboard/src/Dashboard.jsx b/packages/@uppy/dashboard/src/Dashboard.jsx index 981e64669e..eb897a29af 100644 --- a/packages/@uppy/dashboard/src/Dashboard.jsx +++ b/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, diff --git a/packages/@uppy/dashboard/src/components/AddFiles.jsx b/packages/@uppy/dashboard/src/components/AddFiles.jsx index 44a7160100..d46ca17b02 100644 --- a/packages/@uppy/dashboard/src/components/AddFiles.jsx +++ b/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 ( + + ) + } + renderMyDeviceAcquirer = () => { return (
{ + return ( +
+ +
+ ) + } + + renderVideoCamera = () => { + return ( +
+ +
+ ) + } + 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 ( -
- {!disableLocalFiles && this.renderMyDeviceAcquirer()} + {acquirersWithoutLastTwo.map((acquirer) => this.renderAcquirer(acquirer))} {lastTwoAcquirers.map((acquirer) => this.renderAcquirer(acquirer))} + + ) + } + + renderSourcesList = (acquirers, disableLocalFiles) => { + const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props + + return ( +
+ {!disableLocalFiles && this.renderMyDeviceAcquirer()} + {!disableLocalFiles && showNativePhotoCameraButton && this.renderPhotoCamera()} + {!disableLocalFiles && showNativeVideoCameraButton && this.renderVideoCamera()} + {acquirers.length > 0 && this.renderAcquirers(acquirers)}
) } @@ -183,12 +275,16 @@ class AddFiles extends Component { } render () { + const { showNativePhotoCameraButton, showNativeVideoCameraButton } = this.props + return (
{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)}
{this.props.note &&
{this.props.note}
} {this.props.proudlyDisplayPoweredByUppy && this.renderPoweredByUppy(this.props)} diff --git a/packages/@uppy/dashboard/src/locale.js b/packages/@uppy/dashboard/src/locale.js index 69e32ff5cc..5ccc1b7e5f 100644 --- a/packages/@uppy/dashboard/src/locale.js +++ b/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', }, } diff --git a/packages/@uppy/dashboard/types/index.d.ts b/packages/@uppy/dashboard/types/index.d.ts index e0e0fca32c..d168504490 100644 --- a/packages/@uppy/dashboard/types/index.d.ts +++ b/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 diff --git a/packages/@uppy/locales/src/en_US.js b/packages/@uppy/locales/src/en_US.js index 444a3e842c..35a14f5963 100644 --- a/packages/@uppy/locales/src/en_US.js +++ b/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', diff --git a/packages/@uppy/webcam/package.json b/packages/@uppy/webcam/package.json index fe9daf2778..859115b477 100644 --- a/packages/@uppy/webcam/package.json +++ b/packages/@uppy/webcam/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@uppy/utils": "workspace:^", + "is-mobile": "^3.1.1", "preact": "^10.5.13" }, "devDependencies": { diff --git a/packages/@uppy/webcam/src/CameraScreen.jsx b/packages/@uppy/webcam/src/CameraScreen.jsx index 4ff88576cf..479a67b9c8 100644 --- a/packages/@uppy/webcam/src/CameraScreen.jsx +++ b/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 { diff --git a/packages/@uppy/webcam/src/Webcam.jsx b/packages/@uppy/webcam/src/Webcam.jsx index bb7c65b20c..10fef4964a 100644 --- a/packages/@uppy/webcam/src/Webcam.jsx +++ b/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, diff --git a/packages/@uppy/webcam/types/index.d.ts b/packages/@uppy/webcam/types/index.d.ts index db81031151..ee3ee618e7 100644 --- a/packages/@uppy/webcam/types/index.d.ts +++ b/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 {} diff --git a/website/src/docs/dashboard.md b/website/src/docs/dashboard.md index 71c97ca4e5..eb91c30cda 100644 --- a/website/src/docs/dashboard.md +++ b/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', }, } ``` diff --git a/website/src/docs/webcam.md b/website/src/docs/webcam.md index 3652c15090..fe15844d2e 100644 --- a/website/src/docs/webcam.md +++ b/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 diff --git a/yarn.lock b/yarn.lock index fbb812fdea..02b3e78484 100644 --- a/yarn.lock +++ b/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"