Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 12 commits into from Jul 27, 2022
4 changes: 4 additions & 0 deletions packages/@uppy/dashboard/src/Dashboard.jsx
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
106 changes: 101 additions & 5 deletions 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 = () => {
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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('takePicture')}</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('recordVideo')}</div>
</button>
</div>
)
}

renderBrowseButton = (text, onClickFn) => {
const numberOfAcquirers = this.props.acquirers.length
return (
Expand Down Expand Up @@ -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>
)
}
Expand Down Expand Up @@ -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)}
Expand Down
3 changes: 3 additions & 0 deletions packages/@uppy/dashboard/src/locale.js
Expand Up @@ -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
takePicture: 'Take Picture',
recordVideo: 'Record Video',
},
}
2 changes: 2 additions & 0 deletions packages/@uppy/dashboard/types/index.d.ts
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/@uppy/locales/src/nl_NL.js
Expand Up @@ -192,7 +192,7 @@ nl_NL.strings = {
'1': 'Je moet minstens %{smart_count} bestanden selecteren',
},
zoomIn: 'Zoom in',
zoomOut: 'Zoom uit'
zoomOut: 'Zoom uit',
}

if (typeof Uppy !== 'undefined') {
Expand Down
1 change: 1 addition & 0 deletions packages/@uppy/webcam/package.json
Expand Up @@ -28,6 +28,7 @@
},
"dependencies": {
"@uppy/utils": "workspace:^",
"is-mobile": "^3.1.1",
"preact": "^10.5.13"
},
"devDependencies": {
Expand Down
17 changes: 17 additions & 0 deletions packages/@uppy/webcam/src/Webcam.jsx
Expand Up @@ -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'
Expand Down Expand Up @@ -50,6 +51,11 @@ function getMediaDevices () {
// eslint-disable-next-line compat/compat
return navigator.mediaDevices
}

function isModeAvailable (modes, mode) {
return modes.indexOf(mode) !== -1
arturi marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Webcam
*/
Expand Down Expand Up @@ -96,6 +102,7 @@ export default class Webcam extends UIPlugin {
preferredImageMimeType: null,
preferredVideoMimeType: null,
showRecordingLength: false,
mobileNativeCamera: isMobile(),
}

this.opts = { ...defaultOptions, ...opts }
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/@uppy/webcam/types/index.d.ts
Expand Up @@ -21,6 +21,7 @@ export interface WebcamOptions extends PluginOptions {
showRecordingLength?: boolean
preferredImageMimeType?: string
preferredVideoMimeType?: string
mobileNativeCamera?: boolean
}

declare class Webcam extends UIPlugin<WebcamOptions> {}
Expand Down
2 changes: 2 additions & 0 deletions website/src/docs/dashboard.md
Expand Up @@ -97,6 +97,8 @@ uppy.use(Dashboard, {
onRequestCloseModal: () => this.closeModal(),
showSelectedFiles: true,
showRemoveButtonAfterComplete: false,
showNativePhotoCameraButton: false,
showNativeVideoCameraButton: false,
locale: defaultLocale,
browserBackButtonClose: false,
theme: 'light',
Expand Down
7 changes: 7 additions & 0 deletions website/src/docs/webcam.md
Expand Up @@ -72,6 +72,7 @@ uppy.use(Webcam, {
showRecordingLength: false,
preferredVideoMimeType: null,
preferredImageMimeType: null,
mobileNativeCamera: isMobile(),
locale: {},
})
```
Expand Down Expand Up @@ -156,6 +157,12 @@ 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 with “Take Picture” and / or “Record Video” buttons (depending on [`modes`](#modes) option) that open native device UI for pictures / video (`Function: boolean` || `boolean`, default: `isMobile()`).

Set to 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.
arturi marked this conversation as resolved.
Show resolved Hide resolved

### `locale: {}`

```js
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Expand Up @@ -10564,6 +10564,7 @@ __metadata:
dependencies:
"@jest/globals": ^27.4.2
"@uppy/utils": "workspace:^"
is-mobile: ^3.1.1
preact: ^10.5.13
peerDependencies:
"@uppy/core": "workspace:^"
Expand Down Expand Up @@ -24625,6 +24626,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"
Expand Down