diff --git a/package.json b/package.json
index 3aa7c04ad3..8c416ee4e1 100644
--- a/package.json
+++ b/package.json
@@ -122,7 +122,8 @@
"sortablejs": "1.10.1",
"tslib": "^2.0.0",
"webfontloader": "1.6.28",
- "zone.js": "^0.10.3"
+ "zone.js": "^0.10.3",
+ "@kolkov/ngx-gallery": "^1.2.3"
},
"devDependencies": {
"@angular-builders/custom-webpack": "10.0.1",
diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts
index af95411cef..1c6cd83e9c 100644
--- a/src/app/+item-page/item-page.module.ts
+++ b/src/app/+item-page/item-page.module.ts
@@ -27,6 +27,10 @@ import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal
import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module';
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
+import { MediaViewerComponent } from './media-viewer/media-viewer.component';
+import { MediaViewerVideoComponent } from './media-viewer/media-viewer-video/media-viewer-video.component';
+import { MediaViewerImageComponent } from './media-viewer/media-viewer-image/media-viewer-image.component';
+import { NgxGalleryModule } from '@kolkov/ngx-gallery';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -54,6 +58,9 @@ const DECLARATIONS = [
ItemComponent,
UploadBitstreamComponent,
AbstractIncrementalListComponent,
+ MediaViewerComponent,
+ MediaViewerVideoComponent,
+ MediaViewerImageComponent
];
@NgModule({
@@ -64,7 +71,8 @@ const DECLARATIONS = [
EditItemPageModule,
StatisticsModule.forRoot(),
JournalEntitiesModule.withEntryComponents(),
- ResearchEntitiesModule.withEntryComponents()
+ ResearchEntitiesModule.withEntryComponents(),
+ NgxGalleryModule,
],
declarations: [
...DECLARATIONS
diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html
new file mode 100644
index 0000000000..bafc6f079c
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss
new file mode 100644
index 0000000000..72ce4b04d9
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.scss
@@ -0,0 +1,6 @@
+.ngx-gallery {
+ display: inline-block;
+ margin-bottom: 20px;
+ width: 340px !important;
+ height: 279px !important;
+}
diff --git a/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts
new file mode 100644
index 0000000000..1f1fed789f
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts
@@ -0,0 +1,89 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
+import { Bitstream } from '../../../core/shared/bitstream.model';
+import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
+import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
+
+import { MediaViewerImageComponent } from './media-viewer-image.component';
+
+import { of as observableOf } from 'rxjs';
+import { AuthService } from '../../../core/auth/auth.service';
+
+describe('MediaViewerImageComponent', () => {
+ let component: MediaViewerImageComponent;
+ 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(async(() => {
+ TestBed.configureTestingModule({
+ imports:[],
+ declarations: [MediaViewerImageComponent],
+ schemas: [NO_ERRORS_SCHEMA],
+ providers: [
+ { provide: AuthService, useValue: authService },
+ ],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MediaViewerImageComponent);
+ 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/media-viewer/media-viewer-image/media-viewer-image.component.ts b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts
new file mode 100644
index 0000000000..0c32b5603d
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts
@@ -0,0 +1,88 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery';
+import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
+import { NgxGalleryAnimation } from '@kolkov/ngx-gallery';
+import { Observable } from 'rxjs';
+import { AuthService } from '../../../core/auth/auth.service';
+
+/**
+ * This componenet render an image gallery for the image viewer
+ */
+@Component({
+ selector: 'ds-media-viewer-image',
+ templateUrl: './media-viewer-image.component.html',
+ styleUrls: ['./media-viewer-image.component.scss'],
+})
+export class MediaViewerImageComponent implements OnInit {
+ @Input() images: MediaViewerItem[];
+ @Input() preview?: boolean;
+ @Input() image?: string;
+
+ loggedin: boolean;
+
+ galleryOptions: NgxGalleryOptions[];
+ galleryImages: NgxGalleryImage[];
+
+ /**
+ * Whether or not the current user is authenticated
+ */
+ isAuthenticated$: Observable;
+
+ constructor(private authService: AuthService) {}
+
+ /**
+ * Thi method sets up the gallery settings and data
+ */
+ ngOnInit(): void {
+ this.isAuthenticated$ = this.authService.isAuthenticated();
+ 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);
+ }
+ }
+
+ /**
+ * This method convert an array of MediaViewerItem into NgxGalleryImage array
+ * @param medias input NgxGalleryImage array
+ */
+ convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
+ const mappadImages = [];
+ for (const image of medias) {
+ if (image.format === 'image') {
+ mappadImages.push({
+ small: image.thumbnail
+ ? image.thumbnail
+ : './assets/images/replacement_image.svg',
+ medium: image.thumbnail
+ ? image.thumbnail
+ : './assets/images/replacement_image.svg',
+ big: image.bitstream._links.content.href,
+ });
+ }
+ }
+ return mappadImages;
+ }
+}
diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html
new file mode 100644
index 0000000000..a4493e36fc
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html
@@ -0,0 +1,47 @@
+
+
diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss
new file mode 100644
index 0000000000..7702da7361
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.scss
@@ -0,0 +1,4 @@
+video {
+ width: 340px;
+ height: 279px;
+}
diff --git a/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts
new file mode 100644
index 0000000000..88138a252f
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts
@@ -0,0 +1,145 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+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 { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
+import { FileSizePipe } from '../../../shared/utils/file-size-pipe';
+import { VarDirective } from '../../../shared/utils/var.directive';
+import { MetadataFieldWrapperComponent } from '../../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
+import { MockBitstreamFormat1 } from '../../../shared/mocks/item.mock';
+import { MediaViewerVideoComponent } from './media-viewer-video.component';
+import { By } from '@angular/platform-browser';
+
+describe('MediaViewerVideoComponent', () => {
+ let component: MediaViewerVideoComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock,
+ },
+ }),
+ BrowserAnimationsModule,
+ ],
+ declarations: [
+ MediaViewerVideoComponent,
+ 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(MediaViewerVideoComponent);
+ component = fixture.componentInstance;
+ component.medias = mockMediaViewerItem;
+ component.filteredMedias = 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;
+ component.filteredMedias = 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/media-viewer/media-viewer-video/media-viewer-video.component.ts b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts
new file mode 100644
index 0000000000..4c578a51bb
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts
@@ -0,0 +1,55 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
+
+/**
+ * This componenet renders a video viewer and playlist for the media viewer
+ */
+@Component({
+ selector: 'ds-media-viewer-video',
+ templateUrl: './media-viewer-video.component.html',
+ styleUrls: ['./media-viewer-video.component.scss'],
+})
+export class MediaViewerVideoComponent implements OnInit {
+ @Input() medias: MediaViewerItem[];
+
+ filteredMedias: MediaViewerItem[];
+
+ isCollapsed: boolean;
+ currentIndex = 0;
+
+ replacements = {
+ video: './assets/images/replacement_video.svg',
+ audio: './assets/images/replacement_audio.svg',
+ };
+
+ replacementThumbnail: string;
+
+ ngOnInit() {
+ this.isCollapsed = false;
+ this.filteredMedias = this.medias.filter(
+ (media) => media.format === 'audio' || media.format === 'video'
+ );
+ }
+
+ /**
+ * This method sets the reviced index into currentIndex
+ * @param index Selected index
+ */
+ selectedMedia(index: number) {
+ this.currentIndex = index;
+ }
+
+ /**
+ * This method increade the number of the currentIndex
+ */
+ nextMedia() {
+ this.currentIndex++;
+ }
+
+ /**
+ * This method decrese the number of the currentIndex
+ */
+ prevMedia() {
+ this.currentIndex--;
+ }
+}
diff --git a/src/app/+item-page/media-viewer/media-viewer.component.html b/src/app/+item-page/media-viewer/media-viewer.component.html
new file mode 100644
index 0000000000..b79b91629f
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer.component.html
@@ -0,0 +1,36 @@
+
+
+
+ 0">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/media-viewer/media-viewer.component.scss b/src/app/+item-page/media-viewer/media-viewer.component.scss
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer.component.scss
@@ -0,0 +1 @@
+
diff --git a/src/app/+item-page/media-viewer/media-viewer.component.spec.ts b/src/app/+item-page/media-viewer/media-viewer.component.spec.ts
new file mode 100644
index 0000000000..ebea703ec8
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer.component.spec.ts
@@ -0,0 +1,143 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Bitstream } from '../../core/shared/bitstream.model';
+import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
+import { createPaginatedList } from '../../shared/testing/utils.test';
+import { of as observableOf } from 'rxjs';
+import { By } from '@angular/platform-browser';
+import { MediaViewerComponent } from './media-viewer.component';
+import { MockBitstreamFormat1 } from '../../shared/mocks/item.mock';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { BitstreamDataService } from '../../core/data/bitstream-data.service';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
+import { VarDirective } from '../../shared/utils/var.directive';
+import { MetadataFieldWrapperComponent } from '../field-components/metadata-field-wrapper/metadata-field-wrapper.component';
+import { FileSizePipe } from '../../shared/utils/file-size-pipe';
+
+describe('MediaViewerComponent', () => {
+ let comp: MediaViewerComponent;
+ 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(async(() => {
+ TestBed.configureTestingModule({
+ imports: [
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useClass: TranslateLoaderMock,
+ },
+ }),
+ BrowserAnimationsModule,
+ ],
+ declarations: [
+ MediaViewerComponent,
+ VarDirective,
+ FileSizePipe,
+ MetadataFieldWrapperComponent,
+ ],
+ providers: [
+ { provide: BitstreamDataService, useValue: bitstreamDataService },
+ ],
+
+ schemas: [NO_ERRORS_SCHEMA],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MediaViewerComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ describe('when the bitstreams are loading', () => {
+ beforeEach(() => {
+ comp.mediaList$.next([mockMediaViewerItem]);
+ comp.videoOptions = 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.videoOptions = 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-media-viewer-image')
+ );
+ expect(defaultThumbnail.nativeElement).toBeDefined();
+ });
+ });
+});
diff --git a/src/app/+item-page/media-viewer/media-viewer.component.ts b/src/app/+item-page/media-viewer/media-viewer.component.ts
new file mode 100644
index 0000000000..3f9de8ed3e
--- /dev/null
+++ b/src/app/+item-page/media-viewer/media-viewer.component.ts
@@ -0,0 +1,114 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { filter, take } from 'rxjs/operators';
+import { BitstreamDataService } from '../../core/data/bitstream-data.service';
+import { PaginatedList } from '../../core/data/paginated-list.model';
+import { RemoteData } from '../../core/data/remote-data';
+import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
+import { Bitstream } from '../../core/shared/bitstream.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 { followLink } from '../../shared/utils/follow-link-config.model';
+
+/**
+ * This componenet renders the media viewers
+ */
+
+@Component({
+ selector: 'ds-media-viewer',
+ templateUrl: './media-viewer.component.html',
+ styleUrls: ['./media-viewer.component.scss'],
+})
+export class MediaViewerComponent implements OnInit {
+ @Input() item: Item;
+ @Input() videoOptions: boolean;
+
+ mediaList$: BehaviorSubject;
+
+ isLoading: boolean;
+
+ thumbnailPlaceholder = './assets/images/replacement_document.svg';
+
+ constructor(protected bitstreamDataService: BitstreamDataService) {}
+
+ /**
+ * This metod loads all the Bitstreams and Thumbnails and contert it to media item
+ */
+ ngOnInit(): void {
+ this.mediaList$ = new BehaviorSubject([]);
+ this.isLoading = true;
+ this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => {
+ if (bitstreamsRD.payload.page.length === 0) {
+ this.isLoading = false;
+ this.mediaList$.next([]);
+ } else {
+ this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => {
+ for (
+ let index = 0;
+ index < bitstreamsRD.payload.page.length;
+ index++
+ ) {
+ bitstreamsRD.payload.page[index].format
+ .pipe(getFirstSucceededRemoteDataPayload())
+ .subscribe((format) => {
+ const current = this.mediaList$.getValue();
+ const mediaItem = this.createMediaViewerItem(
+ bitstreamsRD.payload.page[index],
+ format,
+ thumbnailsRD.payload && thumbnailsRD.payload.page[index]
+ );
+ this.mediaList$.next([...current, mediaItem]);
+ });
+ }
+ this.isLoading = false;
+ });
+ }
+ });
+ }
+
+ /**
+ * 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 create MediaViewerItem from incoming bitstreams
+ * @param original original remote data bitstream
+ * @param format original bitstream format
+ * @param thumbnail trunbnail remote data bitstream
+ */
+ createMediaViewerItem(
+ original: Bitstream,
+ format: BitstreamFormat,
+ thumbnail: Bitstream
+ ): MediaViewerItem {
+ const mediaItem = new MediaViewerItem();
+ mediaItem.bitstream = original;
+ mediaItem.format = format.mimetype.split('/')[0];
+ mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
+ return mediaItem;
+ }
+}
diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html
index fc8d06ac10..a004712e0f 100644
--- a/src/app/+item-page/simple/item-types/publication/publication.component.html
+++ b/src/app/+item-page/simple/item-types/publication/publication.component.html
@@ -8,9 +8,14 @@
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts
index a8119c8565..120eda930f 100644
--- a/src/app/+item-page/simple/item-types/shared/item.component.ts
+++ b/src/app/+item-page/simple/item-types/shared/item.component.ts
@@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
+import { environment } from '../../../../../environments/environment';
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model';
@@ -20,6 +21,7 @@ export class ItemComponent implements OnInit {
* Route to the item page
*/
itemPageRoute: string;
+ mediaViewer = environment.mediaViewer;
constructor(protected bitstreamDataService: BitstreamDataService) {
}
diff --git a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html
index 241696f688..7a1366dda9 100644
--- a/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html
+++ b/src/app/+item-page/simple/item-types/untyped-item/untyped-item.component.html
@@ -8,9 +8,14 @@
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts
index 31de304665..7b9a08de92 100644
--- a/src/app/core/auth/auth.interceptor.ts
+++ b/src/app/core/auth/auth.interceptor.ts
@@ -102,7 +102,7 @@ export class AuthInterceptor implements HttpInterceptor {
private parseLocation(header: string): string {
let location = header.trim();
location = location.replace('location="', '');
- location = location.replace('"', '');
+ location = location.replace('"', ''); /* lgtm [js/incomplete-sanitization] */
let re = /%3A%2F%2F/g;
location = location.replace(re, '://');
re = /%3A/g;
diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts
index fdfe1c9fac..5f2f123f01 100644
--- a/src/app/core/registry/registry.service.spec.ts
+++ b/src/app/core/registry/registry.service.spec.ts
@@ -326,6 +326,25 @@ describe('RegistryService', () => {
});
});
+ describe('when createMetadataField is called with a blank qualifier', () => {
+ let result: Observable
;
+ let metadataField: MetadataField;
+
+ beforeEach(() => {
+ metadataField = mockFieldsList[0];
+ metadataField.qualifier = '';
+ result = registryService.createMetadataField(metadataField, mockSchemasList[0]);
+ });
+
+ it('should return the created metadata field with a null qualifier', (done) => {
+ metadataField.qualifier = null;
+ result.subscribe((field: MetadataField) => {
+ expect(field).toEqual(metadataField);
+ done();
+ });
+ });
+ });
+
describe('when updateMetadataField is called', () => {
let result: Observable;
@@ -341,6 +360,25 @@ describe('RegistryService', () => {
});
});
+ describe('when updateMetadataField is called with a blank qualifier', () => {
+ let result: Observable;
+ let metadataField: MetadataField;
+
+ beforeEach(() => {
+ metadataField = mockFieldsList[0];
+ metadataField.qualifier = '';
+ result = registryService.updateMetadataField(metadataField);
+ });
+
+ it('should return the updated metadata field with a null qualifier', (done) => {
+ metadataField.qualifier = null;
+ result.subscribe((field: MetadataField) => {
+ expect(field).toEqual(metadataField);
+ done();
+ });
+ });
+ });
+
describe('when deleteMetadataSchema is called', () => {
let result: Observable>;
diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts
index 9ac849bdd3..b7b35c6a5a 100644
--- a/src/app/core/registry/registry.service.ts
+++ b/src/app/core/registry/registry.service.ts
@@ -245,6 +245,9 @@ export class RegistryService {
* @param schema The MetadataSchema to create the field in
*/
public createMetadataField(field: MetadataField, schema: MetadataSchema): Observable {
+ if (!field.qualifier) {
+ field.qualifier = null;
+ }
return this.metadataFieldService.create(field, new RequestParam('schemaId', schema.id)).pipe(
getFirstSucceededRemoteDataPayload(),
hasValueOperator(),
@@ -260,6 +263,9 @@ export class RegistryService {
* @param field The MetadataField to update
*/
public updateMetadataField(field: MetadataField): Observable {
+ if (!field.qualifier) {
+ field.qualifier = null;
+ }
return this.metadataFieldService.put(field).pipe(
getFirstSucceededRemoteDataPayload(),
hasValueOperator(),
diff --git a/src/app/core/shared/media-viewer-item.model.ts b/src/app/core/shared/media-viewer-item.model.ts
new file mode 100644
index 0000000000..cd3a31bd0b
--- /dev/null
+++ b/src/app/core/shared/media-viewer-item.model.ts
@@ -0,0 +1,21 @@
+import { Bitstream } from './bitstream.model';
+
+/**
+ * Model representing a media viewer item
+ */
+export class MediaViewerItem {
+ /**
+ * Incoming Bitsream
+ */
+ bitstream: Bitstream;
+
+ /**
+ * Incoming Bitsream format type
+ */
+ format: string;
+
+ /**
+ * Incoming Bitsream thumbnail
+ */
+ thumbnail: string;
+}
diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts
index ec0b3dd3ba..bc91d0585e 100644
--- a/src/app/core/shared/metadata.utils.spec.ts
+++ b/src/app/core/shared/metadata.utils.spec.ts
@@ -20,6 +20,7 @@ const dcTitle0 = mdValue('Title 0');
const dcTitle1 = mdValue('Title 1');
const dcTitle2 = mdValue('Title 2', 'en_US');
const bar = mdValue('Bar');
+const test = mdValue('Test');
const singleMap = { 'dc.title': [dcTitle0] };
@@ -30,6 +31,11 @@ const multiMap = {
'foo': [bar]
};
+const regexTestMap = {
+ 'foolbar.baz': [test],
+ 'foo.bard': [test],
+};
+
const multiViewModelList = [
{ key: 'dc.description', ...dcDescription, order: 0 },
{ key: 'dc.description.abstract', ...dcAbstract, order: 0 },
@@ -98,6 +104,9 @@ describe('Metadata', () => {
testAll([multiMap, singleMap], 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]);
testAll([multiMap, singleMap], ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]);
});
+ describe('with regexTestMap', () => {
+ testAll(regexTestMap, 'foo.bar.*', []);
+ });
});
describe('allValues method', () => {
diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts
index 612fba1d4a..3fbeb205d2 100644
--- a/src/app/core/shared/metadata.utils.ts
+++ b/src/app/core/shared/metadata.utils.ts
@@ -156,7 +156,7 @@ export class Metadata {
const outputKeys: string[] = [];
for (const inputKey of inputKeys) {
if (inputKey.includes('*')) {
- const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$');
+ const inputKeyRegex = new RegExp('^' + inputKey.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
for (const mapKey of Object.keys(mdMap)) {
if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) {
outputKeys.push(mapKey);
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index ee93ebb59a..4abf57f258 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -2547,6 +2547,13 @@
"publication.search.results.head": "Publication Search Results",
"publication.search.title": "DSpace Angular :: Publication Search",
+
+
+ "media-viewer.next": "Next",
+
+ "media-viewer.previous": "Previous",
+
+ "media-viewer.playlist": "Playlist",
"register-email.title": "New user registration",
diff --git a/src/assets/images/replacement_audio.svg b/src/assets/images/replacement_audio.svg
new file mode 100644
index 0000000000..03899914c9
--- /dev/null
+++ b/src/assets/images/replacement_audio.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/replacement_document.svg b/src/assets/images/replacement_document.svg
new file mode 100644
index 0000000000..f684ca9597
--- /dev/null
+++ b/src/assets/images/replacement_document.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/replacement_image.svg b/src/assets/images/replacement_image.svg
new file mode 100644
index 0000000000..b6817cf7fc
--- /dev/null
+++ b/src/assets/images/replacement_image.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/assets/images/replacement_video.svg b/src/assets/images/replacement_video.svg
new file mode 100644
index 0000000000..1972b25eca
--- /dev/null
+++ b/src/assets/images/replacement_video.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts
index c197a1407c..8ca11e4a8c 100644
--- a/src/config/global-config.interface.ts
+++ b/src/config/global-config.interface.ts
@@ -12,6 +12,7 @@ import { CollectionPageConfig } from './collection-page-config.interface';
import { ThemeConfig } from './theme.model';
import { AuthConfig } from './auth-config.interfaces';
import { UIServerConfig } from './ui-server-config.interface';
+import { MediaViewerConfig } from './media-viewer-config.interface';
export interface GlobalConfig extends Config {
ui: UIServerConfig;
@@ -32,4 +33,5 @@ export interface GlobalConfig extends Config {
collection: CollectionPageConfig;
themes: ThemeConfig[];
rewriteDownloadUrls: boolean;
+ mediaViewer: MediaViewerConfig;
}
diff --git a/src/config/media-viewer-config.interface.ts b/src/config/media-viewer-config.interface.ts
new file mode 100644
index 0000000000..8a6a163872
--- /dev/null
+++ b/src/config/media-viewer-config.interface.ts
@@ -0,0 +1,6 @@
+import { Config } from './config.interface';
+
+export interface MediaViewerConfig extends Config {
+ image: boolean;
+ video: boolean;
+}
diff --git a/src/environments/environment.common.ts b/src/environments/environment.common.ts
index c6ae9858e5..b8248890fc 100644
--- a/src/environments/environment.common.ts
+++ b/src/environments/environment.common.ts
@@ -263,4 +263,11 @@ export const environment: GlobalConfig = {
],
// Whether the UI should rewrite file download URLs to match its domain. Only necessary to enable when running UI and REST API on separate domains
rewriteDownloadUrls: false,
+ // Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with "image" or "video").
+ // For images, this enables a gallery viewer where you can zoom or page through images.
+ // For videos, this enables embedded video streaming
+ mediaViewer: {
+ image: false,
+ video: false,
+ },
};
diff --git a/src/environments/mock-environment.ts b/src/environments/mock-environment.ts
index 4f321c01d4..8de5b187ad 100644
--- a/src/environments/mock-environment.ts
+++ b/src/environments/mock-environment.ts
@@ -217,4 +217,8 @@ export const environment: Partial = {
name: 'base',
},
],
+ mediaViewer: {
+ image: true,
+ video: true
+ },
};
diff --git a/src/styles/_global-styles.scss b/src/styles/_global-styles.scss
index 41c6ceab81..d151bb3ba7 100644
--- a/src/styles/_global-styles.scss
+++ b/src/styles/_global-styles.scss
@@ -35,10 +35,6 @@ body {
margin: 0;
}
-ds-header-navbar-wrapper {
- z-index: var(--ds-nav-z-index);
-}
-
ds-admin-sidebar {
position: fixed;
z-index: var(--ds-sidebar-z-index);
@@ -48,6 +44,19 @@ ds-admin-sidebar {
height: 100vh;
}
+.sticky-top {
+ z-index: 0;
+}
+
+.media-viewer
+ .change-gallery
+ .ngx-gallery
+ ngx-gallery-preview.ngx-gallery-active {
+ right: 0;
+ left: auto;
+ width: calc(100% - 53px);
+}
+
.ds-submission-reorder-dragging {
.ds-hint,
button {
diff --git a/yarn.lock b/yarn.lock
index f042076b0d..c92f8b3154 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1247,6 +1247,11 @@
merge-source-map "^1.1.0"
schema-utils "^2.7.0"
+"@kolkov/ngx-gallery@^1.2.3":
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/@kolkov/ngx-gallery/-/ngx-gallery-1.2.3.tgz#bb15d4b64a5c03905677aa4ca741835aabe41f43"
+ integrity sha512-Dpnhwq3DGPSXrNt65gexo+/Smb2L0bne14A0WONN04+racETtcY33fqFvNWfRw5Nvk2Eza+sq95eEA0NbgF/6g==
+
"@ng-bootstrap/ng-bootstrap@7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-7.0.0.tgz#3bfa62eb52fdb891b1ce693ea11c39127e2d1ab7"