Viewing File: /home/ubuntu/voice-assistant-frontend/node_modules/react-h5-audio-player/src/ProgressBar.tsx

import React, { Component, forwardRef, SyntheticEvent } from 'react'
import { getPosX, throttle } from './utils'
import { OnSeek } from './index'

interface ProgressBarForwardRefProps {
  audio: HTMLAudioElement
  progressUpdateInterval: number
  showDownloadProgress: boolean
  showFilledProgress: boolean
  srcDuration?: number
  onSeek?: OnSeek
  onChangeCurrentTimeError?: () => void
  i18nProgressBar: string
}
interface ProgressBarProps extends ProgressBarForwardRefProps {
  progressBar: React.RefObject<HTMLDivElement>
}

interface ProgressBarState {
  isDraggingProgress: boolean
  currentTimePos?: string
  hasDownloadProgressAnimation: boolean
  downloadProgressArr: DownloadProgress[]
  waitingForSeekCallback: boolean
}

interface DownloadProgress {
  left: string
  width: string
}

interface TimePosInfo {
  currentTime: number
  currentTimePos: string
}

class ProgressBar extends Component<ProgressBarProps, ProgressBarState> {
  audio?: HTMLAudioElement

  timeOnMouseMove = 0 // Audio's current time while mouse is down and moving over the progress bar

  hasAddedAudioEventListener = false

  downloadProgressAnimationTimer?: number

  state: ProgressBarState = {
    isDraggingProgress: false,
    currentTimePos: '0%',
    hasDownloadProgressAnimation: false,
    downloadProgressArr: [],
    waitingForSeekCallback: false,
  }

  getDuration(): number {
    const { audio, srcDuration } = this.props
    return typeof srcDuration === 'undefined' ? audio.duration : srcDuration
  }

  // Get time info while dragging indicator by mouse or touch
  getCurrentProgress = (event: MouseEvent | TouchEvent): TimePosInfo => {
    const { audio, progressBar } = this.props
    const isSingleFileProgressiveDownload =
      audio.src.indexOf('blob:') !== 0 && typeof this.props.srcDuration === 'undefined'

    if (isSingleFileProgressiveDownload && (!audio.src || !isFinite(audio.currentTime) || !progressBar.current)) {
      return { currentTime: 0, currentTimePos: '0%' }
    }

    const progressBarRect = progressBar.current.getBoundingClientRect()
    const maxRelativePos = progressBarRect.width
    let relativePos = getPosX(event) - progressBarRect.left

    if (relativePos < 0) {
      relativePos = 0
    } else if (relativePos > maxRelativePos) {
      relativePos = maxRelativePos
    }
    const duration = this.getDuration()
    const currentTime = (duration * relativePos) / maxRelativePos
    return { currentTime, currentTimePos: `${((relativePos / maxRelativePos) * 100).toFixed(2)}%` }
  }

  handleContextMenu = (event: SyntheticEvent): void => {
    event.preventDefault()
  }

  /* Handle mouse down or touch start on progress bar event */
  handleMouseDownOrTouchStartProgressBar = (event: React.MouseEvent | React.TouchEvent): void => {
    event.stopPropagation()
    const { currentTime, currentTimePos } = this.getCurrentProgress(event.nativeEvent)

    if (isFinite(currentTime)) {
      this.timeOnMouseMove = currentTime
      this.setState({ isDraggingProgress: true, currentTimePos })
      if (event.nativeEvent instanceof MouseEvent) {
        window.addEventListener('mousemove', this.handleWindowMouseOrTouchMove)
        window.addEventListener('mouseup', this.handleWindowMouseOrTouchUp)
      } else {
        window.addEventListener('touchmove', this.handleWindowMouseOrTouchMove)
        window.addEventListener('touchend', this.handleWindowMouseOrTouchUp)
      }
    }
  }

  handleWindowMouseOrTouchMove = (event: TouchEvent | MouseEvent): void => {
    if (event instanceof MouseEvent) {
      event.preventDefault()
    }
    event.stopPropagation()
    // Prevent Chrome drag selection bug
    const windowSelection: Selection | null = window.getSelection()
    if (windowSelection && windowSelection.type === 'Range') {
      windowSelection.empty()
    }

    const { isDraggingProgress } = this.state
    if (isDraggingProgress) {
      const { currentTime, currentTimePos } = this.getCurrentProgress(event)
      this.timeOnMouseMove = currentTime
      this.setState({ currentTimePos })
    }
  }

  handleWindowMouseOrTouchUp = (event: MouseEvent | TouchEvent): void => {
    event.stopPropagation()
    const newTime = this.timeOnMouseMove
    const { audio, onChangeCurrentTimeError, onSeek } = this.props

    if (onSeek) {
      this.setState({ isDraggingProgress: false, waitingForSeekCallback: true }, () => {
        onSeek(audio, newTime).then(
          () => this.setState({ waitingForSeekCallback: false }),
          (err) => {
            throw new Error(err)
          }
        )
      })
    } else {
      const newProps: { isDraggingProgress: boolean; currentTimePos?: string } = {
        isDraggingProgress: false,
      }
      if (audio.readyState === audio.HAVE_NOTHING || audio.readyState === audio.HAVE_METADATA || !isFinite(newTime)) {
        newProps.currentTimePos = '0%'
        onChangeCurrentTimeError && onChangeCurrentTimeError()
      } else {
        audio.currentTime = newTime
      }
      this.setState(newProps)
    }

    if (event instanceof MouseEvent) {
      window.removeEventListener('mousemove', this.handleWindowMouseOrTouchMove)
      window.removeEventListener('mouseup', this.handleWindowMouseOrTouchUp)
    } else {
      window.removeEventListener('touchmove', this.handleWindowMouseOrTouchMove)
      window.removeEventListener('touchend', this.handleWindowMouseOrTouchUp)
    }
  }

  handleAudioTimeUpdate = throttle((e: Event): void => {
    const { isDraggingProgress } = this.state
    const audio = e.target as HTMLAudioElement
    if (isDraggingProgress || this.state.waitingForSeekCallback === true) return

    const { currentTime } = audio
    const duration = this.getDuration()

    this.setState({
      currentTimePos: `${((currentTime / duration) * 100 || 0).toFixed(2)}%`,
    })
  }, this.props.progressUpdateInterval)

  handleAudioDownloadProgressUpdate = (e: Event): void => {
    const audio = e.target as HTMLAudioElement
    const duration = this.getDuration()

    const downloadProgressArr: DownloadProgress[] = []
    for (let i = 0; i < audio.buffered.length; i++) {
      const bufferedStart: number = audio.buffered.start(i)
      const bufferedEnd: number = audio.buffered.end(i)
      downloadProgressArr.push({
        left: `${Math.round((100 / duration) * bufferedStart) || 0}%`,
        width: `${Math.round((100 / duration) * (bufferedEnd - bufferedStart)) || 0}%`,
      })
    }

    clearTimeout(this.downloadProgressAnimationTimer)
    this.setState({ downloadProgressArr, hasDownloadProgressAnimation: true })
    this.downloadProgressAnimationTimer = setTimeout(() => {
      this.setState({ hasDownloadProgressAnimation: false })
    }, 200)
  }

  initialize(): void {
    const { audio } = this.props
    if (audio && !this.hasAddedAudioEventListener) {
      this.audio = audio
      this.hasAddedAudioEventListener = true
      audio.addEventListener('timeupdate', this.handleAudioTimeUpdate)
      audio.addEventListener('progress', this.handleAudioDownloadProgressUpdate)
    }
  }

  componentDidMount(): void {
    this.initialize()
  }

  componentDidUpdate(): void {
    this.initialize()
  }

  componentWillUnmount(): void {
    if (this.audio && this.hasAddedAudioEventListener) {
      this.audio.removeEventListener('timeupdate', this.handleAudioTimeUpdate)
      this.audio.removeEventListener('progress', this.handleAudioDownloadProgressUpdate)
    }
    clearTimeout(this.downloadProgressAnimationTimer)
  }

  render(): React.ReactNode {
    const { showDownloadProgress, showFilledProgress, progressBar, i18nProgressBar } = this.props
    const { currentTimePos, downloadProgressArr, hasDownloadProgressAnimation } = this.state

    return (
      <div
        className="rhap_progress-container"
        ref={progressBar}
        aria-label={i18nProgressBar}
        role="progressbar"
        aria-valuemin={0}
        aria-valuemax={100}
        aria-valuenow={Number(currentTimePos.split('%')[0])}
        tabIndex={0}
        onMouseDown={this.handleMouseDownOrTouchStartProgressBar}
        onTouchStart={this.handleMouseDownOrTouchStartProgressBar}
        onContextMenu={this.handleContextMenu}
      >
        <div className={`rhap_progress-bar ${showDownloadProgress ? 'rhap_progress-bar-show-download' : ''}`}>
          <div className="rhap_progress-indicator" style={{ left: currentTimePos }} />
          {showFilledProgress && <div className="rhap_progress-filled" style={{ width: currentTimePos }} />}
          {showDownloadProgress &&
            downloadProgressArr.map(({ left, width }, i) => (
              <div
                key={i}
                className="rhap_download-progress"
                style={{ left, width, transitionDuration: hasDownloadProgressAnimation ? '.2s' : '0s' }}
              />
            ))}
        </div>
      </div>
    )
  }
}

const ProgressBarForwardRef = (
  props: ProgressBarForwardRefProps,
  ref: React.Ref<HTMLDivElement>
): React.ReactElement => <ProgressBar {...props} progressBar={ref as React.RefObject<HTMLDivElement>} />

export default forwardRef(ProgressBarForwardRef)
export { ProgressBar, ProgressBarForwardRef }
Back to Directory File Manager