Skip to content
Permalink
Browse files
feat(representation): implement video player incl. preview (DEV-701) (#…
…698)

* feat(representation): implement video player incl. preview

* refactor: format code

* refactor(av): better host handler

* refactor(representation): clean up code in video components

* test(pipe): test time pipe

* test(representation): fix and write video tests

* refactor(representation): video details

* chore(deps): bump js-lib to latest

* feat(video): replace moving image file functionality

* fix(video): set correct preview position on timeline

* style(preview): hide preview on click

* refactor(av-timeline): clean up code and add more comments

* refactor(video-preview): clean up code and add more comments

* refactor(video): clean up code and add more comments

* refactor(video): clean up code and add more comments

* feat(video): matrix file error handler and code refactoring

* fix(video): better calc

* refactor(video): clean up code

* refactor(video): remove commented code and fix typo

* fix(video): update time after replacing video file

* fix(representation): replace file only, if data exists
  • Loading branch information
kilchenmann committed Apr 22, 2022
1 parent c77c208 commit 86fc2a7d58e9d3227e3e94b01f14f469d45829b3
Show file tree
Hide file tree
Showing 30 changed files with 1,714 additions and 19 deletions.
@@ -30,6 +30,7 @@ import { DspApiConfigToken, DspApiConnectionToken, DspAppConfigToken, DspInstrum
import { DialogHeaderComponent } from './main/dialog/dialog-header/dialog-header.component';
import { DialogComponent } from './main/dialog/dialog.component';
import { AdminImageDirective } from './main/directive/admin-image/admin-image.directive';
import { DisableContextMenuDirective } from './main/directive/disable-context-menu.directive';
import { ExistingNameDirective } from './main/directive/existing-name/existing-name.directive';
import { ExternalLinksDirective } from './main/directive/external-links.directive';
import { GndDirective } from './main/directive/gnd/gnd.directive';
@@ -47,6 +48,7 @@ import { LinkifyPipe } from './main/pipes/string-transformation/linkify.pipe';
import { StringifyStringLiteralPipe } from './main/pipes/string-transformation/stringify-string-literal.pipe';
import { TitleFromCamelCasePipe } from './main/pipes/string-transformation/title-from-camel-case.pipe';
import { TruncatePipe } from './main/pipes/string-transformation/truncate.pipe';
import { TimePipe } from './main/pipes/time.pipe';
import { SelectLanguageComponent } from './main/select-language/select-language.component';
import { DatadogRumService } from './main/services/datadog-rum.service';
import { MaterialModule } from './material-module';
@@ -98,9 +100,13 @@ import { PropertiesComponent } from './workspace/resource/properties/properties.
import { AddRegionFormComponent } from './workspace/resource/representation/add-region-form/add-region-form.component';
import { ArchiveComponent } from './workspace/resource/representation/archive/archive.component';
import { AudioComponent } from './workspace/resource/representation/audio/audio.component';
import { AvTimelineComponent } from './workspace/resource/representation/av-timeline/av-timeline.component';
import { DocumentComponent } from './workspace/resource/representation/document/document.component';
import { ReplaceFileFormComponent } from './workspace/resource/representation/replace-file-form/replace-file-form.component';
import { StillImageComponent } from './workspace/resource/representation/still-image/still-image.component';
import { UploadComponent } from './workspace/resource/representation/upload/upload.component';
import { VideoPreviewComponent } from './workspace/resource/representation/video/video-preview/video-preview.component';
import { VideoComponent } from './workspace/resource/representation/video/video.component';
import { ResourceInstanceFormComponent } from './workspace/resource/resource-instance-form/resource-instance-form.component';
import { SelectOntologyComponent } from './workspace/resource/resource-instance-form/select-ontology/select-ontology.component';
import { SelectProjectComponent } from './workspace/resource/resource-instance-form/select-project/select-project.component';
@@ -153,7 +159,6 @@ import { SearchSelectOntologyComponent } from './workspace/search/advanced-searc
import { ExpertSearchComponent } from './workspace/search/expert-search/expert-search.component';
import { FulltextSearchComponent } from './workspace/search/fulltext-search/fulltext-search.component';
import { SearchPanelComponent } from './workspace/search/search-panel/search-panel.component';
import { ReplaceFileFormComponent } from './workspace/resource/representation/replace-file-form/replace-file-form.component';

// translate: AoT requires an exported function for factories
export function httpLoaderFactory(httpClient: HttpClient) {
@@ -172,6 +177,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
AppComponent,
ArchiveComponent,
AudioComponent,
AvTimelineComponent,
BoardComponent,
BooleanValueComponent,
CollaborationComponent,
@@ -190,6 +196,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
DecimalValueComponent,
DialogComponent,
DialogHeaderComponent,
DisableContextMenuDirective,
DisplayEditComponent,
DocumentComponent,
DragDropDirective,
@@ -241,6 +248,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
PropertiesComponent,
PropertyFormComponent,
PropertyInfoComponent,
ReplaceFileFormComponent,
ResourceAndPropertySelectionComponent,
ResourceClassFormComponent,
ResourceClassInfoComponent,
@@ -285,6 +293,7 @@ export function httpLoaderFactory(httpClient: HttpClient) {
TextValueAsXMLComponent,
TextValueHtmlLinkDirective,
TimeInputComponent,
TimePipe,
TimeValueComponent,
TitleFromCamelCasePipe,
TruncatePipe,
@@ -295,7 +304,8 @@ export function httpLoaderFactory(httpClient: HttpClient) {
UserMenuComponent,
UsersComponent,
UsersListComponent,
ReplaceFileFormComponent,
VideoComponent,
VideoPreviewComponent
],
imports: [
AngularSplitModule.forRoot(),
@@ -394,7 +394,7 @@

<div *ngSwitchCase="'replaceFile'">
<app-dialog-header [title]="data.title" [subtitle]="data.subtitle"></app-dialog-header>
<mat-dialog-content>
<mat-dialog-content class="form-content">
<app-replace-file-form [representation]="data.representation" [propId]="data.id" (closeDialog)="dialogRef.close($event)"></app-replace-file-form>
</mat-dialog-content>
</div>
@@ -0,0 +1,8 @@
import { DisableContextMenuDirective } from './disable-context-menu.directive';

describe('DisableContextMenuDirective', () => {
it('should create an instance', () => {
const directive = new DisableContextMenuDirective();
expect(directive).toBeTruthy();
});
});
@@ -0,0 +1,17 @@
import { Directive, HostListener } from '@angular/core';

@Directive({
selector: '[appDisableContextMenu]'
})
export class DisableContextMenuDirective {

constructor() { }

@HostListener('contextmenu', ['$event'])

onRightClick(event: Event) {
event.preventDefault();
}


}
@@ -0,0 +1,27 @@
import { TimePipe } from './time.pipe';

describe('TimePipe', () => {

let pipe = new TimePipe();

beforeEach(() => {
pipe = new TimePipe();
});

it('create an instance', () => {
expect(pipe).toBeTruthy();
});


it('should convert 123 seconds into 2 minutes and 3 seconds', () => {
const seconds = 123;
const time = pipe.transform(seconds);
expect(time).toEqual('02:03');
});

it('should convert 12342 seconds into 3 hours 25 minutes and 42 seconds', () => {
const seconds = 12342;
const time = pipe.transform(seconds);
expect(time).toEqual('03:25:42');
});
});
@@ -0,0 +1,30 @@
import { Pipe, PipeTransform } from '@angular/core';

/**
* the TimePipe transforms n seconds to hh:mm:ss
* or in case of zero hours to mm:ss
*/
@Pipe({
name: 'appTime'
})
export class TimePipe implements PipeTransform {

transform(value: number): string {

const dateObj: Date = new Date(value * 1000);
const hours: number = dateObj.getUTCHours();
const minutes = dateObj.getUTCMinutes();
const seconds = dateObj.getSeconds();

if (hours === 0) {
return minutes.toString().padStart(2, '0') + ':' +
seconds.toString().padStart(2, '0');
} else {
return hours.toString().padStart(2, '0') + ':' +
minutes.toString().padStart(2, '0') + ':' +
seconds.toString().padStart(2, '0');
}

}

}
@@ -16,7 +16,7 @@ export class NotificationService {
// todo: maybe we can add more parameters like:
// action: string = 'x', duration: number = 4200
// and / or type: 'note' | 'warning' | 'error' | 'success'; which can be used for the panelClass
openSnackBar(notification: string | ApiResponseError): void {
openSnackBar(notification: string | ApiResponseError, type?: 'success' | 'error'): void {
let duration = 5000;
let message: string;
let panelClass: string;
@@ -30,10 +30,10 @@ export class NotificationService {
const defaultStatusMsg = this._statusMsg.default;
message = `${defaultStatusMsg[notification.status].message} (${notification.status}): ${defaultStatusMsg[notification.status].description}`;
}
panelClass = 'error';
panelClass = type ? type : 'error';
} else {
message = notification;
panelClass = 'success';
panelClass = type ? type : 'success';
}

this._snackBar.open(message, 'x', {
@@ -20,7 +20,6 @@ export class ArchiveComponent implements OnInit {
@Input() parentResource: ReadResource;

originalFilename: string;
temp: string;

constructor(
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
@@ -79,7 +78,9 @@ export class ArchiveComponent implements OnInit {
);

dialogRef.afterClosed().subscribe((data) => {
this._replaceFile(data);
if (data) {
this._replaceFile(data);
}
});
}

@@ -52,7 +52,9 @@ export class AudioComponent implements OnInit {
);

dialogRef.afterClosed().subscribe((data) => {
this._replaceFile(data);
if (data) {
this._replaceFile(data);
}
});
}

@@ -0,0 +1,10 @@
<div class="timeline-wrapper" #timeline>
<div class="progress-wrapper" #progress>
<div class="progress-background"></div>
<div class="progress-buffer"></div>
<div class="progress-fill"></div>
</div>
<div class="thumb" [class.dragging]="dragging" #thumb cdkDragLockAxis="x" cdkDrag
(cdkDragStarted)="toggleDragging()" (cdkDragEnded)="toggleDragging()" (cdkDragMoved)="dragAction($event)"
cdkDragBoundary=".timeline-wrapper"> </div>
</div>
@@ -0,0 +1,63 @@
// timeline / progress bar
.timeline-wrapper {
width: calc(100% - 32px);
height: 24px;
top: 0;
position: absolute;
cursor: pointer;

.progress-wrapper {
width: 100%;
height: 2px;
padding: 11px 0;
position: absolute;
overflow: hidden;
display: flex;

.progress-background,
// .progress-buffer,
.progress-fill {
height: 2px;
width: 100%;
}
.progress-background {
background-color: whitesmoke;
position: absolute;
transform-origin: 100% 100%;
transition: transform 20ms cubic-bezier(0.25, 0.8, 0.25, 1),
background-color 20ms cubic-bezier(0.25, 0.8, 0.25, 1);
}

.progress-fill {
background-color: red;
position: absolute;
transform: scaleX(0);
transform-origin: 0 0;
transition: transform 20ms cubic-bezier(0.25, 0.8, 0.25, 1),
background-color 20ms cubic-bezier(0.25, 0.8, 0.25, 1);
}
}

.thumb {
cursor: grab;
position: absolute;
left: -7px;
bottom: 2px;
box-sizing: border-box;
width: 20px;
height: 20px;
border: 3px solid transparent;
border-radius: 50%;
background-color: red;
transform: scale(0.7);
transition: transform 20ms cubic-bezier(0.25, 0.8, 0.25, 1),
background-color 20ms cubic-bezier(0.25, 0.8, 0.25, 1),
border-color 20ms cubic-bezier(0.25, 0.8, 0.25, 1);
transition: none;

&.dragging {
cursor: grabbing;
border: 3px solid red;
}
}
}
@@ -0,0 +1,27 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { AvTimelineComponent } from './av-timeline.component';

describe('AvTimelineComponent', () => {
let component: AvTimelineComponent;
let fixture: ComponentFixture<AvTimelineComponent>;

beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AvTimelineComponent
]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(AvTimelineComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

0 comments on commit 86fc2a7

Please sign in to comment.