diff --git a/client/package.json b/client/package.json index ce2b4ffb8..4ba41e54c 100644 --- a/client/package.json +++ b/client/package.json @@ -51,6 +51,7 @@ "ng-pick-datetime": "^7.0.0", "ngx-file-drop": "^6.0.0", "ngx-mat-select-search": "^1.7.2", + "ng2-pdf-viewer": "^5.2.3", "ngx-papaparse": "^3.0.2", "pdfmake": "^0.1.53", "po2json": "^1.0.0-alpha", diff --git a/client/src/app/core/core-services/projector-data.service.ts b/client/src/app/core/core-services/projector-data.service.ts index 7b3a2358d..9600e20d2 100644 --- a/client/src/app/core/core-services/projector-data.service.ts +++ b/client/src/app/core/core-services/projector-data.service.ts @@ -5,9 +5,9 @@ import { Observable, BehaviorSubject } from 'rxjs'; import { WebsocketService } from 'app/core/core-services/websocket.service'; import { ProjectorElement, Projector } from 'app/shared/models/core/projector'; -export interface SlideData { +export interface SlideData { data: T; - element: ProjectorElement; + element: P; error?: string; } diff --git a/client/src/app/core/core-services/projector.service.ts b/client/src/app/core/core-services/projector.service.ts index 1d36ee3a2..4e958b5b0 100644 --- a/client/src/app/core/core-services/projector.service.ts +++ b/client/src/app/core/core-services/projector.service.ts @@ -186,6 +186,15 @@ export class ProjectorService { } } + public async updateElement( + projector: Projector, + obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement + ): Promise { + const element = this.getProjectorElement(obj); + projector.replaceElements(element); + await this.projectRequest(projector, projector.elements, projector.elements_preview); + } + /** * Executes the request to change projector elements. * diff --git a/client/src/app/shared/components/slide-container/slide-container.component.scss b/client/src/app/shared/components/slide-container/slide-container.component.scss index cb29e038e..ad8395fe0 100644 --- a/client/src/app/shared/components/slide-container/slide-container.component.scss +++ b/client/src/app/shared/components/slide-container/slide-container.component.scss @@ -10,8 +10,6 @@ line-height: 1.5em; } } - -::ng-deep #slide { - z-index: 5; +#slide { height: 100%; } diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html index 168e6c4be..87659fb3d 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.html @@ -71,7 +71,7 @@ Projector - + diff --git a/client/src/app/site/mediafiles/models/mediafile-projector-element.ts b/client/src/app/site/mediafiles/models/mediafile-projector-element.ts new file mode 100644 index 000000000..98670520e --- /dev/null +++ b/client/src/app/site/mediafiles/models/mediafile-projector-element.ts @@ -0,0 +1,13 @@ +import { ProjectorElement } from 'app/shared/models/core/projector'; + +export interface MediafileProjectorElement extends ProjectorElement { + // Images and Pdf + rotation?: 0 | 90 | 180 | 270; + + // Images + fullscreen?: boolean; + + // Pdf + page?: number; + zoom?: number; // 0 is normal, then +-1, +-2, ... +} diff --git a/client/src/app/site/mediafiles/models/view-mediafile.ts b/client/src/app/site/mediafiles/models/view-mediafile.ts index dbeb91857..51a9cf5ae 100644 --- a/client/src/app/site/mediafiles/models/view-mediafile.ts +++ b/client/src/app/site/mediafiles/models/view-mediafile.ts @@ -7,6 +7,10 @@ import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers'; import { ViewListOfSpeakers } from 'app/site/agenda/models/view-list-of-speakers'; +export const IMAGE_MIMETYPES = ['image/png', 'image/jpeg', 'image/gif']; +export const FONT_MIMETYPES = ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt']; +export const PDF_MIMETYPES = ['application/pdf']; + export interface MediafileTitleInformation { title: string; } @@ -57,6 +61,10 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers return this.mediafile.downloadUrl; } + public get pages(): number | null { + return this.mediafile.mediafile.pages; + } + /** * Determines if the file has the 'hidden' attribute * @returns the hidden attribute, also 'hidden' if there is no file @@ -96,13 +104,17 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers }; } + public isProjectable(): boolean { + return this.isImage() || this.isPdf(); + } + /** * Determine if the file is an image * * @returns true or false */ public isImage(): boolean { - return ['image/png', 'image/jpeg', 'image/gif'].includes(this.type); + return IMAGE_MIMETYPES.includes(this.type); } /** @@ -111,7 +123,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers * @returns true or false */ public isFont(): boolean { - return ['font/ttf', 'font/woff', 'application/font-woff', 'application/font-sfnt'].includes(this.type); + return FONT_MIMETYPES.includes(this.type); } /** @@ -120,7 +132,7 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers * @returns true or false */ public isPdf(): boolean { - return ['application/pdf'].includes(this.type); + return PDF_MIMETYPES.includes(this.type); } /** diff --git a/client/src/app/site/projector/components/presentation-control/presentation-control.component.html b/client/src/app/site/projector/components/presentation-control/presentation-control.component.html new file mode 100644 index 000000000..a3b01a6e6 --- /dev/null +++ b/client/src/app/site/projector/components/presentation-control/presentation-control.component.html @@ -0,0 +1,37 @@ + + + Media file + +
+ {{ getMediafile(element).getTitle() }} +
+ + + fullscreen +
+
+ + + + Page {{ getPage(element) }}/{{ getMediafile(element).pages }} +
+ + + +
+
+
diff --git a/client/src/app/site/projector/components/presentation-control/presentation-control.component.scss b/client/src/app/site/projector/components/presentation-control/presentation-control.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/projector/components/presentation-control/presentation-control.component.spec.ts b/client/src/app/site/projector/components/presentation-control/presentation-control.component.spec.ts new file mode 100644 index 000000000..b4cce692c --- /dev/null +++ b/client/src/app/site/projector/components/presentation-control/presentation-control.component.spec.ts @@ -0,0 +1,26 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from '../../../../../e2e-imports.module'; +import { PresentationControlComponent } from './presentation-control.component'; +import { ProjectorModule } from '../../projector.module'; + +describe('PresentationControlComponent', () => { + let component: PresentationControlComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule, ProjectorModule] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PresentationControlComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts b/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts new file mode 100644 index 000000000..5eafcf77b --- /dev/null +++ b/client/src/app/site/projector/components/presentation-control/presentation-control.component.ts @@ -0,0 +1,144 @@ +import { Component, Input } from '@angular/core'; +import { MatSnackBar } from '@angular/material'; +import { Title } from '@angular/platform-browser'; + +import { TranslateService } from '@ngx-translate/core'; + +import { BaseViewComponent } from 'app/site/base/base-view'; +import { Mediafile } from 'app/shared/models/mediafiles/mediafile'; +import { MediafileRepositoryService } from 'app/core/repositories/mediafiles/mediafile-repository.service'; +import { ProjectorService } from 'app/core/core-services/projector.service'; +import { SlideManager } from 'app/slides/services/slide-manager.service'; +import { ViewMediafile } from 'app/site/mediafiles/models/view-mediafile'; +import { ViewProjector } from '../../models/view-projector'; +import { MediafileProjectorElement } from 'app/site/mediafiles/models/mediafile-projector-element'; + +/** + * The presentation controls. + */ +@Component({ + selector: 'os-presentation-control', + templateUrl: './presentation-control.component.html', + styleUrls: ['./presentation-control.component.scss'] +}) +export class PresentationControlComponent extends BaseViewComponent { + /** + * The projector. + */ + private _projector: ViewProjector; + + @Input() + public set projector(projector: ViewProjector) { + this._projector = projector; + this.updateElements(); + } + + public get projector(): ViewProjector { + return this._projector; + } + + // All mediafile elements. + public elements: MediafileProjectorElement[] = []; + + /** + * Constructor + * + * @param titleService + * @param translate + * @param matSnackBar + * @param mediafileRepo + * @param slideManager + * @param projectorService + */ + public constructor( + titleService: Title, + translate: TranslateService, + matSnackBar: MatSnackBar, + private mediafileRepo: MediafileRepositoryService, + private slideManager: SlideManager, + private projectorService: ProjectorService + ) { + super(titleService, translate, matSnackBar); + } + + /** + * Updates incoming elements + */ + private updateElements(): void { + this.elements = this.projector.elements.filter(element => { + if (element.name !== Mediafile.COLLECTIONSTRING || !element.id) { + return false; + } + const mediafile = this.mediafileRepo.getViewModel(element.id); + return !!mediafile && mediafile.isProjectable(); + }); + } + + public getMediafile(element: MediafileProjectorElement): ViewMediafile { + return this.mediafileRepo.getViewModel(element.id); + } + + /** + * @returns the currently used page number (1 in case of unnumbered elements) + */ + public getPage(element: MediafileProjectorElement): number { + return element.page || 1; + } + + /** + * moves the projected forward by one page (if not already at end) + * + * @param element + */ + public pdfForward(element: MediafileProjectorElement): void { + if (this.getPage(element) < this.getMediafile(element).pages) { + this.pdfSetPage(element, this.getPage(element) + 1); + } + } + + /** + * moves the projected one page backwards (if not already at beginnning) + * + * @param element + */ + public pdfBackward(element: MediafileProjectorElement): void { + if (this.getPage(element) > 1) { + this.pdfSetPage(element, this.getPage(element) - 1); + } + } + + /** + * Moves the element to a specific given page. If the number given is greater + * than the amount of element pages, it does nothing + * + * @param element + * @param page + */ + public pdfSetPage(element: MediafileProjectorElement, page: number): void { + if (this.getMediafile(element).pages >= page) { + element.page = page; + this.updateElement(element); + } + } + + public zoom(element: MediafileProjectorElement, direction: 'in' | 'out' | 'reset'): void { + if (direction === 'reset') { + element.zoom = 0; + } else if (direction === 'in') { + element.zoom = (element.zoom || 0) + 1; + } else if (direction === 'out') { + element.zoom = (element.zoom || 0) - 1; + } + this.updateElement(element); + } + + public fullscreen(element: MediafileProjectorElement): void { + element.fullscreen = !element.fullscreen; + this.updateElement(element); + } + + private updateElement(element: MediafileProjectorElement): void { + const idElement = this.slideManager.getIdentifialbeProjectorElement(element); + this.projectorService.updateElement(this.projector.projector, idElement).then(null, this.raiseError); + } +} diff --git a/client/src/app/site/projector/components/projector-detail/projector-detail.component.html b/client/src/app/site/projector/components/projector-detail/projector-detail.component.html index 0395f7afd..1296de599 100644 --- a/client/src/app/site/projector/components/projector-detail/projector-detail.component.html +++ b/client/src/app/site/projector/components/projector-detail/projector-detail.component.html @@ -262,6 +262,18 @@ + + + + + + diff --git a/client/src/app/site/projector/projector.module.ts b/client/src/app/site/projector/projector.module.ts index 5ce0a9a13..7490309a7 100644 --- a/client/src/app/site/projector/projector.module.ts +++ b/client/src/app/site/projector/projector.module.ts @@ -1,15 +1,16 @@ -import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; -import { ProjectorRoutingModule } from './projector-routing.module'; -import { SharedModule } from '../../shared/shared.module'; -import { ProjectorListComponent } from './components/projector-list/projector-list.component'; -import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component'; import { CountdownControlsComponent } from './components/countdown-controls/countdown-controls.component'; import { CountdownDialogComponent } from './components/countdown-dialog/countdown-dialog.component'; import { MessageControlsComponent } from './components/message-controls/message-controls.component'; import { MessageDialogComponent } from './components/message-dialog/message-dialog.component'; +import { PresentationControlComponent } from './components/presentation-control/presentation-control.component'; +import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component'; +import { ProjectorListComponent } from './components/projector-list/projector-list.component'; import { ProjectorListEntryComponent } from './components/projector-list-entry/projector-list-entry.component'; +import { ProjectorRoutingModule } from './projector-routing.module'; +import { SharedModule } from '../../shared/shared.module'; @NgModule({ imports: [CommonModule, ProjectorRoutingModule, SharedModule], @@ -20,8 +21,14 @@ import { ProjectorListEntryComponent } from './components/projector-list-entry/p CountdownControlsComponent, CountdownDialogComponent, MessageControlsComponent, - MessageDialogComponent + MessageDialogComponent, + PresentationControlComponent ], - entryComponents: [CountdownDialogComponent, MessageDialogComponent] + entryComponents: [ + CountdownDialogComponent, + MessageDialogComponent, + PresentationControlComponent, + ProjectorListEntryComponent + ] }) export class ProjectorModule {} diff --git a/client/src/app/slides/agenda/current-speaker-chyron/current-speaker-chyron-slide.component.html b/client/src/app/slides/agenda/current-speaker-chyron/current-speaker-chyron-slide.component.html index eba96bae0..81ff50cb8 100644 --- a/client/src/app/slides/agenda/current-speaker-chyron/current-speaker-chyron-slide.component.html +++ b/client/src/app/slides/agenda/current-speaker-chyron/current-speaker-chyron-slide.component.html @@ -1,4 +1,4 @@ -
diff --git a/client/src/app/slides/base-slide-component.ts b/client/src/app/slides/base-slide-component.ts index 29d1e2eec..f83515b7f 100644 --- a/client/src/app/slides/base-slide-component.ts +++ b/client/src/app/slides/base-slide-component.ts @@ -2,17 +2,18 @@ import { Input } from '@angular/core'; import { ViewProjector } from 'app/site/projector/models/view-projector'; import { SlideData } from 'app/core/core-services/projector-data.service'; +import { ProjectorElement } from 'app/shared/models/core/projector'; /** * Every slide has to extends this base class. It forces the slides * to have an input for the slidedata. */ -export abstract class BaseSlideComponent { +export abstract class BaseSlideComponent { /** * Each slide must take slide data. */ @Input() - public data: SlideData; + public data: SlideData; /** * The projector where this slide is projected on. diff --git a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.html b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.html index 0aeb50f15..618db8aa4 100644 --- a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.html +++ b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.html @@ -1,5 +1,16 @@
-

{{ data.data.path }}

-

{{ data.data.type }}

-

{{ data.data.media_url_prefix }}

+
+ +
+
+ +
diff --git a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.scss b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.scss index e69de29bb..0ff1a9b0f 100644 --- a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.scss +++ b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.scss @@ -0,0 +1,30 @@ +.fullscreen img, +.nofullscreen img { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; +} +.fullscreen { + z-index: 100; /* TODO: find solution to overlap header/footer */ + width: 100%; + height: 100%; + background-color: white; + position: fixed; + top: 0; + left: 0; + + img { + width: 100%; + height: auto; /* TODO: use dynamic auto width/height for landscape/portrait format */ + } +} + +.nofullscreen { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; +} diff --git a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.spec.ts b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.spec.ts index 49b245453..3cd1b9998 100644 --- a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.spec.ts +++ b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.spec.ts @@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { MediafileSlideComponent } from './mediafile-slide.component'; import { E2EImportsModule } from '../../../../e2e-imports.module'; +import { PdfViewerModule } from 'ng2-pdf-viewer'; describe('MediafileSlideComponent', () => { let component: MediafileSlideComponent; @@ -9,7 +10,7 @@ describe('MediafileSlideComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [E2EImportsModule], + imports: [E2EImportsModule, PdfViewerModule], declarations: [MediafileSlideComponent] }).compileComponents(); })); diff --git a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.ts b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.ts index cab715e8b..5a31c04e5 100644 --- a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.ts +++ b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.component.ts @@ -2,13 +2,31 @@ import { Component } from '@angular/core'; import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { MediafileSlideData } from './mediafile-slide-data'; +import { IMAGE_MIMETYPES, PDF_MIMETYPES } from 'app/site/mediafiles/models/view-mediafile'; +import { MediafileProjectorElement } from 'app/site/mediafiles/models/mediafile-projector-element'; @Component({ selector: 'os-mediafile-slide', templateUrl: './mediafile-slide.component.html', styleUrls: ['./mediafile-slide.component.scss'] }) -export class MediafileSlideComponent extends BaseSlideComponent { +export class MediafileSlideComponent extends BaseSlideComponent { + public get url(): string { + return `${this.data.data.media_url_prefix}/${this.data.data.path}`; + } + + public get zoom(): number { + return Math.pow(1.1, this.data.element.zoom || 0); + } + + public get isImage(): boolean { + return IMAGE_MIMETYPES.includes(this.data.data.type); + } + + public get isPdf(): boolean { + return PDF_MIMETYPES.includes(this.data.data.type); + } + public constructor() { super(); } diff --git a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.module.ts b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.module.ts index 39935f7f1..9ad94d5a5 100644 --- a/client/src/app/slides/mediafiles/mediafile/mediafile-slide.module.ts +++ b/client/src/app/slides/mediafiles/mediafile/mediafile-slide.module.ts @@ -1,7 +1,16 @@ import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { PdfViewerModule } from 'ng2-pdf-viewer'; -import { makeSlideModule } from 'app/slides/base-slide-module'; import { MediafileSlideComponent } from './mediafile-slide.component'; +import { SharedModule } from 'app/shared/shared.module'; +import { SLIDE } from 'app/slides/slide-token'; -@NgModule(makeSlideModule(MediafileSlideComponent)) +@NgModule({ + imports: [CommonModule, SharedModule, PdfViewerModule], + declarations: [MediafileSlideComponent], + providers: [{ provide: SLIDE, useValue: MediafileSlideComponent }], + entryComponents: [MediafileSlideComponent] +}) export class MediafileSlideModule {} diff --git a/client/src/app/slides/services/slide-manager.service.ts b/client/src/app/slides/services/slide-manager.service.ts index f2ef6a768..5ed340c1b 100644 --- a/client/src/app/slides/services/slide-manager.service.ts +++ b/client/src/app/slides/services/slide-manager.service.ts @@ -54,8 +54,8 @@ export class SlideManager { return this.loadedSlideConfigurations[slideName]; } - public getIdentifialbeProjectorElement(element: ProjectorElement): IdentifiableProjectorElement { - const identifiableElement: IdentifiableProjectorElement = element as IdentifiableProjectorElement; + public getIdentifialbeProjectorElement

(element: P): IdentifiableProjectorElement & P { + const identifiableElement: IdentifiableProjectorElement & P = element as IdentifiableProjectorElement & P; const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy. identifiableElement.getIdentifiers = () => identifiers; return identifiableElement;