Merge pull request #4391 from boehlke/mediafiles-projector

Show PDF, PNG and JPEG files in projector
This commit is contained in:
Emanuel Schütze 2019-05-27 22:38:16 +02:00 committed by GitHub
commit 39d77dac29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 357 additions and 28 deletions

View File

@ -51,6 +51,7 @@
"ng-pick-datetime": "^7.0.0", "ng-pick-datetime": "^7.0.0",
"ngx-file-drop": "^6.0.0", "ngx-file-drop": "^6.0.0",
"ngx-mat-select-search": "^1.7.2", "ngx-mat-select-search": "^1.7.2",
"ng2-pdf-viewer": "^5.2.3",
"ngx-papaparse": "^3.0.2", "ngx-papaparse": "^3.0.2",
"pdfmake": "^0.1.53", "pdfmake": "^0.1.53",
"po2json": "^1.0.0-alpha", "po2json": "^1.0.0-alpha",

View File

@ -5,9 +5,9 @@ import { Observable, BehaviorSubject } from 'rxjs';
import { WebsocketService } from 'app/core/core-services/websocket.service'; import { WebsocketService } from 'app/core/core-services/websocket.service';
import { ProjectorElement, Projector } from 'app/shared/models/core/projector'; import { ProjectorElement, Projector } from 'app/shared/models/core/projector';
export interface SlideData<T = { error?: string }> { export interface SlideData<T = { error?: string }, P extends ProjectorElement = ProjectorElement> {
data: T; data: T;
element: ProjectorElement; element: P;
error?: string; error?: string;
} }

View File

@ -186,6 +186,15 @@ export class ProjectorService {
} }
} }
public async updateElement(
projector: Projector,
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
): Promise<void> {
const element = this.getProjectorElement(obj);
projector.replaceElements(element);
await this.projectRequest(projector, projector.elements, projector.elements_preview);
}
/** /**
* Executes the request to change projector elements. * Executes the request to change projector elements.
* *

View File

@ -10,8 +10,6 @@
line-height: 1.5em; line-height: 1.5em;
} }
} }
#slide {
::ng-deep #slide {
z-index: 5;
height: 100%; height: 100%;
} }

View File

@ -71,7 +71,7 @@
<ng-container matColumnDef="projector"> <ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let file"> <mat-cell *matCellDef="let file">
<os-projector-button [object]="file"></os-projector-button> <os-projector-button *ngIf="file.isProjectable()" [object]="file"></os-projector-button>
</mat-cell> </mat-cell>
</ng-container> </ng-container>

View File

@ -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, ...
}

View File

@ -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 { BaseViewModelWithListOfSpeakers } from 'app/site/base/base-view-model-with-list-of-speakers';
import { ViewListOfSpeakers } from 'app/site/agenda/models/view-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 { export interface MediafileTitleInformation {
title: string; title: string;
} }
@ -57,6 +61,10 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
return this.mediafile.downloadUrl; return this.mediafile.downloadUrl;
} }
public get pages(): number | null {
return this.mediafile.mediafile.pages;
}
/** /**
* Determines if the file has the 'hidden' attribute * Determines if the file has the 'hidden' attribute
* @returns the hidden attribute, also 'hidden' if there is no file * @returns the hidden attribute, also 'hidden' if there is no file
@ -96,13 +104,17 @@ export class ViewMediafile extends BaseViewModelWithListOfSpeakers<Mediafile>
}; };
} }
public isProjectable(): boolean {
return this.isImage() || this.isPdf();
}
/** /**
* Determine if the file is an image * Determine if the file is an image
* *
* @returns true or false * @returns true or false
*/ */
public isImage(): boolean { 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<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isFont(): boolean { 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<Mediafile>
* @returns true or false * @returns true or false
*/ */
public isPdf(): boolean { public isPdf(): boolean {
return ['application/pdf'].includes(this.type); return PDF_MIMETYPES.includes(this.type);
} }
/** /**

View File

@ -0,0 +1,37 @@
<mat-expansion-panel *ngIf="elements.length">
<mat-expansion-panel-header>
<span translate>Media file</span>
</mat-expansion-panel-header>
<div *ngFor="let element of elements">
<i>{{ getMediafile(element).getTitle() }}</i>
<div *ngIf="getMediafile(element).isImage()">
<button type="button" *ngIf="!element.fullscreen" mat-icon-button (click)="fullscreen(element)">
<mat-icon>check_box_outline_blank</mat-icon>
</button>
<button type="button" *ngIf="!!element.fullscreen" mat-icon-button (click)="fullscreen(element)">
<mat-icon>check_box</mat-icon>
</button>
<span translate>fullscreen</span>
</div>
<div *ngIf="getMediafile(element).isPdf()">
<button type="button" mat-icon-button (click)="pdfBackward(element)" [disabled]="getPage(element) <= 1">
<mat-icon>arrow_back</mat-icon>
</button>
<button type="button" mat-icon-button (click)="pdfForward(element)" [disabled]="getPage(element) >= getMediafile(element).pages">
<mat-icon>arrow_forward</mat-icon>
</button>
<!-- TODO: Use form for page number; use pdfSetPage then. -->
<span translate>Page</span> {{ getPage(element) }}/{{ getMediafile(element).pages }}
<br>
<button type="button" mat-icon-button (click)="zoom(element, 'in')">
<mat-icon>zoom_in</mat-icon>
</button>
<button type="button" mat-icon-button (click)="zoom(element, 'reset')">
<mat-icon>replay</mat-icon>
</button>
<button type="button" mat-icon-button (click)="zoom(element, 'out')">
<mat-icon>zoom_out</mat-icon>
</button>
</div>
</div>
</mat-expansion-panel>

View File

@ -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<PresentationControlComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule, ProjectorModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PresentationControlComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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);
}
}

View File

@ -262,6 +262,18 @@
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
</mat-expansion-panel> </mat-expansion-panel>
<!-- File display controls -->
<!--<mat-expansion-panel>
<mat-expansion-panel-header>
<span translate>Media controls</span>
</mat-expansion-panel-header>
<os-presentation-control [projector]="projector">
</os-presentation-control>
</mat-expansion-panel>-->
<os-presentation-control [projector]="projector">
</os-presentation-control>
</mat-accordion> </mat-accordion>
</div> </div>
</div> </div>

View File

@ -1,15 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; 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 { CountdownControlsComponent } from './components/countdown-controls/countdown-controls.component';
import { CountdownDialogComponent } from './components/countdown-dialog/countdown-dialog.component'; import { CountdownDialogComponent } from './components/countdown-dialog/countdown-dialog.component';
import { MessageControlsComponent } from './components/message-controls/message-controls.component'; import { MessageControlsComponent } from './components/message-controls/message-controls.component';
import { MessageDialogComponent } from './components/message-dialog/message-dialog.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 { ProjectorListEntryComponent } from './components/projector-list-entry/projector-list-entry.component';
import { ProjectorRoutingModule } from './projector-routing.module';
import { SharedModule } from '../../shared/shared.module';
@NgModule({ @NgModule({
imports: [CommonModule, ProjectorRoutingModule, SharedModule], imports: [CommonModule, ProjectorRoutingModule, SharedModule],
@ -20,8 +21,14 @@ import { ProjectorListEntryComponent } from './components/projector-list-entry/p
CountdownControlsComponent, CountdownControlsComponent,
CountdownDialogComponent, CountdownDialogComponent,
MessageControlsComponent, MessageControlsComponent,
MessageDialogComponent MessageDialogComponent,
PresentationControlComponent
], ],
entryComponents: [CountdownDialogComponent, MessageDialogComponent] entryComponents: [
CountdownDialogComponent,
MessageDialogComponent,
PresentationControlComponent,
ProjectorListEntryComponent
]
}) })
export class ProjectorModule {} export class ProjectorModule {}

View File

@ -2,17 +2,18 @@ import { Input } from '@angular/core';
import { ViewProjector } from 'app/site/projector/models/view-projector'; import { ViewProjector } from 'app/site/projector/models/view-projector';
import { SlideData } from 'app/core/core-services/projector-data.service'; 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 * Every slide has to extends this base class. It forces the slides
* to have an input for the slidedata. * to have an input for the slidedata.
*/ */
export abstract class BaseSlideComponent<T extends object> { export abstract class BaseSlideComponent<T extends object, P extends ProjectorElement = ProjectorElement> {
/** /**
* Each slide must take slide data. * Each slide must take slide data.
*/ */
@Input() @Input()
public data: SlideData<T>; public data: SlideData<T, P>;
/** /**
* The projector where this slide is projected on. * The projector where this slide is projected on.

View File

@ -1,5 +1,16 @@
<div *ngIf="data"> <div *ngIf="data">
<p>{{ data.data.path }}</p> <div *ngIf="isImage" [ngClass]="data.element.fullscreen ? 'fullscreen' : 'nofullscreen'" >
<p>{{ data.data.type }}</p> <img [src]="url" alt="" />
<p>{{ data.data.media_url_prefix }}</p> </div>
<div *ngIf="isPdf" class="fullscreen">
<pdf-viewer
[show-all]="false"
[original-size]="false"
[fit-to-page]="true"
[autoresize]="true"
[page]="data.element.page || 1"
[zoom]="zoom"
[src]="url"
style="display: block;"></pdf-viewer>
</div>
</div> </div>

View File

@ -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;
}

View File

@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MediafileSlideComponent } from './mediafile-slide.component'; import { MediafileSlideComponent } from './mediafile-slide.component';
import { E2EImportsModule } from '../../../../e2e-imports.module'; import { E2EImportsModule } from '../../../../e2e-imports.module';
import { PdfViewerModule } from 'ng2-pdf-viewer';
describe('MediafileSlideComponent', () => { describe('MediafileSlideComponent', () => {
let component: MediafileSlideComponent; let component: MediafileSlideComponent;
@ -9,7 +10,7 @@ describe('MediafileSlideComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule], imports: [E2EImportsModule, PdfViewerModule],
declarations: [MediafileSlideComponent] declarations: [MediafileSlideComponent]
}).compileComponents(); }).compileComponents();
})); }));

View File

@ -2,13 +2,31 @@ import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { MediafileSlideData } from './mediafile-slide-data'; 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({ @Component({
selector: 'os-mediafile-slide', selector: 'os-mediafile-slide',
templateUrl: './mediafile-slide.component.html', templateUrl: './mediafile-slide.component.html',
styleUrls: ['./mediafile-slide.component.scss'] styleUrls: ['./mediafile-slide.component.scss']
}) })
export class MediafileSlideComponent extends BaseSlideComponent<MediafileSlideData> { export class MediafileSlideComponent extends BaseSlideComponent<MediafileSlideData, MediafileProjectorElement> {
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() { public constructor() {
super(); super();
} }

View File

@ -1,7 +1,16 @@
import { NgModule } from '@angular/core'; 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 { 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 {} export class MediafileSlideModule {}

View File

@ -54,8 +54,8 @@ export class SlideManager {
return this.loadedSlideConfigurations[slideName]; return this.loadedSlideConfigurations[slideName];
} }
public getIdentifialbeProjectorElement(element: ProjectorElement): IdentifiableProjectorElement { public getIdentifialbeProjectorElement<P extends ProjectorElement>(element: P): IdentifiableProjectorElement & P {
const identifiableElement: IdentifiableProjectorElement = element as IdentifiableProjectorElement; const identifiableElement: IdentifiableProjectorElement & P = element as IdentifiableProjectorElement & P;
const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy. const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy.
identifiableElement.getIdentifiers = () => identifiers; identifiableElement.getIdentifiers = () => identifiers;
return identifiableElement; return identifiableElement;