Chocobozzz/PeerTube

View on GitHub
client/src/app/+videos/+video-edit/shared/thumbnail-manager/thumbnail-manager.component.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { CommonModule } from '@angular/common'
import {
  Component,
  Input,
  OnInit,
  ViewChild,
  forwardRef
} from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import {
  ServerService
} from '@app/core'
import { ReactiveFileComponent } from '@app/shared/shared-forms/reactive-file.component'
import { BytesPipe } from '@app/shared/shared-main/common/bytes.pipe'
import { EmbedComponent, EmbedVideoInput } from '@app/shared/shared-main/video/embed.component'
import { HTMLServerConfig, Video, VideoState } from '@peertube/peertube-models'
import { imageToDataURL } from '@root-helpers/images'
import { PeerTubePlayer } from '../../../../../standalone/embed-player-api/player'

@Component({
  selector: 'my-thumbnail-manager',
  styleUrls: [ './thumbnail-manager.component.scss' ],
  templateUrl: './thumbnail-manager.component.html',
  standalone: true,
  imports: [ CommonModule, ReactiveFileComponent, EmbedComponent ],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ThumbnailManagerComponent),
      multi: true
    }
  ]
})
export class ThumbnailManagerComponent implements OnInit, ControlValueAccessor {
  @ViewChild('embed') embed: EmbedComponent

  @Input() video: EmbedVideoInput & Pick<Video, 'isLive' | 'state'>

  imageSrc: string
  allowedExtensionsMessage = ''
  maxSizeText: string

  serverConfig: HTMLServerConfig
  bytesPipe: BytesPipe
  imageFile: Blob

  selectingFromVideo = false

  player: PeerTubePlayer

  constructor (
    private serverService: ServerService
  ) {
    this.bytesPipe = new BytesPipe()
    this.maxSizeText = $localize`max size`
  }

  // ---------------------------------------------------------------------------
  // Upload
  // ---------------------------------------------------------------------------

  get videoImageExtensions () {
    return this.serverConfig.video.image.extensions
  }

  get maxVideoImageSize () {
    return this.serverConfig.video.image.size.max
  }

  get maxVideoImageSizeInBytes () {
    return this.bytesPipe.transform(this.maxVideoImageSize)
  }

  canSelectFromVideo () {
    return this.video && !this.video.isLive && this.video.state.id === VideoState.PUBLISHED
  }

  getReactiveFileButtonTooltip () {
    return $localize`(extensions: ${this.videoImageExtensions}, ${this.maxSizeText}\: ${this.maxVideoImageSizeInBytes})`
  }

  ngOnInit () {
    this.serverConfig = this.serverService.getHTMLConfig()

    this.allowedExtensionsMessage = this.videoImageExtensions.join(', ')
  }

  onFileChanged (file: Blob) {
    this.imageFile = file

    this.propagateChange(this.imageFile)
    this.updatePreview()
  }

  propagateChange = (_: any) => { /* empty */ }

  writeValue (file: any) {
    this.imageFile = file
    this.updatePreview()
  }

  registerOnChange (fn: (_: any) => void) {
    this.propagateChange = fn
  }

  registerOnTouched () {
    // Unused
  }

  private updatePreview () {
    if (this.imageFile) {
      imageToDataURL(this.imageFile).then(result => this.imageSrc = result)
    }
  }

  // ---------------------------------------------------------------------------
  // Select from frame
  // ---------------------------------------------------------------------------

  selectFromVideo () {
    this.selectingFromVideo = true

    setTimeout(() => {
      this.player = new PeerTubePlayer(this.embed.getIframe())
    })
  }

  resetSelectFromVideo () {
    if (this.player) {
      this.player.destroy()
      this.player = undefined
    }

    this.selectingFromVideo = false
  }

  async selectFrame () {
    const dataUrl: string = await this.player.getImageDataUrl()

    // Checking for an empty data URL
    if (dataUrl.length <= 6) {
      return
    }

    this.imageSrc = dataUrl

    const blob: Blob = this.dataURItoBlob(dataUrl)

    const file = new File([ blob ], 'preview-file-from-frame.jpg', { type: 'image/jpeg' })

    this.imageFile = file

    this.propagateChange(this.imageFile)

    this.resetSelectFromVideo()
  }

  // Credit: https://stackoverflow.com/a/7261048/1030669
  dataURItoBlob (dataURI: string) {
    const byteString = atob(dataURI.split(',')[1])
    const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]

    const ab = new ArrayBuffer(byteString.length)

    const ia = new Uint8Array(ab)

    for (let i = 0; i < byteString.length; i++) {
      ia[i] = byteString.charCodeAt(i)
    }

    return new Blob([ ab ], { type: mimeString })
  }
}