Request-a-copy improv: Secure media and image viewers

This commit is contained in:
Kim Shepherd
2025-02-13 14:56:49 +01:00
parent 5a53cc9738
commit aea41d74ec
12 changed files with 910 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<ng-container *ngVar="mediaList$ | async as mediaList">
<ds-loading
*ngIf="isLoading"
message="{{ 'loading.default' | translate }}"
[showMessage]="false"
></ds-loading>
<div class="media-viewer" *ngIf="!isLoading">
<ng-container *ngIf="mediaList.length > 0; else showThumbnail">
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
<ng-container *ngVar="mediaOptions.image && mediaList[0]?.format === 'image' as showImage">
<ds-secure-media-viewer-video *ngIf="showVideo"
[medias]="mediaList"
[captions]="captions$ | async"
[accessToken]="accessToken"
></ds-secure-media-viewer-video>
<ds-secure-media-viewer-image *ngIf="showImage"
[images]="mediaList"
[accessToken]="accessToken"
></ds-secure-media-viewer-image>
<ng-container *ngIf="showImage || showVideo; else showThumbnail"></ng-container>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #showThumbnail>
<ds-secure-media-viewer-image *ngIf="mediaOptions.image && mediaOptions.video"
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder"
[accessToken]="accessToken"
[preview]="false"
></ds-secure-media-viewer-image>
<ds-thumbnail *ngIf="!(mediaOptions.image && mediaOptions.video)"
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
</ds-thumbnail>
</ng-template>
</ng-container>

View File

@@ -0,0 +1,160 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../../../core/auth/auth.service';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../../core/shared/media-viewer-item.model';
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock';
import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock';
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../../shared/testing/utils.test';
import { ThemeService } from '../../../../shared/theme-support/theme.service';
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { ItemSecureMediaViewerComponent } from './item-secure-media-viewer.component';
describe('ItemSecureMediaViewerComponent', () => {
let comp: ItemSecureMediaViewerComponent;
let fixture: ComponentFixture<ItemSecureMediaViewerComponent>;
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(
createPaginatedList([mockBitstream]),
),
});
const mockMediaViewerItem: MediaViewerItem = Object.assign(
new MediaViewerItem(),
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
);
beforeEach(waitForAsync(() => {
return TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
BrowserAnimationsModule,
ItemSecureMediaViewerComponent,
VarDirective,
FileSizePipe,
MetadataFieldWrapperComponent,
],
providers: [
{ provide: BitstreamDataService, useValue: bitstreamDataService },
{ provide: ThemeService, useValue: getMockThemeService() },
{ provide: AuthService, useValue: new AuthServiceMock() },
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ItemSecureMediaViewerComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the bitstreams are loading', () => {
beforeEach(() => {
comp.mediaList$.next([mockMediaViewerItem]);
comp.mediaOptions = {
image: true,
video: true,
};
comp.isLoading = true;
fixture.detectChanges();
});
it('should call the createMediaViewerItem', () => {
const mediaItem = comp.createMediaViewerItem(
mockBitstream,
MockBitstreamFormat1,
undefined,
);
expect(mediaItem).toBeTruthy();
expect(mediaItem.thumbnail).toBe(null);
});
it('should display a loading component', () => {
const loading = fixture.debugElement.query(By.css('ds-loading'));
expect(loading.nativeElement).toBeDefined();
});
});
describe('when the bitstreams loading is failed', () => {
beforeEach(() => {
comp.mediaList$.next([]);
comp.mediaOptions = {
image: true,
video: true,
};
comp.isLoading = false;
fixture.detectChanges();
});
it('should call the createMediaViewerItem', () => {
const mediaItem = comp.createMediaViewerItem(
mockBitstream,
MockBitstreamFormat1,
undefined,
);
expect(mediaItem).toBeTruthy();
expect(mediaItem.thumbnail).toBe(null);
});
it('should display a default, thumbnail', () => {
const defaultThumbnail = fixture.debugElement.query(
By.css('ds-secure-media-viewer-image'),
);
expect(defaultThumbnail.nativeElement).toBeDefined();
});
});
});

View File

@@ -0,0 +1,171 @@
import {
AsyncPipe,
NgIf,
} from '@angular/common';
import {
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
} from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import {
BehaviorSubject,
Observable,
Subscription,
} from 'rxjs';
import {
filter,
take,
} from 'rxjs/operators';
import { MediaViewerConfig } from '../../../../../config/media-viewer-config.interface';
import { environment } from '../../../../../environments/environment';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
import { Item } from '../../../../core/shared/item.model';
import { MediaViewerItem } from '../../../../core/shared/media-viewer-item.model';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { hasValue } from '../../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
import { SecureMediaViewerImageComponent } from './media-viewer-image/secure-media-viewer-image.component';
import { SecureMediaViewerVideoComponent } from './media-viewer-video/secure-media-viewer-video.component';
/**
* This component renders the media viewers
*/
@Component({
selector: 'ds-item-secure-media-viewer',
templateUrl: './item-secure-media-viewer.component.html',
styleUrls: ['./item-secure-media-viewer.component.scss'],
imports: [
AsyncPipe,
NgIf,
TranslateModule,
ThemedLoadingComponent,
VarDirective,
SecureMediaViewerVideoComponent,
SecureMediaViewerImageComponent,
ThemedThumbnailComponent,
],
standalone: true,
})
export class ItemSecureMediaViewerComponent implements OnDestroy, OnInit {
@Input() item: Item;
@Input() mediaOptions: MediaViewerConfig = environment.mediaViewer;
@Input() accessToken: string;
mediaList$: BehaviorSubject<MediaViewerItem[]> = new BehaviorSubject([]);
captions$: BehaviorSubject<Bitstream[]> = new BehaviorSubject([]);
isLoading = true;
thumbnailPlaceholder = './assets/images/replacement_document.svg';
thumbnailsRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
subs: Subscription[] = [];
constructor(
protected bitstreamDataService: BitstreamDataService,
protected changeDetectorRef: ChangeDetectorRef,
) {
}
ngOnDestroy(): void {
this.subs.forEach((subscription: Subscription) => subscription.unsubscribe());
}
/**
* This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s
*/
ngOnInit(): void {
const types: string[] = [
...(this.mediaOptions.image ? ['image'] : []),
...(this.mediaOptions.video ? ['audio', 'video'] : []),
];
this.thumbnailsRD$ = this.loadRemoteData('THUMBNAIL');
this.subs.push(this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => {
if (bitstreamsRD.payload.page.length === 0) {
this.isLoading = false;
this.mediaList$.next([]);
} else {
this.subs.push(this.thumbnailsRD$.subscribe((thumbnailsRD: RemoteData<PaginatedList<Bitstream>>) => {
for (
let index = 0;
index < bitstreamsRD.payload.page.length;
index++
) {
this.subs.push(bitstreamsRD.payload.page[index].format
.pipe(getFirstSucceededRemoteDataPayload())
.subscribe((format: BitstreamFormat) => {
const mediaItem = this.createMediaViewerItem(
bitstreamsRD.payload.page[index],
format,
thumbnailsRD.payload && thumbnailsRD.payload.page[index],
);
if (types.includes(mediaItem.format)) {
this.mediaList$.next([...this.mediaList$.getValue(), mediaItem]);
} else if (format.mimetype === 'text/vtt') {
this.captions$.next([...this.captions$.getValue(), bitstreamsRD.payload.page[index]]);
}
}));
}
this.isLoading = false;
this.changeDetectorRef.detectChanges();
}));
}
}));
}
/**
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
* @param bundleName Bundle name
*/
loadRemoteData(
bundleName: string,
): Observable<RemoteData<PaginatedList<Bitstream>>> {
return this.bitstreamDataService
.findAllByItemAndBundleName(
this.item,
bundleName,
{},
true,
true,
followLink('format'),
)
.pipe(
filter(
(bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) =>
hasValue(bitstreamsRD) &&
(hasValue(bitstreamsRD.errorMessage) || hasValue(bitstreamsRD.payload)),
),
take(1),
);
}
/**
* This method creates a {@link MediaViewerItem} from incoming {@link Bitstream}s
* @param original original bitstream
* @param format original bitstream format
* @param thumbnail thumbnail bitstream
*/
createMediaViewerItem(original: Bitstream, format: BitstreamFormat, thumbnail: Bitstream): MediaViewerItem {
const mediaItem = new MediaViewerItem();
mediaItem.bitstream = original;
mediaItem.format = format.mimetype.split('/')[0];
mediaItem.mimetype = format.mimetype;
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
return mediaItem;
}
}

View File

@@ -0,0 +1,7 @@
<div [class.change-gallery]="isAuthenticated$ | async">
<ngx-gallery
class="ngx-gallery"
[options]="galleryOptions"
[images]="galleryImages"
></ngx-gallery>
</div>

View File

@@ -0,0 +1,20 @@
:host ::ng-deep {
.ngx-gallery {
width: unset !important;
height: unset !important;
}
ngx-gallery-image {
max-width: 340px !important;
.ngx-gallery-image {
background-position: left;
}
}
ngx-gallery-image:after {
padding-top: 75%;
display: block;
content: '';
}
}

View File

@@ -0,0 +1,91 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
import { of as observableOf } from 'rxjs';
import { AuthService } from '../../../../../core/auth/auth.service';
import { Bitstream } from '../../../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
import { MockBitstreamFormat1 } from '../../../../../shared/mocks/item.mock';
import { SecureMediaViewerImageComponent } from './secure-media-viewer-image.component';
describe('ItemSecureMediaViewerImageComponent', () => {
let component: SecureMediaViewerImageComponent;
let fixture: ComponentFixture<SecureMediaViewerImageComponent>;
const authService = jasmine.createSpyObj('authService', {
isAuthenticated: observableOf(false),
});
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
],
);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [SecureMediaViewerImageComponent],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: AuthService, useValue: authService },
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SecureMediaViewerImageComponent);
component = fixture.componentInstance;
component.galleryOptions = [new NgxGalleryOptions({})];
component.galleryImages = component.convertToGalleryImage(
mockMediaViewerItems,
);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should contain a gallery options', () => {
expect(component.galleryOptions.length).toBeGreaterThan(0);
});
it('should contain an image array', () => {
expect(component.galleryImages.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,109 @@
import { AsyncPipe } from '@angular/common';
import {
Component,
Input,
OnChanges,
OnInit,
} from '@angular/core';
import {
NgxGalleryAnimation,
NgxGalleryImage,
NgxGalleryModule,
NgxGalleryOptions,
} from '@kolkov/ngx-gallery';
import { Observable } from 'rxjs';
import { AuthService } from '../../../../../core/auth/auth.service';
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
/**
* This componenet render an image gallery for the image viewer
*/
@Component({
selector: 'ds-secure-media-viewer-image',
templateUrl: './secure-media-viewer-image.component.html',
styleUrls: ['./secure-media-viewer-image.component.scss'],
imports: [
NgxGalleryModule,
AsyncPipe,
],
standalone: true,
})
export class SecureMediaViewerImageComponent implements OnChanges, OnInit {
@Input() images: MediaViewerItem[];
@Input() preview?: boolean;
@Input() image?: string;
@Input() accessToken: string;
thumbnailPlaceholder = './assets/images/replacement_image.svg';
galleryOptions: NgxGalleryOptions[] = [];
galleryImages: NgxGalleryImage[] = [];
/**
* Whether or not the current user is authenticated
*/
isAuthenticated$: Observable<boolean>;
constructor(
protected authService: AuthService,
) {
}
ngOnChanges(): void {
this.galleryOptions = [
{
preview: this.preview !== undefined ? this.preview : true,
image: true,
imageSize: 'contain',
thumbnails: false,
imageArrows: false,
startIndex: 0,
imageAnimation: NgxGalleryAnimation.Slide,
previewCloseOnEsc: true,
previewZoom: true,
previewRotate: true,
previewFullscreen: true,
},
];
if (this.image) {
this.galleryImages = [
{
small: this.image,
medium: this.image,
big: this.image,
},
];
} else {
this.galleryImages = this.convertToGalleryImage(this.images);
}
}
ngOnInit(): void {
this.isAuthenticated$ = this.authService.isAuthenticated();
this.ngOnChanges();
}
/**
* This method convert an array of MediaViewerItem into NgxGalleryImage array
* @param medias input NgxGalleryImage array
*/
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
const mappedImages = [];
for (const image of medias) {
if (image.format === 'image') {
mappedImages.push({
small: image.thumbnail
? image.thumbnail
: this.thumbnailPlaceholder,
medium: image.thumbnail
? image.thumbnail
: this.thumbnailPlaceholder,
big: image.bitstream._links.content.href + '?accessToken=' + this.accessToken,
});
}
}
return mappedImages;
}
}

View File

@@ -0,0 +1,54 @@
<video
crossorigin="anonymous"
[src]="medias[currentIndex].bitstream._links.content.href + '?accessToken=' + accessToken"
id="singleVideo"
[poster]="
medias[currentIndex].thumbnail ||
replacements[medias[currentIndex].format]
"
preload="none"
controls
>
<ng-container *ngIf="getMediaCap(medias[currentIndex].bitstream.name, captions) as capInfos">
<ng-container *ngFor="let capInfo of capInfos">
<track [src]="capInfo.src" [label]="capInfo.langLabel" [srclang]="capInfo.srclang" />
</ng-container>
</ng-container>
</video>
<div class="buttons" *ngIf="medias?.length > 1">
<button
class="btn btn-primary previous"
[dsBtnDisabled]="currentIndex === 0"
(click)="prevMedia()"
>
{{ "media-viewer.previous" | translate }}
</button>
<button
class="btn btn-primary next"
[dsBtnDisabled]="currentIndex === medias.length - 1"
(click)="nextMedia()"
>
{{ "media-viewer.next" | translate }}
</button>
<div ngbDropdown class="d-inline-block">
<button
class="btn btn-outline-primary playlist"
id="dropdownBasic1"
ngbDropdownToggle
>
{{ "media-viewer.playlist" | translate }}
</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button
ngbDropdownItem
*ngFor="let item of medias; index as indexOfelement"
class="list-element"
(click)="selectedMedia(indexOfelement)"
>
{{ dsoNameService.getName(item.bitstream) }}
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,10 @@
video {
width: 100%;
height: auto;
max-width: 340px;
}
.buttons {
display: flex;
gap: .25rem;
}

View File

@@ -0,0 +1,149 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
TestBed,
waitForAsync,
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { Bitstream } from '../../../../../core/shared/bitstream.model';
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
import { MetadataFieldWrapperComponent } from '../../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
import { MockBitstreamFormat1 } from '../../../../../shared/mocks/item.mock';
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe';
import { VarDirective } from '../../../../../shared/utils/var.directive';
import { SecureMediaViewerVideoComponent } from './secure-media-viewer-video.component';
describe('SecureMediaViewerVideoComponent', () => {
let component: SecureMediaViewerVideoComponent;
let fixture: ComponentFixture<SecureMediaViewerVideoComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
BrowserAnimationsModule,
SecureMediaViewerVideoComponent,
VarDirective,
FileSizePipe,
MetadataFieldWrapperComponent,
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
sizeBytes: 10201,
content:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
format: observableOf(MockBitstreamFormat1),
bundleName: 'ORIGINAL',
_links: {
self: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
},
content: {
href:
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
},
},
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: {
'dc.title': [
{
language: null,
value: 'test_word.docx',
},
],
},
});
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
],
);
const mockMediaViewerItem: MediaViewerItem[] = Object.assign(
new Array<MediaViewerItem>(),
[{ bitstream: mockBitstream, format: 'video', thumbnail: null }],
);
beforeEach(() => {
fixture = TestBed.createComponent(SecureMediaViewerVideoComponent);
component = fixture.componentInstance;
component.medias = mockMediaViewerItem;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('should show controller buttons when the having mode then one video', () => {
beforeEach(() => {
component.medias = mockMediaViewerItems;
fixture.detectChanges();
});
it('should show buttons', () => {
const controllerButtons = fixture.debugElement.query(By.css('.buttons'));
expect(controllerButtons).toBeTruthy();
});
describe('when the "Next" button is clicked', () => {
beforeEach(() => {
component.currentIndex = 0;
fixture.detectChanges();
});
it('should increase the index', () => {
const viewMore = fixture.debugElement.query(By.css('.next'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(1);
});
});
describe('when the "Previous" button is clicked', () => {
beforeEach(() => {
component.currentIndex = 1;
fixture.detectChanges();
});
it('should decrease the index', () => {
const viewMore = fixture.debugElement.query(By.css('.previous'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(0);
});
});
describe('when the "Playlist element" button is clicked', () => {
beforeEach(() => {
component.isCollapsed = true;
fixture.detectChanges();
});
it('should set the the index with the selected one', () => {
const viewMore = fixture.debugElement.query(By.css('.list-element'));
viewMore.triggerEventHandler('click', null);
expect(component.currentIndex).toBe(0);
});
});
});
});

View File

@@ -0,0 +1,103 @@
import {
NgForOf,
NgIf,
} from '@angular/common';
import {
Component,
Input,
} from '@angular/core';
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { Bitstream } from 'src/app/core/shared/bitstream.model';
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
import { BtnDisabledDirective } from '../../../../../shared/btn-disabled.directive';
import { CaptionInfo } from '../../../../media-viewer/media-viewer-video/caption-info';
import { languageHelper } from '../../../../media-viewer/media-viewer-video/language-helper';
/**
* This component renders a video viewer and playlist for the media viewer
*/
@Component({
selector: 'ds-secure-media-viewer-video',
templateUrl: './secure-media-viewer-video.component.html',
styleUrls: ['./secure-media-viewer-video.component.scss'],
imports: [
NgForOf,
NgbDropdownModule,
TranslateModule,
NgIf,
BtnDisabledDirective,
],
standalone: true,
})
export class SecureMediaViewerVideoComponent {
@Input() medias: MediaViewerItem[];
@Input() captions: Bitstream[] = [];
@Input() accessToken: string;
isCollapsed = false;
currentIndex = 0;
replacements = {
video: './assets/images/replacement_video.svg',
audio: './assets/images/replacement_audio.svg',
};
constructor(
public dsoNameService: DSONameService,
) {
}
/**
* This method check if there is caption file for the media
* The caption file name is the media name plus "-" following two letter
* language code and .vtt suffix
*
* html5 video only support WEBVTT format
*
* Two letter language code reference
* https://www.w3schools.com/tags/ref_language_codes.asp
*/
getMediaCap(name: string, captions: Bitstream[]): CaptionInfo[] {
const capInfos: CaptionInfo[] = [];
const filteredCapMedias: Bitstream[] = captions
.filter((media: Bitstream) => media.name.substring(0, (media.name.length - 7)).toLowerCase() === name.toLowerCase());
for (const media of filteredCapMedias) {
const srclang: string = media.name.slice(-6, -4).toLowerCase();
capInfos.push(new CaptionInfo(
media._links.content.href + '?accessToken=' + this.accessToken,
srclang,
languageHelper[srclang],
));
}
return capInfos;
}
/**
* This method sets the received index into currentIndex
* @param index Selected index
*/
selectedMedia(index: number) {
this.currentIndex = index;
}
/**
* This method increases the number of the currentIndex
*/
nextMedia() {
this.currentIndex++;
}
/**
* This method decreases the number of the currentIndex
*/
prevMedia() {
this.currentIndex--;
}
}