mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-18 00:55:35 +00:00
Headless Hooks: Add useScreenCapture (#5784)
This commit is contained in:
parent
a83b66c866
commit
b0e69cf7ce
30 changed files with 1025 additions and 222 deletions
|
|
@ -11,6 +11,7 @@
|
|||
"@tailwindcss/vite": "^4.0.9",
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/react": "workspace:*",
|
||||
"@uppy/screen-capture": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
|
|
@ -2,156 +2,54 @@
|
|||
/* eslint-disable react/button-has-type */
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import React, { useState, useRef } from 'react'
|
||||
import {
|
||||
Dropzone,
|
||||
FilesGrid,
|
||||
FilesList,
|
||||
ProviderIcon,
|
||||
UploadButton,
|
||||
UppyContextProvider,
|
||||
useWebcam,
|
||||
useDropzone,
|
||||
useFileInput,
|
||||
} from '@uppy/react'
|
||||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
import UppyWebcam from '@uppy/webcam'
|
||||
import UppyScreenCapture from '@uppy/screen-capture'
|
||||
import Webcam from './Webcam.tsx'
|
||||
import ScreenCapture from './ScreenCapture.tsx'
|
||||
import CustomDropzone from './CustomDropzone.tsx'
|
||||
|
||||
import './app.css'
|
||||
import '@uppy/react/dist/styles.css'
|
||||
|
||||
interface WebcamProps {
|
||||
isOpen: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
function Webcam({ isOpen, close }: WebcamProps) {
|
||||
const {
|
||||
start,
|
||||
stop,
|
||||
getVideoProps,
|
||||
getSnapshotButtonProps,
|
||||
getRecordButtonProps,
|
||||
getStopRecordingButtonProps,
|
||||
getSubmitButtonProps,
|
||||
getDiscardButtonProps,
|
||||
} = useWebcam({ onSubmit: close })
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
start()
|
||||
}
|
||||
|
||||
return () => {
|
||||
stop()
|
||||
}
|
||||
}, [start, stop, isOpen])
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-lg w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold">Camera</h2>
|
||||
<button onClick={close} className="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
className="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
{...getVideoProps()}
|
||||
/>
|
||||
<div className="flex gap-4 mt-4">
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...getSnapshotButtonProps()}
|
||||
>
|
||||
Snapshot
|
||||
</button>
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...getRecordButtonProps()}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
{...getStopRecordingButtonProps()}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
className="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...getSubmitButtonProps()}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
{...getDiscardButtonProps()}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CustomDropzoneProps {
|
||||
openWebcamModal: () => void
|
||||
}
|
||||
|
||||
function CustomDropzone({ openWebcamModal }: CustomDropzoneProps) {
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
noClick: true,
|
||||
})
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input {...getInputProps()} className="hidden" />
|
||||
<div
|
||||
{...getRootProps()}
|
||||
role="button"
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<input {...getFileInputProps()} className="hidden" />
|
||||
<button
|
||||
{...getButtonProps()}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="device" fill="#1269cf" />
|
||||
</div>
|
||||
Device
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={openWebcamModal}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="camera" fill="#02B383" />
|
||||
</div>
|
||||
Webcam
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [uppy] = useState(() =>
|
||||
new Uppy()
|
||||
.use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
.use(UppyScreenCapture, {
|
||||
preferredVideoMimeType: 'video/webm',
|
||||
displayMediaConstraints: {
|
||||
video: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frameRate: {
|
||||
ideal: 3,
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
audio: false,
|
||||
},
|
||||
enableScreenshots: true,
|
||||
preferredImageMimeType: 'image/webp',
|
||||
})
|
||||
.use(UppyWebcam),
|
||||
)
|
||||
|
||||
const webcamDialogRef = useRef<HTMLDialogElement>(null)
|
||||
const screenCaptureDialogRef = useRef<HTMLDialogElement>(null)
|
||||
const [isWebcamOpen, setIsWebcamOpen] = useState(false)
|
||||
const [isScreenCaptureOpen, setIsScreenCaptureOpen] = useState(false)
|
||||
|
||||
function openWebcamModal() {
|
||||
setIsWebcamOpen(true)
|
||||
|
|
@ -163,6 +61,16 @@ function App() {
|
|||
webcamDialogRef.current?.close()
|
||||
}
|
||||
|
||||
function openScreenCaptureModal() {
|
||||
setIsScreenCaptureOpen(true)
|
||||
screenCaptureDialogRef.current?.showModal()
|
||||
}
|
||||
|
||||
function closeScreenCaptureModal() {
|
||||
setIsScreenCaptureOpen(false)
|
||||
screenCaptureDialogRef.current?.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<UppyContextProvider uppy={uppy}>
|
||||
<main className="p-5 max-w-xl mx-auto">
|
||||
|
|
@ -177,6 +85,16 @@ function App() {
|
|||
<Webcam isOpen={isWebcamOpen} close={() => closeWebcamModal()} />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
ref={screenCaptureDialogRef}
|
||||
className="backdrop:bg-gray-500/50 rounded-lg shadow-xl p-0 fixed inset-0 m-auto"
|
||||
>
|
||||
<ScreenCapture
|
||||
isOpen={isScreenCaptureOpen}
|
||||
close={() => closeScreenCaptureModal()}
|
||||
/>
|
||||
</dialog>
|
||||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
|
|
@ -191,7 +109,11 @@ function App() {
|
|||
|
||||
<article>
|
||||
<h2 className="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone openWebcamModal={() => openWebcamModal()} />
|
||||
<CustomDropzone
|
||||
openWebcamModal={() => openWebcamModal()}
|
||||
openScreenCaptureModal={() => openScreenCaptureModal()}
|
||||
/>
|
||||
<FilesList />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
|
|||
63
examples/react/src/CustomDropzone.tsx
Normal file
63
examples/react/src/CustomDropzone.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
/* eslint-disable react/button-has-type */
|
||||
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/react'
|
||||
|
||||
export interface CustomDropzoneProps {
|
||||
openWebcamModal: () => void
|
||||
openScreenCaptureModal: () => void
|
||||
}
|
||||
|
||||
export function CustomDropzone({
|
||||
openWebcamModal,
|
||||
openScreenCaptureModal,
|
||||
}: CustomDropzoneProps) {
|
||||
const { getRootProps, getInputProps } = useDropzone({ noClick: true })
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input {...getInputProps()} className="hidden" />
|
||||
<div
|
||||
{...getRootProps()}
|
||||
role="button"
|
||||
className="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<input {...getFileInputProps()} className="hidden" />
|
||||
<button
|
||||
{...getButtonProps()}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="device" fill="#1269cf" />
|
||||
</div>
|
||||
Device
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openWebcamModal}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="camera" fill="#02B383" />
|
||||
</div>
|
||||
Webcam
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={openScreenCaptureModal}
|
||||
className="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div className="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="screen-capture" fill="#FF5733" />
|
||||
</div>
|
||||
Screen Capture
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomDropzone
|
||||
88
examples/react/src/MediaCapture.tsx
Normal file
88
examples/react/src/MediaCapture.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading, react/button-has-type */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import React from 'react'
|
||||
|
||||
type ButtonProps = {
|
||||
type: 'button'
|
||||
onClick: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export interface MediaCaptureProps {
|
||||
title: string
|
||||
close: () => void
|
||||
getVideoProps: () => Record<string, unknown>
|
||||
getPrimaryActionButtonProps: () => ButtonProps
|
||||
primaryActionButtonLabel: string
|
||||
getRecordButtonProps: () => ButtonProps
|
||||
getStopRecordingButtonProps: () => ButtonProps
|
||||
getSubmitButtonProps: () => ButtonProps
|
||||
getDiscardButtonProps: () => ButtonProps
|
||||
}
|
||||
|
||||
export function MediaCapture({
|
||||
title,
|
||||
close,
|
||||
getVideoProps,
|
||||
getPrimaryActionButtonProps,
|
||||
primaryActionButtonLabel,
|
||||
getRecordButtonProps,
|
||||
getStopRecordingButtonProps,
|
||||
getSubmitButtonProps,
|
||||
getDiscardButtonProps,
|
||||
}: MediaCaptureProps) {
|
||||
return (
|
||||
<div className="p-4 max-w-lg w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={close}
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
className="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
{...getVideoProps()}
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<div className="flex gap-4 flex-wrap mt-4">
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...getPrimaryActionButtonProps()}
|
||||
>
|
||||
{primaryActionButtonLabel}
|
||||
</button>
|
||||
<button
|
||||
className="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...getRecordButtonProps()}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
className="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
{...getStopRecordingButtonProps()}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
className="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...getSubmitButtonProps()}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
{...getDiscardButtonProps()}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaCapture
|
||||
47
examples/react/src/ScreenCapture.tsx
Normal file
47
examples/react/src/ScreenCapture.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import React, { useEffect } from 'react'
|
||||
import { useScreenCapture } from '@uppy/react'
|
||||
import MediaCapture from './MediaCapture.tsx'
|
||||
|
||||
export interface ScreenCaptureProps {
|
||||
isOpen: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export function ScreenCapture({ isOpen, close }: ScreenCaptureProps) {
|
||||
const {
|
||||
start,
|
||||
stop,
|
||||
getVideoProps,
|
||||
getScreenshotButtonProps,
|
||||
getRecordButtonProps,
|
||||
getStopRecordingButtonProps,
|
||||
getSubmitButtonProps,
|
||||
getDiscardButtonProps,
|
||||
} = useScreenCapture({ onSubmit: close })
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
start()
|
||||
}
|
||||
return () => {
|
||||
stop()
|
||||
}
|
||||
}, [start, stop, isOpen])
|
||||
|
||||
return (
|
||||
<MediaCapture
|
||||
title="Screen Capture"
|
||||
close={close}
|
||||
getVideoProps={getVideoProps}
|
||||
getPrimaryActionButtonProps={getScreenshotButtonProps}
|
||||
primaryActionButtonLabel="Screenshot"
|
||||
getRecordButtonProps={getRecordButtonProps}
|
||||
getStopRecordingButtonProps={getStopRecordingButtonProps}
|
||||
getSubmitButtonProps={getSubmitButtonProps}
|
||||
getDiscardButtonProps={getDiscardButtonProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScreenCapture
|
||||
48
examples/react/src/Webcam.tsx
Normal file
48
examples/react/src/Webcam.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable react/react-in-jsx-scope */
|
||||
import React, { useEffect } from 'react'
|
||||
import { useWebcam } from '@uppy/react'
|
||||
import MediaCapture from './MediaCapture.tsx'
|
||||
|
||||
export interface WebcamProps {
|
||||
isOpen: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export function Webcam({ isOpen, close }: WebcamProps) {
|
||||
const {
|
||||
start,
|
||||
stop,
|
||||
getVideoProps,
|
||||
getSnapshotButtonProps,
|
||||
getRecordButtonProps,
|
||||
getStopRecordingButtonProps,
|
||||
getSubmitButtonProps,
|
||||
getDiscardButtonProps,
|
||||
} = useWebcam({ onSubmit: close })
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
start()
|
||||
}
|
||||
return () => {
|
||||
stop()
|
||||
}
|
||||
}, [start, stop, isOpen])
|
||||
|
||||
return (
|
||||
<MediaCapture
|
||||
title="Camera"
|
||||
close={close}
|
||||
getVideoProps={getVideoProps}
|
||||
getPrimaryActionButtonProps={getSnapshotButtonProps}
|
||||
primaryActionButtonLabel="Snapshot"
|
||||
getRecordButtonProps={getRecordButtonProps}
|
||||
getStopRecordingButtonProps={getStopRecordingButtonProps}
|
||||
getSubmitButtonProps={getSubmitButtonProps}
|
||||
getDiscardButtonProps={getDiscardButtonProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Webcam
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/drag-drop": "workspace:*",
|
||||
"@uppy/progress-bar": "workspace:*",
|
||||
"@uppy/screen-capture": "workspace:*",
|
||||
"@uppy/svelte": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*"
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
|
||||
interface Props {
|
||||
openWebcamModal: () => void
|
||||
openScreenCaptureModal: () => void
|
||||
}
|
||||
|
||||
const { openWebcamModal }: Props = $props()
|
||||
const { openWebcamModal, openScreenCaptureModal }: Props = $props()
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({ noClick: true })
|
||||
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
||||
|
|
@ -39,6 +40,16 @@
|
|||
</div>
|
||||
Webcam
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={openScreenCaptureModal}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div class="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="screen-capture" fill="#FF69B4" />
|
||||
</div>
|
||||
Screen Capture
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
73
examples/sveltekit/src/components/MediaCapture.svelte
Normal file
73
examples/sveltekit/src/components/MediaCapture.svelte
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
type ButtonProps = Record<string, unknown>
|
||||
type VideoProps = Record<string, unknown>
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
close: () => void
|
||||
videoProps: VideoProps
|
||||
primaryActionButtonProps: ButtonProps
|
||||
primaryActionButtonLabel: string
|
||||
recordButtonProps: ButtonProps
|
||||
stopRecordingButtonProps: ButtonProps
|
||||
submitButtonProps: ButtonProps
|
||||
discardButtonProps: ButtonProps
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
close,
|
||||
videoProps,
|
||||
primaryActionButtonProps,
|
||||
primaryActionButtonLabel,
|
||||
recordButtonProps,
|
||||
stopRecordingButtonProps,
|
||||
submitButtonProps,
|
||||
discardButtonProps,
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="p-4 max-w-lg w-full">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">{title}</h2>
|
||||
<button type="button" onclick={close} class="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
{...videoProps}
|
||||
></video>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...primaryActionButtonProps}
|
||||
>
|
||||
{primaryActionButtonLabel}
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...recordButtonProps}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
{...stopRecordingButtonProps}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...submitButtonProps}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
{...discardButtonProps}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
38
examples/sveltekit/src/components/ScreenCapture.svelte
Normal file
38
examples/sveltekit/src/components/ScreenCapture.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import MediaCapture from './MediaCapture.svelte'
|
||||
import { onDestroy, untrack } from 'svelte'
|
||||
import { useScreenCapture } from '@uppy/svelte'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const { isOpen, close }: Props = $props()
|
||||
const screenCaptureStore = useScreenCapture({ onSubmit: close })
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Use untrack to not trigger the effect again
|
||||
untrack(() => screenCaptureStore.start())
|
||||
} else {
|
||||
untrack(() => screenCaptureStore.stop())
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
untrack(() => screenCaptureStore.stop())
|
||||
})
|
||||
</script>
|
||||
|
||||
<MediaCapture
|
||||
title="Screen Capture"
|
||||
{close}
|
||||
videoProps={screenCaptureStore.getVideoProps()}
|
||||
primaryActionButtonProps={screenCaptureStore.getScreenshotButtonProps()}
|
||||
primaryActionButtonLabel="Screenshot"
|
||||
recordButtonProps={screenCaptureStore.getRecordButtonProps()}
|
||||
stopRecordingButtonProps={screenCaptureStore.getStopRecordingButtonProps()}
|
||||
submitButtonProps={screenCaptureStore.getSubmitButtonProps()}
|
||||
discardButtonProps={screenCaptureStore.getDiscardButtonProps()}
|
||||
/>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import MediaCapture from './MediaCapture.svelte'
|
||||
import { onDestroy, untrack } from 'svelte'
|
||||
import { useWebcam } from '@uppy/svelte'
|
||||
|
||||
|
|
@ -24,47 +25,14 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<div class="p-4 max-w-lg w-full">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Camera</h2>
|
||||
<button onclick={close} class="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
{...webcamStore.getVideoProps()}
|
||||
></video>
|
||||
<div class="flex gap-4 mt-4">
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...webcamStore.getSnapshotButtonProps()}
|
||||
>
|
||||
Snapshot
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...webcamStore.getRecordButtonProps()}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
{...webcamStore.getStopRecordingButtonProps()}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...webcamStore.getSubmitButtonProps()}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
{...webcamStore.getDiscardButtonProps()}
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<MediaCapture
|
||||
title="Camera"
|
||||
{close}
|
||||
videoProps={webcamStore.getVideoProps()}
|
||||
primaryActionButtonProps={webcamStore.getSnapshotButtonProps()}
|
||||
primaryActionButtonLabel="Snapshot"
|
||||
recordButtonProps={webcamStore.getRecordButtonProps()}
|
||||
stopRecordingButtonProps={webcamStore.getStopRecordingButtonProps()}
|
||||
submitButtonProps={webcamStore.getSubmitButtonProps()}
|
||||
discardButtonProps={webcamStore.getDiscardButtonProps()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import Uppy from '@uppy/core'
|
||||
import Tus from '@uppy/tus'
|
||||
import UppyWebcam from '@uppy/webcam'
|
||||
import UppyScreenCapture from '@uppy/screen-capture'
|
||||
import {
|
||||
UppyContextProvider,
|
||||
Dropzone,
|
||||
|
|
@ -13,16 +14,21 @@
|
|||
|
||||
import CustomDropzone from '../components/CustomDropzone.svelte'
|
||||
import Webcam from '../components/Webcam.svelte'
|
||||
import ScreenCapture from '../components/ScreenCapture.svelte'
|
||||
|
||||
const uppy = new Uppy()
|
||||
.use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
.use(UppyWebcam)
|
||||
.use(UppyScreenCapture)
|
||||
|
||||
let webcamDialogRef: HTMLDialogElement
|
||||
let isWebcamOpen = $state(false)
|
||||
|
||||
let screenCaptureDialogRef: HTMLDialogElement
|
||||
let isScreenCaptureOpen = $state(false)
|
||||
|
||||
function openWebcamModal() {
|
||||
isWebcamOpen = true
|
||||
webcamDialogRef?.showModal()
|
||||
|
|
@ -32,6 +38,16 @@
|
|||
isWebcamOpen = false
|
||||
webcamDialogRef?.close()
|
||||
}
|
||||
|
||||
function openScreenCaptureModal() {
|
||||
isScreenCaptureOpen = true
|
||||
screenCaptureDialogRef?.showModal()
|
||||
}
|
||||
|
||||
function closeScreenCaptureModal() {
|
||||
isScreenCaptureOpen = false
|
||||
screenCaptureDialogRef?.close()
|
||||
}
|
||||
</script>
|
||||
|
||||
<UppyContextProvider {uppy}>
|
||||
|
|
@ -47,6 +63,13 @@
|
|||
<Webcam isOpen={isWebcamOpen} close={closeWebcamModal} />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
bind:this={screenCaptureDialogRef}
|
||||
class="backdrop:bg-gray-500/50 rounded-lg shadow-xl p-0 fixed inset-0 m-auto"
|
||||
>
|
||||
<ScreenCapture isOpen={isScreenCaptureOpen} close={closeScreenCaptureModal} />
|
||||
</dialog>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
|
|
@ -61,7 +84,7 @@
|
|||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone {openWebcamModal} />
|
||||
<CustomDropzone {openWebcamModal} {openScreenCaptureModal} />
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/drag-drop": "workspace:*",
|
||||
"@uppy/progress-bar": "workspace:*",
|
||||
"@uppy/screen-capture": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/vue": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -12,21 +12,40 @@
|
|||
<Webcam :isOpen="isWebcamOpen" :close="closeWebcamModal" />
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
ref="screenCaptureDialogRef"
|
||||
class="backdrop:bg-gray-500/50 rounded-lg shadow-xl p-0 fixed inset-0 m-auto"
|
||||
>
|
||||
<ScreenCapture
|
||||
:isOpen="isScreenCaptureOpen"
|
||||
:close="closeScreenCaptureModal"
|
||||
/>
|
||||
</dialog>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With list</h2>
|
||||
<Dropzone />
|
||||
<Dropzone
|
||||
:openWebcamModal="openWebcamModal"
|
||||
:openScreenCaptureModal="openScreenCaptureModal"
|
||||
/>
|
||||
<FilesList />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With grid</h2>
|
||||
<Dropzone />
|
||||
<Dropzone
|
||||
:openWebcamModal="openWebcamModal"
|
||||
:openScreenCaptureModal="openScreenCaptureModal"
|
||||
/>
|
||||
<FilesGrid :columns="2" />
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h2 class="text-2xl my-4">With custom dropzone</h2>
|
||||
<CustomDropzone :openWebcamModal="openWebcamModal" />
|
||||
<CustomDropzone
|
||||
:openWebcamModal="openWebcamModal"
|
||||
:openScreenCaptureModal="openScreenCaptureModal"
|
||||
/>
|
||||
</article>
|
||||
</main>
|
||||
</UppyContextProvider>
|
||||
|
|
@ -45,10 +64,14 @@ import {
|
|||
} from '@uppy/vue'
|
||||
import CustomDropzone from './Dropzone.vue'
|
||||
import Webcam from './Webcam.vue'
|
||||
import ScreenCapture from './ScreenCapture.vue'
|
||||
import UppyWebcam from '@uppy/webcam'
|
||||
import UppyScreenCapture from '@uppy/screen-capture'
|
||||
|
||||
const webcamDialogRef = ref<HTMLDialogElement | null>(null)
|
||||
const isWebcamOpen = ref(false)
|
||||
const screenCaptureDialogRef = ref<HTMLDialogElement | null>(null)
|
||||
const isScreenCaptureOpen = ref(false)
|
||||
|
||||
function openWebcamModal() {
|
||||
isWebcamOpen.value = true
|
||||
|
|
@ -60,12 +83,23 @@ function closeWebcamModal() {
|
|||
webcamDialogRef.value?.close()
|
||||
}
|
||||
|
||||
function openScreenCaptureModal() {
|
||||
isScreenCaptureOpen.value = true
|
||||
screenCaptureDialogRef.value?.showModal()
|
||||
}
|
||||
|
||||
function closeScreenCaptureModal() {
|
||||
isScreenCaptureOpen.value = false
|
||||
screenCaptureDialogRef.value?.close()
|
||||
}
|
||||
|
||||
const uppy = computed(() =>
|
||||
new Uppy()
|
||||
.use(Tus, {
|
||||
endpoint: 'https://tusd.tusdemo.net/files/',
|
||||
})
|
||||
.use(UppyWebcam),
|
||||
.use(UppyWebcam)
|
||||
.use(UppyScreenCapture),
|
||||
)
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,16 @@
|
|||
</div>
|
||||
Webcam
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="props.openScreenCaptureModal"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
<div class="bg-white shadow-md rounded-md p-1">
|
||||
<ProviderIcon provider="screen-capture" fill="#FF69B4" />
|
||||
</div>
|
||||
Screen Capture
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -35,6 +45,7 @@ import { useDropzone, useFileInput, ProviderIcon } from '@uppy/vue'
|
|||
|
||||
const props = defineProps<{
|
||||
openWebcamModal: () => void
|
||||
openScreenCaptureModal: () => void
|
||||
}>()
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
|
|
|
|||
66
examples/vue3/src/MediaCapture.vue
Normal file
66
examples/vue3/src/MediaCapture.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="p-4 max-w-lg w-full">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">{{ title }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
@click="close"
|
||||
class="text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
v-bind="videoProps"
|
||||
>
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="primaryActionButtonProps"
|
||||
>
|
||||
{{ primaryActionButtonLabel }}
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="recordButtonProps"
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
v-bind="stopRecordingButtonProps"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
v-bind="submitButtonProps"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
v-bind="discardButtonProps"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
close: () => void
|
||||
videoProps: Record<string, unknown>
|
||||
primaryActionButtonProps: Record<string, unknown>
|
||||
primaryActionButtonLabel: string
|
||||
recordButtonProps: Record<string, unknown>
|
||||
stopRecordingButtonProps: Record<string, unknown>
|
||||
submitButtonProps: Record<string, unknown>
|
||||
discardButtonProps: Record<string, unknown>
|
||||
}>()
|
||||
</script>
|
||||
46
examples/vue3/src/ScreenCapture.vue
Normal file
46
examples/vue3/src/ScreenCapture.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<MediaCapture
|
||||
title="Screen Capture"
|
||||
:close="props.close"
|
||||
:video-props="screenCapture.getVideoProps()"
|
||||
:primary-action-button-props="screenCapture.getScreenshotButtonProps()"
|
||||
primary-action-button-label="Screenshot"
|
||||
:record-button-props="screenCapture.getRecordButtonProps()"
|
||||
:stop-recording-button-props="screenCapture.getStopRecordingButtonProps()"
|
||||
:submit-button-props="screenCapture.getSubmitButtonProps()"
|
||||
:discard-button-props="screenCapture.getDiscardButtonProps()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScreenCapture } from '@uppy/vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import MediaCapture from './MediaCapture.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
close: () => void
|
||||
}>()
|
||||
|
||||
const screenCapture = useScreenCapture({ onSubmit: props.close })
|
||||
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
screenCapture.value.start()
|
||||
} else {
|
||||
screenCapture.value.stop()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isOpen) {
|
||||
screenCapture.value.start()
|
||||
}
|
||||
return () => {
|
||||
screenCapture.value.stop()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -1,53 +1,21 @@
|
|||
<template>
|
||||
<div class="p-4 max-w-lg w-full">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Camera</h2>
|
||||
<button @click="props.close" class="text-gray-500 hover:text-gray-700">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<video
|
||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
v-bind="webcam.getVideoProps()"
|
||||
/>
|
||||
<div class="flex gap-4 mt-4">
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="webcam.getSnapshotButtonProps()"
|
||||
>
|
||||
Snapshot
|
||||
</button>
|
||||
<button
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="webcam.getRecordButtonProps()"
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
v-bind="webcam.getStopRecordingButtonProps()"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
v-bind="webcam.getSubmitButtonProps()"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
v-bind="webcam.getDiscardButtonProps()"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<MediaCapture
|
||||
title="Camera"
|
||||
:close="props.close"
|
||||
:video-props="webcam.getVideoProps()"
|
||||
:primary-action-button-props="webcam.getSnapshotButtonProps()"
|
||||
primary-action-button-label="Snapshot"
|
||||
:record-button-props="webcam.getRecordButtonProps()"
|
||||
:stop-recording-button-props="webcam.getStopRecordingButtonProps()"
|
||||
:submit-button-props="webcam.getSubmitButtonProps()"
|
||||
:discard-button-props="webcam.getDiscardButtonProps()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWebcam } from '@uppy/vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import MediaCapture from './MediaCapture.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean
|
||||
|
|
|
|||
224
packages/@uppy/components/src/hooks/screencapture.ts
Normal file
224
packages/@uppy/components/src/hooks/screencapture.ts
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
import type { Uppy, UppyEventMap } from '@uppy/core'
|
||||
import type { ScreenCaptureState } from '@uppy/screen-capture'
|
||||
import ScreenCapture from '@uppy/screen-capture'
|
||||
|
||||
type ButtonProps = {
|
||||
type: 'button'
|
||||
onClick: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
export type ScreenCaptureSnapshot = {
|
||||
state: ScreenCaptureState
|
||||
stop: () => void
|
||||
start: () => void
|
||||
getVideoProps: () => {
|
||||
playsInline: boolean
|
||||
autoPlay?: boolean
|
||||
muted: boolean
|
||||
controls?: boolean | undefined
|
||||
src?: string
|
||||
}
|
||||
getScreenshotButtonProps: () => ButtonProps
|
||||
getRecordButtonProps: () => ButtonProps
|
||||
getStopRecordingButtonProps: () => ButtonProps
|
||||
getSubmitButtonProps: () => ButtonProps
|
||||
getDiscardButtonProps: () => ButtonProps
|
||||
}
|
||||
|
||||
export type ScreenCaptureStore = {
|
||||
subscribe: (listener: () => void) => () => void
|
||||
getSnapshot: () => ScreenCaptureSnapshot
|
||||
}
|
||||
|
||||
const videoId = 'uppy-screencapture-video'
|
||||
|
||||
class Subscribers {
|
||||
private subscribers: Set<() => void> = new Set()
|
||||
|
||||
add = (listener: () => void): (() => void) => {
|
||||
this.subscribers.add(listener)
|
||||
return () => this.subscribers.delete(listener)
|
||||
}
|
||||
|
||||
emit = (): void => {
|
||||
for (const listener of this.subscribers) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
clear = (): void => {
|
||||
this.subscribers.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export function createScreenCaptureController(
|
||||
uppy: Uppy,
|
||||
onSubmit?: () => void,
|
||||
): ScreenCaptureStore {
|
||||
const plugin = uppy.getPlugin<ScreenCapture<any, any>>('ScreenCapture')
|
||||
if (!plugin) {
|
||||
throw new Error(
|
||||
'ScreenCapture plugin is not installed. Install @uppy/screen-capture and add it to the Uppy instance with `uppy.use(ScreenCapture)`.',
|
||||
)
|
||||
}
|
||||
const subscribers = new Subscribers()
|
||||
|
||||
const onStateUpdate: UppyEventMap<any, any>['state-update'] = (
|
||||
prev,
|
||||
next,
|
||||
patch,
|
||||
): void => {
|
||||
const screenCapturePatch = patch?.plugins?.ScreenCapture as
|
||||
| ScreenCaptureState
|
||||
| undefined
|
||||
|
||||
if (screenCapturePatch) {
|
||||
subscribers.emit()
|
||||
}
|
||||
}
|
||||
|
||||
const stop = () => {
|
||||
uppy.off('state-update', onStateUpdate)
|
||||
plugin.stop()
|
||||
}
|
||||
|
||||
const start = () => {
|
||||
uppy.on('state-update', onStateUpdate)
|
||||
plugin.start()
|
||||
}
|
||||
|
||||
const getVideoProps = () => {
|
||||
const ref = document.getElementById(videoId) as HTMLVideoElement | null
|
||||
const { status, recordedVideo, capturedScreenshotUrl } =
|
||||
plugin.getPluginState()
|
||||
if (status === 'captured' && recordedVideo) {
|
||||
if (ref) {
|
||||
ref.srcObject = null
|
||||
}
|
||||
return {
|
||||
id: videoId,
|
||||
playsInline: true,
|
||||
controls: true,
|
||||
muted: false,
|
||||
src: recordedVideo,
|
||||
autoPlay: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the status is 'captured', we use the capturedScreenshotUrl as the poster
|
||||
* for the video element. This allows us to display the screenshot as a static image
|
||||
* disabling controls and autoplay helps disguise the video element
|
||||
* as a result we're using the same video element for both the screenshot and the recorded video.
|
||||
*/
|
||||
if (status === 'captured' && capturedScreenshotUrl) {
|
||||
if (ref) {
|
||||
ref.srcObject = null
|
||||
}
|
||||
return {
|
||||
id: videoId,
|
||||
playsInline: true,
|
||||
controls: false,
|
||||
muted: false,
|
||||
poster: capturedScreenshotUrl,
|
||||
autoPlay: undefined,
|
||||
}
|
||||
}
|
||||
if (
|
||||
ref &&
|
||||
plugin.videoStream &&
|
||||
!(capturedScreenshotUrl || recordedVideo)
|
||||
) {
|
||||
ref.srcObject = plugin.videoStream
|
||||
}
|
||||
|
||||
return {
|
||||
id: videoId,
|
||||
playsInline: true,
|
||||
autoPlay: true,
|
||||
muted: true,
|
||||
controls: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const getScreenshotButtonProps = () => {
|
||||
const { status } = plugin.getPluginState()
|
||||
return {
|
||||
type: 'button' as const,
|
||||
onClick: async () => {
|
||||
await plugin.captureScreenshot()
|
||||
},
|
||||
disabled: status !== 'ready',
|
||||
}
|
||||
}
|
||||
|
||||
const getRecordButtonProps = () => {
|
||||
const { status } = plugin.getPluginState()
|
||||
return {
|
||||
type: 'button' as const,
|
||||
onClick: () => {
|
||||
plugin.startRecording()
|
||||
},
|
||||
disabled: status !== 'ready',
|
||||
}
|
||||
}
|
||||
|
||||
const getStopRecordingButtonProps = () => {
|
||||
const { status } = plugin.getPluginState()
|
||||
return {
|
||||
type: 'button' as const,
|
||||
onClick: () => {
|
||||
plugin.stopRecording()
|
||||
},
|
||||
disabled: status !== 'recording',
|
||||
}
|
||||
}
|
||||
|
||||
const getSubmitButtonProps = () => {
|
||||
const { status } = plugin.getPluginState()
|
||||
return {
|
||||
type: 'button' as const,
|
||||
onClick: () => {
|
||||
plugin.submit()
|
||||
plugin.stop()
|
||||
onSubmit?.()
|
||||
},
|
||||
disabled: !(status === 'captured'),
|
||||
}
|
||||
}
|
||||
|
||||
const getDiscardButtonProps = () => {
|
||||
const { status } = plugin.getPluginState()
|
||||
return {
|
||||
type: 'button' as const,
|
||||
onClick: () => {
|
||||
plugin.discardRecordedMedia()
|
||||
},
|
||||
disabled: !(status === 'captured'),
|
||||
}
|
||||
}
|
||||
|
||||
let cachedState = plugin.getPluginState()
|
||||
let snapshot: ScreenCaptureSnapshot = {
|
||||
state: cachedState,
|
||||
stop,
|
||||
start,
|
||||
getVideoProps,
|
||||
getScreenshotButtonProps,
|
||||
getRecordButtonProps,
|
||||
getStopRecordingButtonProps,
|
||||
getSubmitButtonProps,
|
||||
getDiscardButtonProps,
|
||||
}
|
||||
|
||||
const getSnapshot = () => {
|
||||
const nextState = plugin.getPluginState()
|
||||
if (nextState === cachedState) return snapshot
|
||||
cachedState = nextState
|
||||
snapshot = { ...snapshot, state: nextState }
|
||||
return snapshot
|
||||
}
|
||||
|
||||
return { subscribe: subscribers.add, getSnapshot }
|
||||
}
|
||||
|
|
@ -30,6 +30,12 @@ export {
|
|||
type WebcamSnapshot,
|
||||
} from './hooks/webcam.js'
|
||||
|
||||
export {
|
||||
createScreenCaptureController,
|
||||
type ScreenCaptureStore,
|
||||
type ScreenCaptureSnapshot,
|
||||
} from './hooks/screencapture.js'
|
||||
|
||||
// Types and utils
|
||||
export type {
|
||||
UppyContext,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export { default as useUppyEvent } from './useUppyEvent.js'
|
|||
export { useDropzone } from './useDropzone.js'
|
||||
export { useWebcam } from './useWebcam.js'
|
||||
export { useFileInput } from './useFileInput.js'
|
||||
export { useScreenCapture } from './useScreenCapture.js'
|
||||
|
||||
// Headless components
|
||||
export {
|
||||
|
|
|
|||
27
packages/@uppy/react/src/useScreenCapture.ts
Normal file
27
packages/@uppy/react/src/useScreenCapture.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { useMemo } from 'react'
|
||||
import { useSyncExternalStore } from 'use-sync-external-store/shim/index.js'
|
||||
import {
|
||||
createScreenCaptureController,
|
||||
type ScreenCaptureSnapshot,
|
||||
} from '@uppy/components'
|
||||
import { useUppyContext } from './headless/UppyContextProvider.js'
|
||||
|
||||
type ScreenCaptureProps = {
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export function useScreenCapture({
|
||||
onSubmit,
|
||||
}: ScreenCaptureProps): ScreenCaptureSnapshot {
|
||||
const { uppy } = useUppyContext()
|
||||
const controller = useMemo(
|
||||
() => createScreenCaptureController(uppy, onSubmit),
|
||||
[uppy, onSubmit],
|
||||
)
|
||||
const store = useSyncExternalStore(
|
||||
controller.subscribe,
|
||||
controller.getSnapshot,
|
||||
controller.getSnapshot,
|
||||
)
|
||||
return store
|
||||
}
|
||||
|
|
@ -32,6 +32,13 @@ function getMediaDevices() {
|
|||
const SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/webp'] as const
|
||||
type SupportedImageType = (typeof SUPPORTED_IMAGE_TYPES)[number]
|
||||
|
||||
export type ScreenCaptureStatus =
|
||||
| 'init'
|
||||
| 'ready'
|
||||
| 'recording'
|
||||
| 'captured'
|
||||
| 'error'
|
||||
|
||||
export interface ScreenCaptureOptions extends UIPluginOptions {
|
||||
displayMediaConstraints?: MediaStreamConstraints
|
||||
userMediaConstraints?: MediaStreamConstraints
|
||||
|
|
@ -74,6 +81,7 @@ export type ScreenCaptureState = {
|
|||
recordedVideo: string | null
|
||||
screenRecError: string | null
|
||||
capturedScreenshotUrl: string | null
|
||||
status: ScreenCaptureStatus
|
||||
}
|
||||
|
||||
export default class ScreenCapture<
|
||||
|
|
@ -143,6 +151,15 @@ export default class ScreenCapture<
|
|||
// initialize
|
||||
this.captureActive = false
|
||||
this.capturedMediaFile = null
|
||||
this.setPluginState({
|
||||
streamActive: false,
|
||||
audioStreamActive: false,
|
||||
recording: false,
|
||||
recordedVideo: null,
|
||||
screenRecError: null,
|
||||
capturedScreenshotUrl: null,
|
||||
status: 'init',
|
||||
})
|
||||
}
|
||||
|
||||
install(): null | undefined {
|
||||
|
|
@ -154,6 +171,7 @@ export default class ScreenCapture<
|
|||
this.setPluginState({
|
||||
streamActive: false,
|
||||
audioStreamActive: false,
|
||||
status: 'init',
|
||||
})
|
||||
|
||||
const { target } = this.opts
|
||||
|
|
@ -216,6 +234,8 @@ export default class ScreenCapture<
|
|||
|
||||
this.setPluginState({
|
||||
streamActive: true,
|
||||
status: 'ready',
|
||||
screenRecError: null,
|
||||
})
|
||||
|
||||
return videoStream
|
||||
|
|
@ -223,6 +243,7 @@ export default class ScreenCapture<
|
|||
.catch((err) => {
|
||||
this.setPluginState({
|
||||
screenRecError: err,
|
||||
status: 'error',
|
||||
})
|
||||
|
||||
this.userDenied = true
|
||||
|
|
@ -311,10 +332,12 @@ export default class ScreenCapture<
|
|||
// set plugin state to recording
|
||||
this.setPluginState({
|
||||
recording: true,
|
||||
status: 'recording',
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
this.uppy.log(err, 'error')
|
||||
this.setPluginState({ screenRecError: err.message, status: 'error' })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -330,6 +353,7 @@ export default class ScreenCapture<
|
|||
// @ts-expect-error we can't know Dashboard types here
|
||||
this.parent.hideAllPanels()
|
||||
}
|
||||
this.setPluginState({ status: 'init' })
|
||||
} else if (recording) {
|
||||
// stop recorder if it is active
|
||||
this.uppy.log('Capture stream inactive — stop recording')
|
||||
|
|
@ -371,6 +395,7 @@ export default class ScreenCapture<
|
|||
this.setPluginState({
|
||||
// eslint-disable-next-line compat/compat
|
||||
recordedVideo: URL.createObjectURL(file.data),
|
||||
status: 'captured',
|
||||
})
|
||||
})
|
||||
.then(
|
||||
|
|
@ -401,6 +426,7 @@ export default class ScreenCapture<
|
|||
this.setPluginState({
|
||||
recordedVideo: null,
|
||||
capturedScreenshotUrl: null,
|
||||
status: this.getPluginState().streamActive ? 'ready' : 'init',
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -468,6 +494,7 @@ export default class ScreenCapture<
|
|||
audioStreamActive: false,
|
||||
recordedVideo: null,
|
||||
capturedScreenshotUrl: null,
|
||||
status: 'init',
|
||||
})
|
||||
|
||||
this.captureActive = false
|
||||
|
|
@ -573,6 +600,7 @@ export default class ScreenCapture<
|
|||
const screenshotUrl = URL.createObjectURL(blob)
|
||||
this.setPluginState({
|
||||
capturedScreenshotUrl: screenshotUrl,
|
||||
status: 'captured',
|
||||
})
|
||||
resolve()
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -1,2 +1,6 @@
|
|||
export { default } from './ScreenCapture.jsx'
|
||||
export type { ScreenCaptureOptions } from './ScreenCapture.jsx'
|
||||
export type {
|
||||
ScreenCaptureOptions,
|
||||
ScreenCaptureState,
|
||||
} from './ScreenCapture.jsx'
|
||||
export { type ScreenCaptureStatus } from './ScreenCapture.jsx'
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ export * from './components/headless/generated/index.js'
|
|||
export * from './useDropzone.js'
|
||||
export * from './useFileInput.js'
|
||||
export * from './useWebcam.svelte.js'
|
||||
export * from './useScreenCapture.svelte.js'
|
||||
|
|
|
|||
73
packages/@uppy/svelte/src/lib/useScreenCapture.svelte.ts
Normal file
73
packages/@uppy/svelte/src/lib/useScreenCapture.svelte.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
createScreenCaptureController,
|
||||
type ScreenCaptureSnapshot,
|
||||
} from '@uppy/components'
|
||||
import { useExternalStore } from './useSyncExternalStore.svelte.js'
|
||||
import { getUppyContext } from './components/headless/uppyContext.js'
|
||||
|
||||
type ScreenCaptureProps = {
|
||||
onSubmit?: () => void
|
||||
}
|
||||
|
||||
type ButtonProps = {
|
||||
type: 'button'
|
||||
onclick: () => void
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
type SvelteScreenCaptureSnapshot = {
|
||||
state: ScreenCaptureSnapshot['state']
|
||||
stop: () => void
|
||||
start: () => void
|
||||
getVideoProps: () => ReturnType<ScreenCaptureSnapshot['getVideoProps']>
|
||||
getScreenshotButtonProps: () => ButtonProps
|
||||
getRecordButtonProps: () => ButtonProps
|
||||
getStopRecordingButtonProps: () => ButtonProps
|
||||
getSubmitButtonProps: () => ButtonProps
|
||||
getDiscardButtonProps: () => ButtonProps
|
||||
}
|
||||
|
||||
export function useScreenCapture(props?: ScreenCaptureProps): SvelteScreenCaptureSnapshot {
|
||||
const ctx = getUppyContext()
|
||||
|
||||
const controller = createScreenCaptureController(ctx.uppy, props?.onSubmit)
|
||||
const store = useExternalStore<ScreenCaptureSnapshot>(
|
||||
controller.getSnapshot,
|
||||
controller.subscribe,
|
||||
)
|
||||
|
||||
return {
|
||||
get state() {
|
||||
return store.value.state
|
||||
},
|
||||
get stop() {
|
||||
return store.value.stop
|
||||
},
|
||||
get start() {
|
||||
return store.value.start
|
||||
},
|
||||
get getVideoProps() {
|
||||
return store.value.getVideoProps
|
||||
},
|
||||
getScreenshotButtonProps: () => {
|
||||
const { onClick, ...rest } = store.value.getScreenshotButtonProps()
|
||||
return { ...rest, onclick: onClick }
|
||||
},
|
||||
getRecordButtonProps: () => {
|
||||
const { onClick, ...rest } = store.value.getRecordButtonProps()
|
||||
return { ...rest, onclick: onClick }
|
||||
},
|
||||
getStopRecordingButtonProps: () => {
|
||||
const { onClick, ...rest } = store.value.getStopRecordingButtonProps()
|
||||
return { ...rest, onclick: onClick }
|
||||
},
|
||||
getSubmitButtonProps: () => {
|
||||
const { onClick, ...rest } = store.value.getSubmitButtonProps()
|
||||
return { ...rest, onclick: onClick }
|
||||
},
|
||||
getDiscardButtonProps: () => {
|
||||
const { onClick, ...rest } = store.value.getDiscardButtonProps()
|
||||
return { ...rest, onclick: onClick }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -11,3 +11,4 @@ export * from './headless/generated/index.js'
|
|||
export * from './useDropzone.js'
|
||||
export * from './useFileInput.js'
|
||||
export * from './useWebcam.js'
|
||||
export * from './useScreenCapture.js'
|
||||
|
|
|
|||
25
packages/@uppy/vue/src/useScreenCapture.ts
Normal file
25
packages/@uppy/vue/src/useScreenCapture.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { type ShallowRef } from 'vue'
|
||||
import {
|
||||
createScreenCaptureController,
|
||||
type ScreenCaptureSnapshot,
|
||||
} from '@uppy/components'
|
||||
import { injectUppyContext } from './headless/context-provider.js'
|
||||
import { useExternalStore } from './useSyncExternalStore.js'
|
||||
|
||||
type ScreenCaptureProps = {
|
||||
onSubmit?: () => void
|
||||
}
|
||||
|
||||
export function useScreenCapture(
|
||||
props?: ScreenCaptureProps,
|
||||
): ShallowRef<ScreenCaptureSnapshot> {
|
||||
const ctx = injectUppyContext()
|
||||
|
||||
const controller = createScreenCaptureController(ctx.uppy, props?.onSubmit)
|
||||
const store = useExternalStore<ScreenCaptureSnapshot>(
|
||||
controller.getSnapshot,
|
||||
controller.subscribe,
|
||||
)
|
||||
|
||||
return store
|
||||
}
|
||||
|
|
@ -11644,6 +11644,7 @@ __metadata:
|
|||
"@types/react-dom": "npm:^19.0.4"
|
||||
"@uppy/core": "workspace:*"
|
||||
"@uppy/react": "workspace:*"
|
||||
"@uppy/screen-capture": "workspace:*"
|
||||
"@uppy/tus": "workspace:*"
|
||||
"@uppy/webcam": "workspace:*"
|
||||
"@vitejs/plugin-react": "npm:^4.3.4"
|
||||
|
|
@ -11682,6 +11683,7 @@ __metadata:
|
|||
"@uppy/dashboard": "workspace:*"
|
||||
"@uppy/drag-drop": "workspace:*"
|
||||
"@uppy/progress-bar": "workspace:*"
|
||||
"@uppy/screen-capture": "workspace:*"
|
||||
"@uppy/svelte": "workspace:*"
|
||||
"@uppy/tus": "workspace:*"
|
||||
"@uppy/webcam": "workspace:*"
|
||||
|
|
@ -11750,6 +11752,7 @@ __metadata:
|
|||
"@uppy/dashboard": "workspace:*"
|
||||
"@uppy/drag-drop": "workspace:*"
|
||||
"@uppy/progress-bar": "workspace:*"
|
||||
"@uppy/screen-capture": "workspace:*"
|
||||
"@uppy/tus": "workspace:*"
|
||||
"@uppy/vue": "workspace:*"
|
||||
"@uppy/webcam": "workspace:*"
|
||||
|
|
@ -12302,7 +12305,7 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@uppy/screen-capture@workspace:^, @uppy/screen-capture@workspace:packages/@uppy/screen-capture":
|
||||
"@uppy/screen-capture@workspace:*, @uppy/screen-capture@workspace:^, @uppy/screen-capture@workspace:packages/@uppy/screen-capture":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@uppy/screen-capture@workspace:packages/@uppy/screen-capture"
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue