ソースを参照

@uppy/screen-capture: migrate to TS (#4965)

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Merlijn Vos 1 年間 前
コミット
59740ae808

+ 1 - 0
packages/@uppy/screen-capture/.npmignore

@@ -0,0 +1 @@
+tsconfig.*

+ 25 - 3
packages/@uppy/screen-capture/src/RecordButton.jsx → packages/@uppy/screen-capture/src/RecordButton.tsx

@@ -1,9 +1,17 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { h } from 'preact'
 
+type $TSFixMe = any
+
 /**
  * Control screen capture recording. Will show record or stop button.
  */
-export default function RecordButton ({ recording, onStartRecording, onStopRecording, i18n }) {
+export default function RecordButton({
+  recording,
+  onStartRecording,
+  onStopRecording,
+  i18n,
+}: $TSFixMe) {
   if (recording) {
     return (
       <button
@@ -14,7 +22,14 @@ export default function RecordButton ({ recording, onStartRecording, onStopRecor
         onClick={onStopRecording}
         data-uppy-super-focusable
       >
-        <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="100" height="100" viewBox="0 0 100 100">
+        <svg
+          aria-hidden="true"
+          focusable="false"
+          className="uppy-c-icon"
+          width="100"
+          height="100"
+          viewBox="0 0 100 100"
+        >
           <rect x="15" y="15" width="70" height="70" />
         </svg>
       </button>
@@ -30,7 +45,14 @@ export default function RecordButton ({ recording, onStartRecording, onStopRecor
       onClick={onStartRecording}
       data-uppy-super-focusable
     >
-      <svg aria-hidden="true" focusable="false" className="uppy-c-icon" width="100" height="100" viewBox="0 0 100 100">
+      <svg
+        aria-hidden="true"
+        focusable="false"
+        className="uppy-c-icon"
+        width="100"
+        height="100"
+        viewBox="0 0 100 100"
+      >
         <circle cx="50" cy="50" r="40" />
       </svg>
     </button>

+ 0 - 58
packages/@uppy/screen-capture/src/RecorderScreen.jsx

@@ -1,58 +0,0 @@
-/* eslint-disable react/jsx-props-no-spreading */
-import { h, Component } from 'preact'
-import RecordButton from './RecordButton.jsx'
-import SubmitButton from './SubmitButton.jsx'
-import StopWatch from './StopWatch.jsx'
-import StreamStatus from './StreamStatus.jsx'
-
-class RecorderScreen extends Component {
-  componentWillUnmount () {
-    const { onStop } = this.props
-    onStop()
-  }
-
-  render () {
-    const { recording, stream: videoStream, recordedVideo } = this.props
-
-    const videoProps = {
-      playsinline: true,
-    }
-
-    // show stream
-    if (recording || (!recordedVideo && !recording)) {
-      videoProps.muted = true
-      videoProps.autoplay = true
-      videoProps.srcObject = videoStream
-    }
-
-    // show preview
-    if (recordedVideo && !recording) {
-      videoProps.muted = false
-      videoProps.controls = true
-      videoProps.src = recordedVideo
-
-      // reset srcObject in dom. If not resetted, stream sticks in element
-      if (this.videoElement) {
-        this.videoElement.srcObject = undefined
-      }
-    }
-
-    return (
-      <div className="uppy uppy-ScreenCapture-container">
-        <div className="uppy-ScreenCapture-videoContainer">
-          <StreamStatus {...this.props} />
-          {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
-          <video ref={videoElement => { this.videoElement = videoElement }} className="uppy-ScreenCapture-video" {...videoProps} />
-          <StopWatch {...this.props} />
-        </div>
-
-        <div className="uppy-ScreenCapture-buttonContainer">
-          <RecordButton {...this.props} />
-          <SubmitButton {...this.props} />
-        </div>
-      </div>
-    )
-  }
-}
-
-export default RecorderScreen

+ 87 - 0
packages/@uppy/screen-capture/src/RecorderScreen.tsx

@@ -0,0 +1,87 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import { h, Component, type ComponentChild } from 'preact'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import RecordButton from './RecordButton.tsx'
+import SubmitButton from './SubmitButton.tsx'
+import StopWatch from './StopWatch.tsx'
+import StreamStatus from './StreamStatus.tsx'
+
+import ScreenCapture, { type ScreenCaptureState } from './ScreenCapture.tsx'
+
+type RecorderScreenProps<M extends Meta, B extends Body> = {
+  onStartRecording: ScreenCapture<M, B>['startRecording']
+  onStopRecording: ScreenCapture<M, B>['stopRecording']
+  onStop: ScreenCapture<M, B>['stop']
+  onSubmit: ScreenCapture<M, B>['submit']
+  i18n: ScreenCapture<M, B>['i18n']
+  stream: ScreenCapture<M, B>['videoStream']
+} & ScreenCaptureState
+
+class RecorderScreen<M extends Meta, B extends Body> extends Component<
+  RecorderScreenProps<M, B>
+> {
+  videoElement: HTMLVideoElement | null
+
+  componentWillUnmount(): void {
+    const { onStop } = this.props
+    onStop()
+  }
+
+  render(): ComponentChild {
+    const { recording, stream: videoStream, recordedVideo } = this.props
+
+    const videoProps: {
+      muted?: boolean
+      autoplay?: boolean
+      playsinline?: boolean
+      controls?: boolean
+      src?: string
+      srcObject?: MediaStream | null
+    } = {
+      playsinline: true,
+    }
+
+    // show stream
+    if (recording || (!recordedVideo && !recording)) {
+      videoProps.muted = true
+      videoProps.autoplay = true
+      videoProps.srcObject = videoStream
+    }
+
+    // show preview
+    if (recordedVideo && !recording) {
+      videoProps.muted = false
+      videoProps.controls = true
+      videoProps.src = recordedVideo
+
+      // reset srcObject in dom. If not resetted, stream sticks in element
+      if (this.videoElement) {
+        this.videoElement.srcObject = null
+      }
+    }
+
+    return (
+      <div className="uppy uppy-ScreenCapture-container">
+        <div className="uppy-ScreenCapture-videoContainer">
+          <StreamStatus {...this.props} />
+          {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
+          <video
+            ref={(videoElement) => {
+              this.videoElement = videoElement
+            }}
+            className="uppy-ScreenCapture-video"
+            {...videoProps}
+          />
+          <StopWatch {...this.props} />
+        </div>
+
+        <div className="uppy-ScreenCapture-buttonContainer">
+          <RecordButton {...this.props} />
+          <SubmitButton {...this.props} />
+        </div>
+      </div>
+    )
+  }
+}
+
+export default RecorderScreen

+ 178 - 102
packages/@uppy/screen-capture/src/ScreenCapture.jsx → packages/@uppy/screen-capture/src/ScreenCapture.tsx

@@ -1,31 +1,103 @@
-import { h } from 'preact'
-import { UIPlugin } from '@uppy/core'
+import { h, type ComponentChild } from 'preact'
+import { UIPlugin, Uppy, type UIPluginOptions } from '@uppy/core'
 import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension'
-import ScreenRecIcon from './ScreenRecIcon.jsx'
-import RecorderScreen from './RecorderScreen.jsx'
+import type { DefinePluginOpts } from '@uppy/core/lib/BasePlugin.ts'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import ScreenRecIcon from './ScreenRecIcon.tsx'
+import RecorderScreen from './RecorderScreen.tsx'
 
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore We don't want TS to generate types for the package.json
 import packageJson from '../package.json'
-import locale from './locale.js'
+import locale from './locale.ts'
 
 // Check if screen capturing is supported.
 // mediaDevices is supprted on mobile Safari, getDisplayMedia is not
-function isScreenRecordingSupported () {
+function isScreenRecordingSupported() {
   return window.MediaRecorder && navigator.mediaDevices?.getDisplayMedia // eslint-disable-line compat/compat
 }
 
 // Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
-function getMediaDevices () {
+function getMediaDevices() {
   return window.MediaRecorder && navigator.mediaDevices // eslint-disable-line compat/compat
 }
 
-/**
- * Screen capture
- */
-export default class ScreenCapture extends UIPlugin {
+export interface ScreenCaptureOptions extends UIPluginOptions {
+  title?: string
+  displayMediaConstraints?: MediaStreamConstraints
+  userMediaConstraints?: MediaStreamConstraints
+  preferredVideoMimeType?: string
+}
+
+// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
+const defaultOptions = {
+  // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#Properties_of_shared_screen_tracks
+  displayMediaConstraints: {
+    video: {
+      width: 1280,
+      height: 720,
+      frameRate: {
+        ideal: 3,
+        max: 5,
+      },
+      cursor: 'motion',
+      displaySurface: 'monitor',
+    },
+  },
+  // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/audio
+  userMediaConstraints: {
+    audio: true,
+  },
+  preferredVideoMimeType: 'video/webm',
+}
+
+type Opts = DefinePluginOpts<ScreenCaptureOptions, keyof typeof defaultOptions>
+
+export type ScreenCaptureState = {
+  streamActive: boolean
+  audioStreamActive: boolean
+  recording: boolean
+  recordedVideo: string | null
+  screenRecError: string | null
+}
+
+export default class ScreenCapture<
+  M extends Meta,
+  B extends Body,
+> extends UIPlugin<Opts, M, B, ScreenCaptureState> {
   static VERSION = packageJson.version
 
-  constructor (uppy, opts) {
-    super(uppy, opts)
+  mediaDevices: MediaDevices
+
+  protocol: string
+
+  icon: ComponentChild
+
+  streamInterrupted: () => void
+
+  captureActive: boolean
+
+  capturedMediaFile: null | {
+    source: string
+    name: string
+    data: Blob
+    type: string
+  }
+
+  videoStream: null | MediaStream
+
+  audioStream: null | MediaStream
+
+  userDenied: boolean
+
+  recorder: null | MediaRecorder
+
+  outputStream: null | MediaStream
+
+  recordingChunks: Blob[] | null
+
+  constructor(uppy: Uppy<M, B>, opts?: ScreenCaptureOptions) {
+    super(uppy, { ...defaultOptions, ...opts })
     this.mediaDevices = getMediaDevices()
     // eslint-disable-next-line no-restricted-globals
     this.protocol = location.protocol === 'https:' ? 'https' : 'http'
@@ -36,32 +108,6 @@ export default class ScreenCapture extends UIPlugin {
 
     this.defaultLocale = locale
 
-    // set default options
-    // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
-    const defaultOptions = {
-      // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#Properties_of_shared_screen_tracks
-      displayMediaConstraints: {
-        video: {
-          width: 1280,
-          height: 720,
-          frameRate: {
-            ideal: 3,
-            max: 5,
-          },
-          cursor: 'motion',
-          displaySurface: 'monitor',
-        },
-      },
-      // https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints/audio
-      userMediaConstraints: {
-        audio: true,
-      },
-      preferredVideoMimeType: 'video/webm',
-    }
-
-    // merge default options with the ones set by user
-    this.opts = { ...defaultOptions, ...opts }
-
     // i18n
     this.i18nInit()
 
@@ -83,7 +129,7 @@ export default class ScreenCapture extends UIPlugin {
     this.capturedMediaFile = null
   }
 
-  install () {
+  install(): null | undefined {
     if (!isScreenRecordingSupported()) {
       this.uppy.log('Screen recorder access is not supported', 'warning')
       return null
@@ -102,7 +148,7 @@ export default class ScreenCapture extends UIPlugin {
     return undefined
   }
 
-  uninstall () {
+  uninstall(): void {
     if (this.videoStream) {
       this.stop()
     }
@@ -110,7 +156,7 @@ export default class ScreenCapture extends UIPlugin {
     this.unmount()
   }
 
-  start () {
+  start(): Promise<void> {
     if (!this.mediaDevices) {
       return Promise.reject(new Error('Screen recorder access not supported'))
     }
@@ -119,29 +165,31 @@ export default class ScreenCapture extends UIPlugin {
 
     this.selectAudioStreamSource()
 
-    return this.selectVideoStreamSource()
-      .then(res => {
-        // something happened in start -> return
-        if (res === false) {
-          // Close the Dashboard panel if plugin is installed
-          // into Dashboard (could be other parent UI plugin)
-          if (this.parent && this.parent.hideAllPanels) {
-            this.parent.hideAllPanels()
-            this.captureActive = false
-          }
+    return this.selectVideoStreamSource().then((res) => {
+      // something happened in start -> return
+      if (res === false) {
+        // Close the Dashboard panel if plugin is installed
+        // into Dashboard (could be other parent UI plugin)
+        // @ts-expect-error we can't know Dashboard types here
+        if (this.parent && this.parent.hideAllPanels) {
+          // @ts-expect-error we can't know Dashboard types here
+          this.parent.hideAllPanels()
+          this.captureActive = false
         }
-      })
+      }
+    })
   }
 
-  selectVideoStreamSource () {
+  selectVideoStreamSource(): Promise<MediaStream | false> {
     // if active stream available, return it
     if (this.videoStream) {
-      return new Promise(resolve => resolve(this.videoStream))
+      return new Promise((resolve) => resolve(this.videoStream!))
     }
 
     // ask user to select source to record and get mediastream from that
     // eslint-disable-next-line compat/compat
-    return this.mediaDevices.getDisplayMedia(this.opts.displayMediaConstraints)
+    return this.mediaDevices
+      .getDisplayMedia(this.opts.displayMediaConstraints)
       .then((videoStream) => {
         this.videoStream = videoStream
 
@@ -171,15 +219,16 @@ export default class ScreenCapture extends UIPlugin {
       })
   }
 
-  selectAudioStreamSource () {
+  selectAudioStreamSource(): Promise<MediaStream | false> {
     // if active stream available, return it
     if (this.audioStream) {
-      return new Promise(resolve => resolve(this.audioStream))
+      return new Promise((resolve) => resolve(this.audioStream!))
     }
 
     // ask user to select source to record and get mediastream from that
     // eslint-disable-next-line compat/compat
-    return this.mediaDevices.getUserMedia(this.opts.userMediaConstraints)
+    return this.mediaDevices
+      .getUserMedia(this.opts.userMediaConstraints)
       .then((audioStream) => {
         this.audioStream = audioStream
 
@@ -198,19 +247,24 @@ export default class ScreenCapture extends UIPlugin {
       })
   }
 
-  startRecording () {
-    const options = {}
+  startRecording(): void {
+    const options: { mimeType?: string } = {}
     this.capturedMediaFile = null
     this.recordingChunks = []
     const { preferredVideoMimeType } = this.opts
 
     this.selectVideoStreamSource()
       .then((videoStream) => {
+        if (videoStream === false) {
+          throw new Error('No video stream available')
+        }
         // Attempt to use the passed preferredVideoMimeType (if any) during recording.
         // If the browser doesn't support it, we'll fall back to the browser default instead
-        if (preferredVideoMimeType
-            && MediaRecorder.isTypeSupported(preferredVideoMimeType)
-            && getFileTypeExtension(preferredVideoMimeType)) {
+        if (
+          preferredVideoMimeType &&
+          MediaRecorder.isTypeSupported(preferredVideoMimeType) &&
+          getFileTypeExtension(preferredVideoMimeType)
+        ) {
           options.mimeType = preferredVideoMimeType
         }
 
@@ -232,7 +286,7 @@ export default class ScreenCapture extends UIPlugin {
 
         // push data to buffer when data available
         this.recorder.addEventListener('dataavailable', (event) => {
-          this.recordingChunks.push(event.data)
+          this.recordingChunks!.push(event.data)
         })
 
         // start recording
@@ -248,14 +302,16 @@ export default class ScreenCapture extends UIPlugin {
       })
   }
 
-  streamInactivated () {
+  streamInactivated(): void {
     // get screen recorder state
     const { recordedVideo, recording } = { ...this.getPluginState() }
 
     if (!recordedVideo && !recording) {
       // Close the Dashboard panel if plugin is installed
       // into Dashboard (could be other parent UI plugin)
+      // @ts-expect-error we can't know Dashboard types here
       if (this.parent && this.parent.hideAllPanels) {
+        // @ts-expect-error we can't know Dashboard types here
         this.parent.hideAllPanels()
       }
     } else if (recording) {
@@ -268,46 +324,53 @@ export default class ScreenCapture extends UIPlugin {
     this.audioStream = null
 
     this.setPluginState({
-      streamActive: false, audioStreamActive: false,
+      streamActive: false,
+      audioStreamActive: false,
     })
   }
 
-  stopRecording () {
-    const stopped = new Promise((resolve) => {
-      this.recorder.addEventListener('stop', () => {
+  stopRecording(): Promise<void> {
+    const stopped = new Promise<void>((resolve) => {
+      this.recorder!.addEventListener('stop', () => {
         resolve()
       })
 
-      this.recorder.stop()
+      this.recorder!.stop()
     })
 
-    return stopped.then(() => {
-      // recording stopped
-      this.setPluginState({
-        recording: false,
+    return stopped
+      .then(() => {
+        // recording stopped
+        this.setPluginState({
+          recording: false,
+        })
+        // get video file after recorder stopped
+        return this.getVideo()
       })
-      // get video file after recorder stopped
-      return this.getVideo()
-    }).then((file) => {
-      // store media file
-      this.capturedMediaFile = file
-
-      // create object url for capture result preview
-      this.setPluginState({
-        // eslint-disable-next-line compat/compat
-        recordedVideo: URL.createObjectURL(file.data),
+      .then((file) => {
+        // store media file
+        this.capturedMediaFile = file
+
+        // create object url for capture result preview
+        this.setPluginState({
+          // eslint-disable-next-line compat/compat
+          recordedVideo: URL.createObjectURL(file.data),
+        })
       })
-    }).then(() => {
-      this.recordingChunks = null
-      this.recorder = null
-    }, (error) => {
-      this.recordingChunks = null
-      this.recorder = null
-      throw error
-    })
+      .then(
+        () => {
+          this.recordingChunks = null
+          this.recorder = null
+        },
+        (error) => {
+          this.recordingChunks = null
+          this.recorder = null
+          throw error
+        },
+      )
   }
 
-  submit () {
+  submit(): void {
     try {
       // add recorded file to uppy
       if (this.capturedMediaFile) {
@@ -321,7 +384,7 @@ export default class ScreenCapture extends UIPlugin {
     }
   }
 
-  stop () {
+  stop(): void {
     // flush video stream
     if (this.videoStream) {
       this.videoStream.getVideoTracks().forEach((track) => {
@@ -363,16 +426,25 @@ export default class ScreenCapture extends UIPlugin {
     this.captureActive = false
   }
 
-  getVideo () {
-    const mimeType = this.recordingChunks[0].type
+  getVideo(): Promise<{
+    source: string
+    name: string
+    data: Blob
+    type: string
+  }> {
+    const mimeType = this.recordingChunks![0].type
     const fileExtension = getFileTypeExtension(mimeType)
 
     if (!fileExtension) {
-      return Promise.reject(new Error(`Could not retrieve recording: Unsupported media type "${mimeType}"`))
+      return Promise.reject(
+        new Error(
+          `Could not retrieve recording: Unsupported media type "${mimeType}"`,
+        ),
+      )
     }
 
     const name = `screencap-${Date.now()}.${fileExtension}`
-    const blob = new Blob(this.recordingChunks, { type: mimeType })
+    const blob = new Blob(this.recordingChunks!, { type: mimeType })
     const file = {
       source: this.id,
       name,
@@ -383,16 +455,20 @@ export default class ScreenCapture extends UIPlugin {
     return Promise.resolve(file)
   }
 
-  render () {
+  render(): ComponentChild {
     // get screen recorder state
     const recorderState = this.getPluginState()
 
-    if (!recorderState.streamActive && !this.captureActive && !this.userDenied) {
+    if (
+      !recorderState.streamActive &&
+      !this.captureActive &&
+      !this.userDenied
+    ) {
       this.start()
     }
 
     return (
-      <RecorderScreen
+      <RecorderScreen<M, B>
         {...recorderState} // eslint-disable-line react/jsx-props-no-spreading
         onStartRecording={this.startRecording}
         onStopRecording={this.stopRecording}

+ 10 - 2
packages/@uppy/screen-capture/src/ScreenRecIcon.jsx → packages/@uppy/screen-capture/src/ScreenRecIcon.tsx

@@ -1,8 +1,16 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { h } from 'preact'
 
-export default () => {
+export default function ScreenRecIcon() {
   return (
-    <svg className="uppy-DashboardTab-iconScreenRec" aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
+    <svg
+      className="uppy-DashboardTab-iconScreenRec"
+      aria-hidden="true"
+      focusable="false"
+      width="32"
+      height="32"
+      viewBox="0 0 32 32"
+    >
       <g fill="currentcolor" fillRule="evenodd">
         <path d="M24.182 9H7.818C6.81 9 6 9.742 6 10.667v10c0 .916.81 1.666 1.818 1.666h4.546V24h7.272v-1.667h4.546c1 0 1.809-.75 1.809-1.666l.009-10C26 9.742 25.182 9 24.182 9zM24 21H8V11h16v10z" />
         <circle cx="16" cy="16" r="2" />

+ 0 - 107
packages/@uppy/screen-capture/src/StopWatch.jsx

@@ -1,107 +0,0 @@
-import { h, Component } from 'preact'
-
-class StopWatch extends Component {
-  constructor (props) {
-    super(props)
-    this.state = { elapsedTime: 0 }
-
-    this.wrapperStyle = {
-      width: '100%',
-      height: '100%',
-      display: 'flex',
-    }
-
-    this.overlayStyle = {
-      position: 'absolute',
-      width: '100%',
-      height: '100%',
-      background: 'black',
-      opacity: 0.7,
-    }
-
-    this.infoContainerStyle = {
-      marginLeft: 'auto',
-      marginRight: 'auto',
-      marginTop: 'auto',
-      marginBottom: 'auto',
-      zIndex: 1,
-      color: 'white',
-    }
-
-    this.infotextStyle = {
-      marginLeft: 'auto',
-      marginRight: 'auto',
-      marginBottom: '1rem',
-      fontSize: '1.5rem',
-    }
-
-    this.timeStyle = {
-      display: 'block',
-      fontWeight: 'bold',
-      marginLeft: 'auto',
-      marginRight: 'auto',
-      fontSize: '3rem',
-      fontFamily: 'Courier New',
-    }
-  }
-
-  startTimer () {
-    this.timerTick()
-    this.timerRunning = true
-  }
-
-  resetTimer () {
-    clearTimeout(this.timer)
-    this.setState({ elapsedTime: 0 })
-    this.timerRunning = false
-  }
-
-  timerTick () {
-    this.timer = setTimeout(() => {
-      this.setState(state => ({ elapsedTime: state.elapsedTime + 1 }))
-      this.timerTick()
-    }, 1000)
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  fmtMSS (s) {
-    // eslint-disable-next-line no-return-assign, no-param-reassign
-    return (s - (s %= 60)) / 60 + (s > 9 ? ':' : ':0') + s
-  }
-
-  render () {
-    const { recording, i18n } = { ...this.props }
-    const { elapsedTime } = this.state
-
-    // second to minutes and seconds
-    const minAndSec = this.fmtMSS(elapsedTime)
-
-    if (recording && !this.timerRunning) {
-      this.startTimer()
-    }
-
-    if (!recording && this.timerRunning) {
-      this.resetTimer()
-    }
-
-    if (recording) {
-      return (
-        <div style={this.wrapperStyle}>
-          <div style={this.overlayStyle} />
-          <div style={this.infoContainerStyle}>
-            <div style={this.infotextStyle}>
-              {i18n('recording')}
-            </div>
-            <div style={this.timeStyle}>
-              {minAndSec}
-            </div>
-          </div>
-
-        </div>
-      )
-    }
-    return null
-  }
-}
-
-export default StopWatch

+ 110 - 0
packages/@uppy/screen-capture/src/StopWatch.tsx

@@ -0,0 +1,110 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+import { h, Component } from 'preact'
+
+type $TSFixMe = any
+
+function fmtMSS(s: number) {
+  // eslint-disable-next-line no-return-assign, no-param-reassign
+  return (s - (s %= 60)) / 60 + (s > 9 ? ':' : ':0') + s
+}
+
+class StopWatch extends Component {
+  private wrapperStyle = {
+    width: '100%',
+    height: '100%',
+    display: 'flex',
+  } as const
+
+  private overlayStyle = {
+    position: 'absolute',
+    width: '100%',
+    height: '100%',
+    background: 'black',
+    opacity: 0.7,
+  } as const
+
+  private infoContainerStyle = {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+    marginTop: 'auto',
+    marginBottom: 'auto',
+    zIndex: 1,
+    color: 'white',
+  } as const
+
+  private infotextStyle = {
+    marginLeft: 'auto',
+    marginRight: 'auto',
+    marginBottom: '1rem',
+    fontSize: '1.5rem',
+  } as const
+
+  private timeStyle = {
+    display: 'block',
+    fontWeight: 'bold',
+    marginLeft: 'auto',
+    marginRight: 'auto',
+    fontSize: '3rem',
+    fontFamily: 'Courier New',
+  } as const
+
+  private timerRunning: boolean
+
+  private timer: ReturnType<typeof setTimeout>
+
+  constructor(props: $TSFixMe) {
+    super(props)
+    this.state = { elapsedTime: 0 }
+  }
+
+  startTimer() {
+    this.timerTick()
+    this.timerRunning = true
+  }
+
+  resetTimer() {
+    clearTimeout(this.timer)
+    this.setState({ elapsedTime: 0 })
+    this.timerRunning = false
+  }
+
+  timerTick() {
+    this.timer = setTimeout(() => {
+      this.setState((state: $TSFixMe) => ({
+        elapsedTime: state.elapsedTime + 1,
+      }))
+      this.timerTick()
+    }, 1000)
+  }
+
+  render() {
+    const { recording, i18n } = { ...this.props } as $TSFixMe
+    const { elapsedTime } = this.state as $TSFixMe
+
+    // second to minutes and seconds
+    const minAndSec = fmtMSS(elapsedTime)
+
+    if (recording && !this.timerRunning) {
+      this.startTimer()
+    }
+
+    if (!recording && this.timerRunning) {
+      this.resetTimer()
+    }
+
+    if (recording) {
+      return (
+        <div style={this.wrapperStyle}>
+          <div style={this.overlayStyle} />
+          <div style={this.infoContainerStyle}>
+            <div style={this.infotextStyle}>{i18n('recording')}</div>
+            <div style={this.timeStyle}>{minAndSec}</div>
+          </div>
+        </div>
+      )
+    }
+    return null
+  }
+}
+
+export default StopWatch

+ 28 - 5
packages/@uppy/screen-capture/src/StreamStatus.jsx → packages/@uppy/screen-capture/src/StreamStatus.tsx

@@ -1,10 +1,23 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { h } from 'preact'
 
-export default ({ streamActive, i18n }) => {
+type $TSFixMe = any
+
+export default function StreamStatus({ streamActive, i18n }: $TSFixMe) {
   if (streamActive) {
     return (
-      <div title={i18n('streamActive')} aria-label={i18n('streamActive')} className="uppy-ScreenCapture-icon--stream uppy-ScreenCapture-icon--streamActive">
-        <svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
+      <div
+        title={i18n('streamActive')}
+        aria-label={i18n('streamActive')}
+        className="uppy-ScreenCapture-icon--stream uppy-ScreenCapture-icon--streamActive"
+      >
+        <svg
+          aria-hidden="true"
+          focusable="false"
+          width="24"
+          height="24"
+          viewBox="0 0 24 24"
+        >
           <path d="M0 0h24v24H0z" opacity=".1" fill="none" />
           <path d="M0 0h24v24H0z" fill="none" />
           <path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
@@ -13,8 +26,18 @@ export default ({ streamActive, i18n }) => {
     )
   }
   return (
-    <div title={i18n('streamPassive')} aria-label={i18n('streamPassive')} className="uppy-ScreenCapture-icon--stream">
-      <svg aria-hidden="true" focusable="false" width="24" height="24" viewBox="0 0 24 24">
+    <div
+      title={i18n('streamPassive')}
+      aria-label={i18n('streamPassive')}
+      className="uppy-ScreenCapture-icon--stream"
+    >
+      <svg
+        aria-hidden="true"
+        focusable="false"
+        width="24"
+        height="24"
+        viewBox="0 0 24 24"
+      >
         <path d="M0 0h24v24H0z" opacity=".1" fill="none" />
         <path d="M0 0h24v24H0z" fill="none" />
         <path d="M21 3H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm0-4v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11z" />

+ 14 - 2
packages/@uppy/screen-capture/src/SubmitButton.jsx → packages/@uppy/screen-capture/src/SubmitButton.tsx

@@ -1,9 +1,17 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
 import { h } from 'preact'
 
+type $TSFixMe = any
+
 /**
  * Submit recorded video to uppy. Enabled when file is available
  */
-export default function SubmitButton ({ recording, recordedVideo, onSubmit, i18n }) {
+export default function SubmitButton({
+  recording,
+  recordedVideo,
+  onSubmit,
+  i18n,
+}: $TSFixMe) {
   if (recordedVideo && !recording) {
     return (
       <button
@@ -23,7 +31,11 @@ export default function SubmitButton ({ recording, recordedVideo, onSubmit, i18n
           focusable="false"
           className="uppy-c-icon"
         >
-          <path fill="#fff" fillRule="nonzero" d="M10.66 0L12 1.31 4.136 9 0 4.956l1.34-1.31L4.136 6.38z" />
+          <path
+            fill="#fff"
+            fillRule="nonzero"
+            d="M10.66 0L12 1.31 4.136 9 0 4.956l1.34-1.31L4.136 6.38z"
+          />
         </svg>
       </button>
     )

+ 0 - 1
packages/@uppy/screen-capture/src/index.js

@@ -1 +0,0 @@
-export { default } from './ScreenCapture.jsx'

+ 1 - 0
packages/@uppy/screen-capture/src/index.ts

@@ -0,0 +1 @@
+export { default } from './ScreenCapture.tsx'

+ 0 - 0
packages/@uppy/screen-capture/src/locale.js → packages/@uppy/screen-capture/src/locale.ts


+ 25 - 0
packages/@uppy/screen-capture/tsconfig.build.json

@@ -0,0 +1,25 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "noImplicitAny": false,
+    "outDir": "./lib",
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"]
+    },
+    "resolveJsonModule": false,
+    "rootDir": "./src",
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json"
+    },
+    {
+      "path": "../core/tsconfig.build.json"
+    }
+  ]
+}

+ 21 - 0
packages/@uppy/screen-capture/tsconfig.json

@@ -0,0 +1,21 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "paths": {
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+    },
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../utils/tsconfig.build.json",
+    },
+    {
+      "path": "../core/tsconfig.build.json",
+    },
+  ],
+}