Skip to content

Commit

Permalink
Merge branch '3.x' of https://github.com/transloadit/uppy into 3.x
Browse files Browse the repository at this point in the history
* '3.x' of https://github.com/transloadit/uppy:
  webcam: Add support for mobileNativeCamera option to Webcam and Dashboard (#3844)
  • Loading branch information
Murderlon committed Jul 27, 2022
2 parents 1586f99 + a7a4bd0 commit ad5b6bc
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 6 deletions.
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('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 (
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
takePictureBtn: 'Take Picture',
recordVideoBtn: '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: 2 additions & 0 deletions packages/@uppy/locales/src/en_US.js
Expand Up @@ -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.',
Expand Down Expand Up @@ -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',
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
2 changes: 1 addition & 1 deletion packages/@uppy/webcam/src/CameraScreen.jsx
Expand Up @@ -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 {
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.includes(mode)
}

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

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
5 changes: 5 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 Expand Up @@ -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',
},
}
```
Expand Down
9 changes: 9 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,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
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Expand Up @@ -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:^"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit ad5b6bc

Please sign in to comment.