view src/app/app.component.ts @ 505:cf4a17efb5d4

Update angular deps, fix material api changes. (currently running an earlier TypeScript version due to restrictions from angular)
author Lucas Thompson <dev@lucas.im>
date Thu, 12 Oct 2017 13:12:33 +0100
parents c39df81c4dae
children
line wrap: on
line source
import {Component, Inject, OnDestroy} from '@angular/core';
import {
  AudioPlayerService,
  AudioResourceError,
  AudioResource,
  AudioLoadResponse
} from './services/audio-player/audio-player.service';
import {
  ExtractionResult,
  FeatureExtractionService
} from './services/feature-extraction/feature-extraction.service';
import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
import {DomSanitizer} from '@angular/platform-browser';
import {MatIconRegistry} from '@angular/material';
import {Subscription} from 'rxjs/Subscription';
import {
  AnalysisItem,
  isPendingAnalysisItem,
  isPendingRootAudioItem,
  isLoadedRootAudioItem,
  Item,
  RootAudioItem,
  getRootAudioItem
} from './analysis-item/AnalysisItem';
import {OnSeekHandler} from './playhead/PlayHeadHelpers';
import {UrlResourceLifetimeManager} from './services/File';
import {createExtractionRequest} from './analysis-item/AnalysisItem';
import {PersistentStack} from './Session';
import {NotificationService} from './services/notifications/notifications.service';

@Component({
  selector: 'ugly-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
  canExtract: boolean;
  private onAudioDataSubscription: Subscription;
  private onProgressUpdated: Subscription;
  private analyses: PersistentStack<Item>; // TODO some immutable state container describing entire session
  private nRecordings: number; // TODO user control for naming a recording
  private countingId: number; // TODO improve uniquely identifying items
  private onSeek: OnSeekHandler;

  constructor(private audioService: AudioPlayerService,
              private featureService: FeatureExtractionService,
              private iconRegistry: MatIconRegistry,
              private sanitizer: DomSanitizer,
              @Inject(
                'UrlResourceLifetimeManager'
              ) private resourceManager: UrlResourceLifetimeManager,
              private notifier: NotificationService) {
    this.analyses = new PersistentStack<AnalysisItem>();
    this.canExtract = false;
    this.nRecordings = 0;
    this.countingId = 0;
    this.onSeek = (time) => this.audioService.seekTo(time);

    iconRegistry.addSvgIcon(
      'duck',
      sanitizer.bypassSecurityTrustResourceUrl('assets/duck.svg')
    );

    this.onAudioDataSubscription = this.audioService.audioLoaded$.subscribe(
      resource => {
        const findCurrentAudio =
          val => isPendingRootAudioItem(val) && val.uri === getRootAudioItem(
            this.analyses.get(0)
          ).uri;
        const wasError = (res: AudioLoadResponse):
          res is AudioResourceError => (res as any).message != null;
        if (wasError(resource)) {
          this.notifier.displayError(resource.message);
          this.analyses.findIndexAndUse(
            findCurrentAudio,
            index => this.analyses.remove(index)
          );
          this.canExtract = false;
        } else {
          const audioData = (resource as AudioResource).samples;
          if (audioData) {
            this.canExtract = true;
            this.analyses.findIndexAndUse(
              findCurrentAudio,
              currentRootIndex => this.analyses.set(
                currentRootIndex,
                Object.assign(
                  {},
                  this.analyses.get(currentRootIndex),
                  {audioData}
                )
              ));
          }
        }
      }
    );
    this.onProgressUpdated = this.featureService.progressUpdated$.subscribe(
      progress => {
        this.analyses.findIndexAndUse(
          val => val.id === progress.id,
          index => this.analyses.setMutating(
            index,
            Object.assign(
              {},
              this.analyses.get(index),
              {progress: progress.value}
            )
          )
        );
      }
    );
  }

  onFileOpened(file: File | Blob, createExportableItem = false) {
    this.canExtract = false;
    const url = this.audioService.loadAudio(file);
    // TODO is it safe to assume it is a recording?
    const title = (file instanceof File) ?
      (file as File).name : `Recording ${this.nRecordings++}`;

    if (this.analyses.filter(item => item.title === title).length > 0) {
      // TODO this reveals how brittle the current name / uri based id is
      // need something more robust, and also need to notify the user
      // in a suitable way in the actual event of a duplicate file
      console.warn('There is already a notebook based on this audio file.');
      return;
    }

    const pending = {
      uri: url,
      hasSharedTimeline: true,
      title: title,
      description: new Date().toLocaleString(),
      id: `${++this.countingId}`,
      mimeType: file.type,
      isExportable: createExportableItem
    } as RootAudioItem;

    // TODO re-ordering of items for display
    // , one alternative is a Angular Pipe / Filter for use in the Template
    this.analyses.unshiftMutating(pending);
  }

  extractFeatures(outputInfo: ExtractorOutputInfo): string {
    if (!this.canExtract || !outputInfo) {
      return;
    }

    this.canExtract = false;

    const rootAudio = getRootAudioItem(this.analyses.get(0));

    if (isLoadedRootAudioItem(rootAudio)) {
      const placeholderCard: AnalysisItem = {
        parent: rootAudio,
        hasSharedTimeline: true,
        extractorKey: outputInfo.extractorKey,
        outputId: outputInfo.outputId,
        title: outputInfo.name,
        description: outputInfo.outputId,
        id: `${++this.countingId}`,
        progress: 0
      };
      this.analyses.unshiftMutating(placeholderCard);
      this.sendExtractionRequest(placeholderCard);
      return placeholderCard.id;
    }
    throw new Error('Cannot extract. No audio loaded');
  }

  removeItem(item: Item): void {
    const indicesToRemove: number[] = this.analyses.reduce(
      (toRemove, current, index) => {
        if (isPendingAnalysisItem(current) && current.parent.id === item.id) {
          toRemove.push(index);
        } else if (item.id === current.id) {
          toRemove.push(index);
        }
        return toRemove;
      }, []);
    this.analyses.remove(...indicesToRemove);
  }

  ngOnDestroy(): void {
    this.onAudioDataSubscription.unsubscribe();
    this.onProgressUpdated.unsubscribe();
  }

  private sendExtractionRequest(analysis: AnalysisItem): Promise<void> {
    const findAndUpdateItem = (result: ExtractionResult): void => {
      // TODO subscribe to the extraction service instead
      this.analyses.findIndexAndUse(
        val => val.id === result.id,
        (index) => this.analyses.set(
          index,
          Object.assign(
            {},
            this.analyses.get(index),
            result.result,
            result.unit ? {unit: result.unit} : {}
          )
        )
      );
      this.canExtract = true;
    };
    return this.featureService.extract(
      analysis.id,
      createExtractionRequest(analysis))
      .then(findAndUpdateItem)
      .catch(err => {
        this.canExtract = true;
        this.analyses.findIndexAndUse(
          val => val.id === analysis.id,
          index => this.analyses.remove(index)
        );
        this.notifier.displayError(`Error whilst extracting: ${err}`);
      });
  }
}