changeset 456:7bb0bac6f8dc

Add export button for recordings and option to remove audio item (also removes all related analyses atm). Revokes associated object url for audio on removal. Will be problematic if the history is used for undo / redo.
author Lucas Thompson <dev@lucas.im>
date Thu, 29 Jun 2017 20:11:14 +0100
parents d27f1ca7ba6a
children 906dd152e333
files src/app/analysis-item/analysis-item.component.html src/app/analysis-item/analysis-item.component.ts src/app/app.component.html src/app/app.component.ts src/app/notebook-feed/notebook-feed.component.html src/app/notebook-feed/notebook-feed.component.ts
diffstat 6 files changed, 94 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/src/app/analysis-item/analysis-item.component.html	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/analysis-item/analysis-item.component.html	Thu Jun 29 20:11:14 2017 +0100
@@ -101,4 +101,15 @@
       </ng-template>
     </div>
   </md-card-content>
+  <md-card-actions
+    *ngIf="isAudioItem()">
+    <a md-icon-button
+       *ngIf="isAudioItem() && item.isExportable"
+      [href]="sanitize(item.uri)"
+      [download]="generateFilename(item)"
+    ><md-icon>file_download</md-icon></a>
+    <button md-icon-button (click)="remove.emit(item)">
+      <md-icon>delete_forever</md-icon>
+    </button>
+  </md-card-actions>
 </md-card>
--- a/src/app/analysis-item/analysis-item.component.ts	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/analysis-item/analysis-item.component.ts	Thu Jun 29 20:11:14 2017 +0100
@@ -6,7 +6,9 @@
   Component,
   Input,
   OnDestroy,
-  OnInit
+  OnInit,
+  Output,
+  EventEmitter
 } from '@angular/core';
 import {naivePagingMapper} from '../visualisations/WavesJunk';
 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
@@ -19,6 +21,7 @@
   RenderLoopService,
   TaskRemover
 } from '../services/render-loop/render-loop.service';
+import {DomSanitizer} from '@angular/platform-browser';
 
 export interface Item {
   id: string;
@@ -31,6 +34,7 @@
 export interface PendingRootAudioItem extends Item {
   uri: string;
   mimeType?: string;
+  isExportable?: boolean;
 }
 export interface RootAudioItem extends PendingRootAudioItem {
   audioData: AudioBuffer;
@@ -115,13 +119,17 @@
   @Input() item: Item;
   @Input() contentWidth: number;
   @Input() onSeek: OnSeekHandler;
+  @Output() remove: EventEmitter<Item>;
   // TODO move / re-think - naivePagingMapper feels like a big ol' bodge
   private removeAnimation: TaskRemover;
   private hasProgressOnInit = false;
   private mIsActive: boolean;
   private mTimeline: Timeline;
 
-  constructor(private renderLoop: RenderLoopService) {}
+  constructor(private renderLoop: RenderLoopService,
+              private sanitizer: DomSanitizer) {
+    this.remove = new EventEmitter<Item>();
+  }
 
   ngOnInit(): void {
     this.resetRemoveAnimation();
@@ -164,6 +172,18 @@
     this.removeAnimation();
   }
 
+  private sanitize(url: string) {
+    return this.sanitizer.bypassSecurityTrustUrl(url);
+  }
+
+  private generateFilename(item: PendingRootAudioItem): string {
+    // TODO this is too brittle, and will often produce the wrong result
+    // i.e. audio/mpeg results in .mpeg, when .mp3 is likely desired
+    const mimeParts = item.mimeType ? item.mimeType.split('/') : [];
+    const extension = mimeParts.length === 2 ? mimeParts[1] : '';
+    return `${item.title}.${extension}`;
+  }
+
   private resetRemoveAnimation(): void {
     if (this.removeAnimation) {
       this.removeAnimation();
--- a/src/app/app.component.html	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/app.component.html	Thu Jun 29 20:11:14 2017 +0100
@@ -7,7 +7,7 @@
 
       <ugly-playback-control></ugly-playback-control>
       <ugly-recording-control
-        (finishedRecording)="onFileOpened($event); tray.close()"
+        (finishedRecording)="onFileOpened($event, true); tray.close()"
       ></ugly-recording-control>
 
       <!-- This fills the remaining space of the current row -->
@@ -33,6 +33,7 @@
   </ugly-action-tray>
   <div class="ugly-content">
     <ugly-notebook-feed
+      (removeItem)="removeItem($event)"
       [analyses]="analyses.toIterable()"
       [rootAudioUri]="rootAudioItem.uri"
       [onSeek]="onSeek"></ugly-notebook-feed>
--- a/src/app/app.component.ts	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/app.component.ts	Thu Jun 29 20:11:14 2017 +0100
@@ -1,7 +1,8 @@
-import {Component, OnDestroy} from '@angular/core';
+import {Component, Inject, OnDestroy} from '@angular/core';
 import {
   AudioPlayerService,
-  AudioResourceError, AudioResource
+  AudioResourceError,
+  AudioResource
 } from './services/audio-player/audio-player.service';
 import {FeatureExtractionService} from './services/feature-extraction/feature-extraction.service';
 import {ExtractorOutputInfo} from './feature-extraction-menu/feature-extraction-menu.component';
@@ -10,10 +11,15 @@
 import {Subscription} from 'rxjs/Subscription';
 import {
   AnalysisItem,
+  isPendingAnalysisItem, isPendingRootAudioItem,
   isRootAudioItem,
-  Item, PendingAnalysisItem, PendingRootAudioItem, RootAudioItem
+  Item,
+  PendingAnalysisItem,
+  PendingRootAudioItem,
+  RootAudioItem
 } from './analysis-item/analysis-item.component';
 import {OnSeekHandler} from './playhead/PlayHeadHelpers';
+import {UrlResourceLifetimeManager} from './app.module';
 
 class PersistentStack<T> {
   private stack: T[];
@@ -60,6 +66,28 @@
     ];
   }
 
+  map<U>(transform: (value: T, index: number, array: T[]) => U): U[] {
+    return this.stack.map(transform);
+  }
+
+  reduce<U>(reducer: (previousValue: U,
+                      currentValue: T,
+                      currentIndex: number,
+                      array: T[]) => U,
+            initialValue: U): U {
+    return this.stack.reduce(reducer, initialValue);
+  }
+
+  remove(...indices: number[]) {
+    this.history.push([...this.stack]);
+    this.stack = this.stack.reduce((acc, item, i) => {
+      if (!indices.includes(i)) {
+        acc.push(item);
+      }
+      return acc;
+    }, [] as T[]);
+  }
+
   toIterable(): Iterable<T> {
     return this.stack;
   }
@@ -84,7 +112,10 @@
   constructor(private audioService: AudioPlayerService,
               private featureService: FeatureExtractionService,
               private iconRegistry: MdIconRegistry,
-              private sanitizer: DomSanitizer) {
+              private sanitizer: DomSanitizer,
+              @Inject(
+                'UrlResourceLifetimeManager'
+              ) private resourceManager: UrlResourceLifetimeManager) {
     this.analyses = new PersistentStack<AnalysisItem>();
     this.canExtract = false;
     this.nRecordings = 0;
@@ -144,7 +175,7 @@
     );
   }
 
-  onFileOpened(file: File | Blob) {
+  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?
@@ -165,7 +196,8 @@
       title: title,
       description: new Date().toLocaleString(),
       id: `${++this.countingId}`,
-      mimeType: file.type
+      mimeType: file.type,
+      isExportable: createExportableItem
     } as PendingRootAudioItem;
     this.rootAudioItem = pending as RootAudioItem; // TODO this is silly
 
@@ -223,6 +255,22 @@
     });
   }
 
+  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;
+      }, []);
+    if (isPendingRootAudioItem(item)) {
+      this.resourceManager.revokeUrlToResource(item.uri);
+    }
+    this.analyses.remove(...indicesToRemove);
+  }
+
   ngOnDestroy(): void {
     this.onAudioDataSubscription.unsubscribe();
     this.onProgressUpdated.unsubscribe();
--- a/src/app/notebook-feed/notebook-feed.component.html	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/notebook-feed/notebook-feed.component.html	Thu Jun 29 20:11:14 2017 +0100
@@ -1,7 +1,7 @@
 <div class="feed">
   <ng-template ngFor let-item [ngForOf]="analyses">
     <div [class.break]="isAudioItem(item)">
-      <ugly-analysis-item
+      <ugly-analysis-item (remove)="removeItem.emit($event)"
         [timeline]="getOrCreateTimeline(item)"
         [isActive]="isActiveItem(item)"
         [item]="item"
--- a/src/app/notebook-feed/notebook-feed.component.ts	Thu Jun 29 20:09:11 2017 +0100
+++ b/src/app/notebook-feed/notebook-feed.component.ts	Thu Jun 29 20:11:14 2017 +0100
@@ -4,10 +4,10 @@
 import {
   ChangeDetectionStrategy,
   ChangeDetectorRef,
-  Component,
+  Component, EventEmitter,
   Inject,
   Input,
-  OnDestroy
+  OnDestroy, Output
 } from '@angular/core';
 import Waves from 'waves-ui-piper';
 import {
@@ -32,6 +32,7 @@
     this._rootAudioUri = uri;
   }
   @Input() onSeek: OnSeekHandler;
+  @Output() removeItem: EventEmitter<Item>;
 
   get rootAudioUri(): string {
     return this._rootAudioUri;
@@ -46,6 +47,7 @@
     private ref: ChangeDetectorRef,
     @Inject('DimensionObservable') private onResize: Observable<Dimension>
   ) {
+    this.removeItem = new EventEmitter<Item>();
     this.timelines = new Map();
     this.onResize.subscribe(dim => {
       this.lastWidth = this.width;