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.mobileVideoFileInput.click()
}

triggerPhotoCameraInputClick = () => {
this.mobilePhotoFileInput.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: isMobile(),
}

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

install () {
if (this.opts.mobileNativeCamera) {
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
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 that open native device UI for pictures / video (`Function: boolean` || `boolean`, default: `isMobile()`).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really "and/or"? Can you disable one or the other?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently, but since we allow that for the webcam plugin, I thought we need to mimic that functionality here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added modes support, please take a look @Murderlon


Set to a boolean to forcefully enable / disable this feature, or a function that checks for mobile device and returns a boolean — by default we use [`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