diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.html b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.html new file mode 100644 index 0000000000..6acb4299ee --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.html @@ -0,0 +1,35 @@ + + + + 0; else showThumbnail"> + + + + + + + + + + + + + + + diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.scss b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.spec.ts b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.spec.ts new file mode 100644 index 0000000000..afc77ddb37 --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.spec.ts @@ -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; + + 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(); + }); + }); +}); diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.ts b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.ts new file mode 100644 index 0000000000..fb06d0ec95 --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/item-secure-media-viewer.component.ts @@ -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 = new BehaviorSubject([]); + + captions$: BehaviorSubject = new BehaviorSubject([]); + + isLoading = true; + + thumbnailPlaceholder = './assets/images/replacement_document.svg'; + + thumbnailsRD$: Observable>>; + + 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>) => { + if (bitstreamsRD.payload.page.length === 0) { + this.isLoading = false; + this.mediaList$.next([]); + } else { + this.subs.push(this.thumbnailsRD$.subscribe((thumbnailsRD: RemoteData>) => { + 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>> { + return this.bitstreamDataService + .findAllByItemAndBundleName( + this.item, + bundleName, + {}, + true, + true, + followLink('format'), + ) + .pipe( + filter( + (bitstreamsRD: RemoteData>) => + 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; + } +} diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.html b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.html new file mode 100644 index 0000000000..bafc6f079c --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.html @@ -0,0 +1,7 @@ + + + diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.scss b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.scss new file mode 100644 index 0000000000..cba963b6fa --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.scss @@ -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: ''; + } +} diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.spec.ts b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.spec.ts new file mode 100644 index 0000000000..536c073e41 --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.spec.ts @@ -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; + + 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(), + [ + { 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); + }); +}); diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.ts b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.ts new file mode 100644 index 0000000000..c6c9cec24a --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-image/secure-media-viewer-image.component.ts @@ -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; + + 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; + } +} diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.html b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.html new file mode 100644 index 0000000000..8a6a447d9e --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.html @@ -0,0 +1,54 @@ + + + + + + + + + 1"> + + {{ "media-viewer.previous" | translate }} + + + + {{ "media-viewer.next" | translate }} + + + + {{ "media-viewer.playlist" | translate }} + + + + {{ dsoNameService.getName(item.bitstream) }} + + + + diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.scss b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.scss new file mode 100644 index 0000000000..bb8b9d360e --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.scss @@ -0,0 +1,10 @@ +video { + width: 100%; + height: auto; + max-width: 340px; +} + +.buttons { + display: flex; + gap: .25rem; +} diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.spec.ts b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.spec.ts new file mode 100644 index 0000000000..1e3282641c --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.spec.ts @@ -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; + + 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(), + [ + { bitstream: mockBitstream, format: 'video', thumbnail: null }, + { bitstream: mockBitstream, format: 'video', thumbnail: null }, + ], + ); + const mockMediaViewerItem: MediaViewerItem[] = Object.assign( + new Array(), + [{ 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); + }); + }); + }); +}); diff --git a/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.ts b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.ts new file mode 100644 index 0000000000..75f597c8a2 --- /dev/null +++ b/src/app/item-page/access-by-token/field-components/media-viewer/media-viewer-video/secure-media-viewer-video.component.ts @@ -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--; + } +}