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
2 changes: 2 additions & 0 deletions packages/@uppy/dashboard/src/Dashboard.jsx
Expand Up @@ -94,6 +94,7 @@ export default class Dashboard extends UIPlugin {
autoOpenFileEditor: false,
disabled: false,
disableLocalFiles: false,
mobileNativeCamera: false,
}

// merge default options with the ones set by user
Expand Down Expand Up @@ -967,6 +968,7 @@ export default class Dashboard extends UIPlugin {
maxNumberOfFiles: this.uppy.opts.restrictions.maxNumberOfFiles,
requiredMetaFields: this.uppy.opts.restrictions.requiredMetaFields,
showSelectedFiles: this.opts.showSelectedFiles,
mobileNativeCamera: this.opts.mobileNativeCamera,
handleCancelRestore: this.handleCancelRestore,
handleRequestThumbnail: this.handleRequestThumbnail,
handleCancelThumbnail: this.handleCancelThumbnail,
Expand Down
111 changes: 106 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.mobilePhotoFileInput.click()
}

triggerPhotoCameraInputClick = () => {
this.mobileVideoFileInput.click()
}

onFileInputChange = (event) => {
this.props.handleInputChange(event)

Expand Down Expand Up @@ -39,6 +47,35 @@ class AddFiles extends Component {
)
}

renderHiddenCameraInput = (type, refCallback) => {
let accept
switch (type) {
case 'photo':
accept = 'image/*'
break
case 'video':
accept = 'video/*'
break
default:
break
}
arturi marked this conversation as resolved.
Show resolved Hide resolved

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 +103,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 +227,29 @@ 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, mobileNativeCamera) => {
return (
<div className="uppy-Dashboard-AddFiles-list" role="tablist">
{!disableLocalFiles && this.renderMyDeviceAcquirer()}
{!disableLocalFiles && mobileNativeCamera && this.renderPhotoCamera()}
{!disableLocalFiles && mobileNativeCamera && this.renderVideoCamera()}
{acquirers.length > 0 && this.renderAcquirers(acquirers)}
</div>
)
}
Expand Down Expand Up @@ -187,8 +286,10 @@ class AddFiles extends Component {
<div className="uppy-Dashboard-AddFiles">
{this.renderHiddenInput(false, (ref) => { this.fileInput = ref })}
{this.renderHiddenInput(true, (ref) => { this.folderInput = ref })}
{this.renderHiddenCameraInput('photo', (ref) => { this.mobilePhotoFileInput = ref })}
{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.mobileNativeCamera)}
<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: 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
9 changes: 9 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 @@ -96,6 +97,7 @@ export default class Webcam extends UIPlugin {
preferredImageMimeType: null,
preferredVideoMimeType: null,
showRecordingLength: false,
mobileNativeCamera: false,
}

this.opts = { ...defaultOptions, ...opts }
Expand Down Expand Up @@ -588,6 +590,13 @@ export default class Webcam extends UIPlugin {
}

install () {
if (this.opts.mobileNativeCamera && isMobile()) {
arturi marked this conversation as resolved.
Show resolved Hide resolved
this.uppy.getPlugin('Dashboard').setOptions({
mobileNativeCamera: true,
})
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?: string
}

declare class Webcam extends UIPlugin<WebcamOptions> {}
Expand Down
5 changes: 5 additions & 0 deletions website/src/docs/webcam.md
Expand Up @@ -72,6 +72,7 @@ uppy.use(Webcam, {
showRecordingLength: false,
preferredVideoMimeType: null,
preferredImageMimeType: null,
mobileNativeCamera: false,
locale: {},
})
```
Expand Down Expand Up @@ -156,6 +157,10 @@ 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 the native device UI, accessed via two buttons: “Take Picture” and “Record Video”. Defaults to `false`.

### `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