mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 07:23:03 +00:00
Request-a-copy improv: Secure media and image viewers
This commit is contained in:
@@ -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>
|
@@ -0,0 +1 @@
|
|||||||
|
|
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<div [class.change-gallery]="isAuthenticated$ | async">
|
||||||
|
<ngx-gallery
|
||||||
|
class="ngx-gallery"
|
||||||
|
[options]="galleryOptions"
|
||||||
|
[images]="galleryImages"
|
||||||
|
></ngx-gallery>
|
||||||
|
</div>
|
@@ -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: '';
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
@@ -0,0 +1,10 @@
|
|||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: .25rem;
|
||||||
|
}
|
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -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--;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user