annotate src/app/analysis-item/analysis-item.component.ts @ 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 8113b6f5a75e
children ccce2c09502e
rev   line source
dev@170 1 /**
dev@170 2 * Created by lucast on 21/03/2017.
dev@170 3 */
dev@231 4 import {
dev@231 5 ChangeDetectionStrategy,
dev@231 6 Component,
dev@231 7 Input,
dev@408 8 OnDestroy,
dev@456 9 OnInit,
dev@456 10 Output,
dev@456 11 EventEmitter
dev@236 12 } from '@angular/core';
dev@348 13 import {naivePagingMapper} from '../visualisations/WavesJunk';
dev@408 14 import {OnSeekHandler} from '../playhead/PlayHeadHelpers';
dev@361 15 import {
dev@381 16 defaultColourGenerator,
dev@361 17 HigherLevelFeatureShape,
dev@361 18 KnownShapedFeature
dev@361 19 } from '../visualisations/FeatureUtilities';
dev@408 20 import {
dev@408 21 RenderLoopService,
dev@408 22 TaskRemover
dev@408 23 } from '../services/render-loop/render-loop.service';
dev@456 24 import {DomSanitizer} from '@angular/platform-browser';
dev@170 25
dev@350 26 export interface Item {
dev@350 27 id: string;
dev@200 28 hasSharedTimeline: boolean;
dev@200 29 title?: string;
dev@200 30 description?: string;
dev@224 31 progress?: number;
dev@350 32 }
dev@350 33
dev@350 34 export interface PendingRootAudioItem extends Item {
dev@350 35 uri: string;
dev@453 36 mimeType?: string;
dev@456 37 isExportable?: boolean;
dev@350 38 }
dev@378 39 export interface RootAudioItem extends PendingRootAudioItem {
dev@350 40 audioData: AudioBuffer;
dev@350 41 }
dev@350 42
dev@350 43 export interface PendingAnalysisItem extends Item {
dev@350 44 parent: RootAudioItem;
dev@350 45 extractorKey: string;
dev@350 46 }
dev@350 47
dev@396 48 export type AnalysisItem = PendingAnalysisItem & KnownShapedFeature & {
dev@396 49 unit?: string
dev@396 50 };
dev@361 51
dev@361 52 export function isItem(item: Item): item is Item {
dev@361 53 return item.id != null && item.hasSharedTimeline != null;
dev@350 54 }
dev@350 55
dev@350 56 export function isPendingRootAudioItem(item: Item): item is PendingRootAudioItem {
dev@361 57 return isItem(item) && typeof (item as RootAudioItem).uri === 'string';
dev@350 58 }
dev@350 59
dev@350 60 export function isRootAudioItem(item: Item): item is RootAudioItem {
dev@410 61 return item && isPendingRootAudioItem(item) &&
dev@351 62 (item as RootAudioItem).audioData instanceof AudioBuffer;
dev@350 63 }
dev@350 64
dev@350 65 export function isPendingAnalysisItem(item: Item): item is AnalysisItem {
dev@350 66 const downcast = (item as AnalysisItem);
dev@350 67 return isRootAudioItem(downcast.parent)
dev@350 68 && typeof downcast.extractorKey === 'string';
dev@350 69 }
dev@350 70
dev@350 71 export function isAnalysisItem(item: Item): item is AnalysisItem {
dev@350 72 const downcast = (item as AnalysisItem);
dev@361 73 return isPendingAnalysisItem(item) &&
dev@361 74 downcast.shape != null &&
dev@361 75 downcast.collected != null;
dev@350 76 }
dev@350 77
dev@350 78 // these should probably be actual concrete types with their own getUri methods
dev@350 79 export function getRootUri(item: Item): string {
dev@350 80 if (isPendingRootAudioItem(item)) {
dev@350 81 return item.uri;
dev@350 82 }
dev@350 83 if (isPendingAnalysisItem(item)) {
dev@350 84 return item.parent.uri;
dev@350 85 }
dev@350 86 throw new Error('Invalid item: No URI property set.');
dev@170 87 }
dev@170 88
dev@170 89 @Component({
dev@170 90 selector: 'ugly-analysis-item',
dev@170 91 templateUrl: './analysis-item.component.html',
dev@231 92 styleUrls: ['./analysis-item.component.css'],
dev@231 93 changeDetection: ChangeDetectionStrategy.OnPush
dev@170 94 })
dev@408 95 export class AnalysisItemComponent implements OnInit, OnDestroy {
dev@224 96
dev@408 97 // TODO should be TimelineTimeContext?
dev@408 98 @Input() set timeline(timeline: Timeline) {
dev@408 99 this.mTimeline = timeline;
dev@408 100 this.resetRemoveAnimation();
dev@408 101 }
dev@408 102
dev@408 103 get timeline(): Timeline {
dev@408 104 return this.mTimeline;
dev@408 105 }
dev@408 106
dev@408 107 @Input() set isActive(isActive: boolean) {
dev@408 108 this.removeAnimation();
dev@408 109 this.mIsActive = isActive;
dev@408 110 if (isActive) {
dev@408 111 this.resetRemoveAnimation();
dev@408 112 }
dev@408 113 }
dev@408 114
dev@408 115 get isActive() {
dev@408 116 return this.mIsActive;
dev@408 117 }
dev@408 118
dev@350 119 @Input() item: Item;
dev@285 120 @Input() contentWidth: number;
dev@348 121 @Input() onSeek: OnSeekHandler;
dev@456 122 @Output() remove: EventEmitter<Item>;
dev@408 123 // TODO move / re-think - naivePagingMapper feels like a big ol' bodge
dev@408 124 private removeAnimation: TaskRemover;
dev@224 125 private hasProgressOnInit = false;
dev@408 126 private mIsActive: boolean;
dev@408 127 private mTimeline: Timeline;
dev@224 128
dev@456 129 constructor(private renderLoop: RenderLoopService,
dev@456 130 private sanitizer: DomSanitizer) {
dev@456 131 this.remove = new EventEmitter<Item>();
dev@456 132 }
dev@348 133
dev@224 134 ngOnInit(): void {
dev@408 135 this.resetRemoveAnimation();
dev@231 136 this.hasProgressOnInit = this.item.progress != null;
dev@224 137 }
dev@224 138
dev@224 139 isLoading(): boolean {
dev@231 140 return this.hasProgressOnInit && this.item.progress < 100;
dev@224 141 }
dev@348 142
dev@348 143 isAudioItem(): boolean {
dev@408 144 return this.item && isRootAudioItem(this.item);
dev@348 145 }
dev@361 146
dev@410 147 isPending(): boolean {
dev@410 148 return this.item &&
dev@410 149 !isRootAudioItem(this.item) && !isAnalysisItem(this.item) &&
dev@410 150 (isPendingAnalysisItem(this.item) || isPendingRootAudioItem(this.item));
dev@410 151 }
dev@410 152
dev@361 153 getFeatureShape(): HigherLevelFeatureShape | null {
dev@361 154 return !isPendingRootAudioItem(this.item) &&
dev@361 155 isAnalysisItem(this.item) ? this.item.shape : null;
dev@361 156 }
dev@381 157
dev@412 158 getDuration(): number | null {
dev@412 159 if (isRootAudioItem(this.item)) {
dev@412 160 return this.item.audioData.duration;
dev@412 161 }
dev@412 162 if (isAnalysisItem(this.item)) {
dev@412 163 return this.item.parent.audioData.duration;
dev@412 164 }
dev@412 165 }
dev@412 166
dev@381 167 getNextColour(): string {
dev@381 168 return defaultColourGenerator.next().value;
dev@381 169 }
dev@408 170
dev@408 171 ngOnDestroy(): void {
dev@408 172 this.removeAnimation();
dev@408 173 }
dev@408 174
dev@456 175 private sanitize(url: string) {
dev@456 176 return this.sanitizer.bypassSecurityTrustUrl(url);
dev@456 177 }
dev@456 178
dev@456 179 private generateFilename(item: PendingRootAudioItem): string {
dev@456 180 // TODO this is too brittle, and will often produce the wrong result
dev@456 181 // i.e. audio/mpeg results in .mpeg, when .mp3 is likely desired
dev@456 182 const mimeParts = item.mimeType ? item.mimeType.split('/') : [];
dev@456 183 const extension = mimeParts.length === 2 ? mimeParts[1] : '';
dev@456 184 return `${item.title}.${extension}`;
dev@456 185 }
dev@456 186
dev@408 187 private resetRemoveAnimation(): void {
dev@408 188 if (this.removeAnimation) {
dev@408 189 this.removeAnimation();
dev@408 190 }
dev@408 191 const createPagingTask = () => {
dev@408 192 const pagingMapper = naivePagingMapper(this.timeline);
dev@408 193 return this.renderLoop.addPlayingTask(currentTime => {
dev@408 194 pagingMapper(currentTime);
dev@408 195 });
dev@408 196 };
dev@408 197 // only add a pager to audio items, it can drive the feature items
dev@408 198 const remover = this.timeline && this.isAudioItem() ?
dev@408 199 createPagingTask() : () => {};
dev@408 200 this.removeAnimation = () => {
dev@408 201 remover();
dev@408 202 this.removeAnimation = () => {};
dev@408 203 };
dev@408 204 }
dev@170 205 }