diff --git a/package.json b/package.json index ae4abd2e41..ddbcf5d719 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "ng2-file-upload": "1.4.0", "ng2-nouislider": "^1.8.2", "ngx-bootstrap": "^5.3.2", + "ngx-gallery": "^5.10.0", "ngx-infinite-scroll": "6.0.1", "ngx-moment": "^3.4.0", "ngx-pagination": "3.0.3", diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 4c3a64e117..06dd6a4262 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -31,6 +31,11 @@ import { TabbedRelatedEntitiesSearchComponent } from './simple/related-entities/ import { StatisticsModule } from '../statistics/statistics.module'; import { AbstractIncrementalListComponent } from './simple/abstract-incremental-list/abstract-incremental-list.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 'ngx-gallery'; + @NgModule({ imports: [ CommonModule, @@ -38,7 +43,8 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- ItemPageRoutingModule, EditItemPageModule, SearchPageModule, - StatisticsModule.forRoot() + StatisticsModule.forRoot(), + NgxGalleryModule, ], declarations: [ ItemPageComponent, @@ -62,6 +68,9 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- UploadBitstreamComponent, TabbedRelatedEntitiesSearchComponent, AbstractIncrementalListComponent, + MediaViewerComponent, + MediaViewerVideoComponent, + MediaViewerImageComponent, ], exports: [ ItemComponent, @@ -72,12 +81,8 @@ import { AbstractIncrementalListComponent } from './simple/abstract-incremental- RelatedItemsComponent, MetadataRepresentationListComponent, ItemPageTitleFieldComponent, - TabbedRelatedEntitiesSearchComponent + TabbedRelatedEntitiesSearchComponent, ], - entryComponents: [ - PublicationComponent - ] + entryComponents: [PublicationComponent], }) -export class ItemPageModule { - -} +export class ItemPageModule {} 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..b0398830a8 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.html @@ -0,0 +1 @@ + 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..e69de29bb2 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..2cc02bd2ac --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.spec.ts @@ -0,0 +1,41 @@ +import { DebugElement } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +import { MediaViewerImageComponent } from './media-viewer-image.component'; + +describe('MediaViewerImageComponent', () => { + let component: MediaViewerImageComponent; + let fixture: ComponentFixture; + let debugElement: DebugElement; + let htmlElement: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MediaViewerImageComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MediaViewerImageComponent); + component = fixture.componentInstance; + component.galleryOptions = [ + { + image: true, + imageSize: 'contain', + thumbnails: false, + imageArrows: false, + width: '340px', + height: '279px', + }, + ]; + debugElement = fixture.debugElement.query(By.css('ngx-gallery')); + htmlElement = debugElement.nativeElement; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe; +}); 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..78fef69765 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-image/media-viewer-image.component.ts @@ -0,0 +1,43 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { NgxGalleryOptions, NgxGalleryImage } from 'ngx-gallery'; +import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; + +@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[]; + constructor() {} + + galleryOptions: NgxGalleryOptions[]; + galleryImages: NgxGalleryImage[]; + + ngOnInit(): void { + this.galleryImages = new Array(); + this.galleryOptions = [ + { + image: true, + imageSize: 'contain', + thumbnails: false, + imageArrows: false, + width: '340px', + height: '279px', + }, + ]; + for (const image of this.images) { + this.galleryImages = [ + ...this.galleryImages, + { + small: image.thumbnail + ? image.thumbnail + : './assets/images/replacements_image.svg', + medium: image.bitstream._links.content.href, + big: image.bitstream._links.content.href, + }, + ]; + } + } +} 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..ec813a3116 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.html @@ -0,0 +1,48 @@ + + +
+ + + +
+ +
+ +
+
+
+
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..48935880de --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.spec.ts @@ -0,0 +1,46 @@ +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 { 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 { MediaViewerVideoComponent } from './media-viewer-video.component'; + +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(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MediaViewerVideoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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..12a55979bf --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer-video/media-viewer-video.component.ts @@ -0,0 +1,38 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; + +@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[]; + + isCollapsed: boolean; + currentIndex = 0; + + replacements = { + video: './assets/images/replacement_video.svg', + audio: './assets/images/replacement_audio.svg', + document: './assets/images/replacement_document.svg', + }; + + replacementThumbnail: string; + constructor() {} + + ngOnInit() { + this.isCollapsed = false; + } + + selectedMedia(index: number) { + console.log(index); + this.currentIndex = index; + } + nextMedia() { + this.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..dd901e2230 --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.html @@ -0,0 +1,19 @@ + + +
+ + + + + + +
+
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..e69de29bb2 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..c487facafd --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.spec.ts @@ -0,0 +1,106 @@ +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 bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', { + findAllByItemAndBundleName: createSuccessfulRemoteDataObject$( + createPaginatedList([]) + ), + }); + + 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 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.isLoading = true; + fixture.detectChanges(); + }); + + it('should display a loading component', () => { + const loading = fixture.debugElement.query(By.css('ds-loading')); + expect(loading.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..c6c336826a --- /dev/null +++ b/src/app/+item-page/media-viewer/media-viewer.component.ts @@ -0,0 +1,76 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { filter, takeWhile } from 'rxjs/operators'; +import { BitstreamDataService } from '../../core/data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { RemoteData } from '../../core/data/remote-data'; +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 { hasNoValue, hasValue } from '../../shared/empty.util'; + +@Component({ + selector: 'ds-media-viewer', + templateUrl: './media-viewer.component.html', + styleUrls: ['./media-viewer.component.scss'], +}) +export class MediaViewerComponent implements OnInit { + @Input() item: Item; + + mediaList$: BehaviorSubject; + + isLoading: boolean; + + constructor( + protected bitstreamDataService: BitstreamDataService, + protected bitstreamFormatDataService: BitstreamFormatDataService + ) {} + + ngOnInit(): void { + this.mediaList$ = new BehaviorSubject([]); + this.isLoading = true; + + this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => { + console.log(bitstreamsRD); + this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => { + for (let index = 0; index < bitstreamsRD.payload.page.length; index++) { + this.bitstreamFormatDataService + .findByBitstream(bitstreamsRD.payload.page[index]) + .pipe(getFirstSucceededRemoteDataPayload()) + .subscribe((format) => { + const current = this.mediaList$.getValue(); + const mediaItem = new MediaViewerItem(); + mediaItem.bitstream = bitstreamsRD.payload.page[index]; + mediaItem.format = format.mimetype.split('/')[0]; + mediaItem.thumbnail = + thumbnailsRD.payload && thumbnailsRD.payload.page[index] + ? thumbnailsRD.payload.page[index]._links.content.href + : null; + this.mediaList$.next([...current, mediaItem]); + }); + } + this.isLoading = false; + }); + }); + } + + loadRemoteData( + bundleName: string + ): Observable>> { + return this.bitstreamDataService + .findAllByItemAndBundleName(this.item, bundleName) + .pipe( + filter((bitstreamsRD: RemoteData>) => + hasValue(bitstreamsRD) + ), + takeWhile( + (bitstreamsRD_1: RemoteData>) => + hasNoValue(bitstreamsRD_1.payload) && + hasNoValue(bitstreamsRD_1.error), + true + ) + ); + } +} 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 9eb704b9e9..4b802ac583 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 @@ -3,9 +3,10 @@
- + + 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/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 369d3e0e63..b9ef252fe3 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -2449,6 +2449,13 @@ "publication.search.results.head": "Publication Search Results", "publication.search.title": "DSpace Angular :: Publication Search", + + + "media-viewer.next": "Next", + + "media-viewer.previus": "Previus", + + "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/environments/environment.common.ts b/src/environments/environment.common.ts index 4f73339690..58a8443b6c 100644 --- a/src/environments/environment.common.ts +++ b/src/environments/environment.common.ts @@ -17,9 +17,9 @@ export const environment: GlobalConfig = { // The REST API server settings. // NOTE: these must be "synced" with the 'dspace.server.url' setting in your backend's local.cfg. rest: { - ssl: true, - host: 'dspace7.4science.cloud', - port: 443, + ssl: false, + host: 'localhost', + port: 8080, // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: '/server', }, diff --git a/yarn.lock b/yarn.lock index d44182cc51..261651bb18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6928,6 +6928,11 @@ ngx-bootstrap@^5.3.2: resolved "https://registry.yarnpkg.com/ngx-bootstrap/-/ngx-bootstrap-5.3.2.tgz#0668b01202610657e998b3ca87669645e0b31dc9" integrity sha512-gSMf8EXYl99Q3gqkq4RVhoTNSTYHz2Or6Cig2BJRbLJyqk15ZQE5qcq/ldHS8zzx/wgCA3HQeI63t2j2mEU9PA== +ngx-gallery@^5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/ngx-gallery/-/ngx-gallery-5.10.0.tgz#21f623cb788578dbb5a3625c869712de2b95258c" + integrity sha512-+2DnsBfkIzNQoReOHf6+OMf06+qyQQMyVVN4iQAtL0+KykjVqDZiBwLQtmwajDWMGph6O1HNKLrqTcmgqw+d2A== + ngx-infinite-scroll@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-6.0.1.tgz#571e54860ce32839451569bcf6e7a63cfae327bd"