Merge pull request #4547 from tsiegleauq/projector-messages-rework

Add better projector messages
This commit is contained in:
Emanuel Schütze 2019-04-03 09:05:11 +02:00 committed by GitHub
commit 8dfeaa5b09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 379 additions and 368 deletions

View File

@ -50,7 +50,7 @@ export class ProjectorButtonComponent implements OnInit {
* Pre-define projection target
*/
@Input()
public projector: Projector;
public projector: Projector | null;
/**
* The constructor
@ -100,6 +100,9 @@ export class ProjectorButtonComponent implements OnInit {
if (!this.object) {
return false;
}
return this.projectorService.isProjected(this.object);
return this.projector
? this.projectorService.isProjectedOn(this.object, this.projector)
: this.projectorService.isProjected(this.object);
}
}

View File

@ -8,6 +8,7 @@
grid-template-columns: min-content auto auto;
}
// could be in a shared scss
.projector-button {
grid-area: project;
margin: auto 10px auto 0;
@ -21,6 +22,7 @@
white-space: nowrap;
}
// could be in a shared scss
.action-buttons {
grid-area: buttons;
text-align: right;

View File

@ -0,0 +1,33 @@
<mat-card *ngIf="message">
<div class="grid-wrapper">
<div class="projector-button">
<os-projector-button [object]="message" [projector]="projector"></os-projector-button>
</div>
<div class="action-buttons">
<button
type="button"
class="small-mat-icon-button"
mat-icon-button
(click)="onBringDialog()"
matTooltip="{{ 'Open projection dialog' | translate }}"
>
<mat-icon>
open_in_new
</mat-icon>
</button>
<button type="button" class="small-mat-icon-button" mat-icon-button (click)="onEdit()">
<mat-icon>
edit
</mat-icon>
</button>
<button type="button" class="small-mat-icon-button" mat-icon-button (click)="onDelete()">
<mat-icon>
close
</mat-icon>
</button>
</div>
<div class="message" [innerHTML]="message.getPreview(100)"></div>
</div>
</mat-card>

View File

@ -0,0 +1,45 @@
.grid-wrapper {
display: grid;
width: 100%;
grid-template-areas:
'project message buttons'
'project message buttons';
grid-gap: 10px;
grid-template-columns: min-content auto auto;
}
// could be in a shared scss
.projector-button {
grid-area: project;
margin: auto 10px auto 0;
}
// could be in a shared scss
.action-buttons {
grid-area: buttons;
text-align: right;
}
// smaller and closer mat icon buttons.
// use on button-tags
// TODO: Somehow does not apply when in style.scss
.small-mat-icon-button {
$size: 20px;
margin: 0 5px;
position: relative;
width: $size;
height: $size;
::ng-deep .mat-icon {
position: absolute;
left: 0;
top: 0;
line-height: normal;
width: $size;
height: $size;
}
.material-icons {
font-size: $size;
}
}

View File

@ -1,21 +1,21 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageControlsComponent } from './message-controls.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { ProjectorMessageListComponent } from './projector-message-list.component';
describe('ProjectorMessageListComponent', () => {
let component: ProjectorMessageListComponent;
let fixture: ComponentFixture<ProjectorMessageListComponent>;
describe('MessageControlsComponent', () => {
let component: MessageControlsComponent;
let fixture: ComponentFixture<MessageControlsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [ProjectorMessageListComponent]
declarations: [MessageControlsComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectorMessageListComponent);
fixture = TestBed.createComponent(MessageControlsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,90 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { Projector } from 'app/shared/models/core/projector';
import { ProjectorMessageRepositoryService } from 'app/core/repositories/projector/projector-message-repository.service';
import { MatSnackBar } from '@angular/material';
import { ViewProjectorMessage } from '../../models/view-projector-message';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { TranslateService } from '@ngx-translate/core';
import { Title } from '@angular/platform-browser';
import { ProjectionDialogService } from 'app/core/ui-services/projection-dialog.service';
/**
* Small controls component for messages.
* Used in the projector detail view, can could be embedded anywhere else
*/
@Component({
selector: 'os-message-controls',
templateUrl: './message-controls.component.html',
styleUrls: ['./message-controls.component.scss']
})
export class MessageControlsComponent extends BaseViewComponent implements OnInit {
/**
* Input slot for the projector message model
*/
@Input()
public message: ViewProjectorMessage;
/**
* Output event for the edit button
*/
@Output()
public editEvent = new EventEmitter<ViewProjectorMessage>();
/**
* Pre defined projection target (if any)
*/
@Input()
public projector: Projector;
/**
* Constructor
*
* @param titleService set the title, required by parent
* @param matSnackBar show errors
* @param translate translate properties
* @param repo the projector message repo
* @param promptService delete prompt
*/
public constructor(
titleService: Title,
matSnackBar: MatSnackBar,
protected translate: TranslateService,
private repo: ProjectorMessageRepositoryService,
private promptService: PromptService,
private projectionDialogService: ProjectionDialogService
) {
super(titleService, translate, matSnackBar);
}
/**
* Init
*/
public ngOnInit(): void {}
/**
* Fires an edit event
*/
public onEdit(): void {
this.editEvent.next(this.message);
}
/**
* Brings the projection dialog
*/
public onBringDialog(): void {
this.projectionDialogService.openProjectDialogFor(this.message);
}
/**
* On delete button
*/
public async onDelete(): Promise<void> {
const content =
this.translate.instant('Delete message') + ` ${this.translate.instant(this.message.getTitle())}?`;
if (await this.promptService.open('Are you sure?', content)) {
this.repo.delete(this.message).then(() => {}, this.raiseError);
}
}
}

View File

@ -0,0 +1,27 @@
<h1 mat-dialog-title>
<span translate>Message</span>
</h1>
<form [formGroup]="messageForm">
<div mat-dialog-content>
<!-- Message -->
<editor formControlName="text" [init]="tinyMceSettings"></editor>
</div>
<div mat-dialog-actions>
<!-- Submit countdown button -->
<button
type="submit"
mat-button
color="primary"
[mat-dialog-close]="messageForm.value"
[disabled]="!messageForm.valid"
>
<span translate>Save</span>
</button>
<!-- Cancel Countdown button -->
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</div>
</form>

View File

@ -0,0 +1,38 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessageDialogComponent, MessageData } from './message-dialog.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
describe('MessageDialogComponent', () => {
let component: MessageDialogComponent;
let fixture: ComponentFixture<MessageDialogComponent>;
const dialogData: MessageData = {
text: ''
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MessageDialogComponent],
imports: [E2EImportsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: dialogData
}
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessageDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,58 @@
import { Component, OnInit, Inject } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
/**
* Determine what to send
*/
export interface MessageData {
text: string;
}
/**
* Dialog component to edit projector messages
*/
@Component({
selector: 'os-message-dialog',
templateUrl: './message-dialog.component.html',
styleUrls: ['./message-dialog.component.scss']
})
export class MessageDialogComponent extends BaseViewComponent implements OnInit {
/**
* The form data
*/
public messageForm: FormGroup;
/**
* Constrcutor
*
* @param title required by parent
* @param matSnackBar to show errors
* @param translate to translate properties
* @param formBuilder to create the message form
* @param data the injected data, i.e the current text of a message to edit
*/
public constructor(
title: Title,
matSnackBar: MatSnackBar,
translate: TranslateService,
private formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA) public data: MessageData
) {
super(title, translate, matSnackBar);
}
/**
* Init create the form
*/
public ngOnInit(): void {
this.messageForm = this.formBuilder.group({
text: [this.data.text, Validators.required]
});
}
}

View File

@ -185,11 +185,7 @@
<span translate>Countdowns</span>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item
*ngFor="let countdown of countdowns"
class="larger-mat-list-item"
[ngClass]="{ projected: isProjected(countdown) }"
>
<mat-list-item *ngFor="let countdown of countdowns" class="larger-mat-list-item">
<os-countdown-controls
class="dynamic-list-entry"
[countdown]="countdown"
@ -207,21 +203,25 @@
</mat-expansion-panel>
<!-- messages -->
<mat-expansion-panel *ngIf="messages.length">
<mat-expansion-panel>
<mat-expansion-panel-header>
<span translate>Messages</span>
</mat-expansion-panel-header>
<mat-list>
<mat-list-item *ngFor="let message of messages" [ngClass]="{ projected: isProjected(message) }">
<button type="button" mat-icon-button (click)="project(message)">
<mat-icon>videocam</mat-icon>
</button>
<span>{{ message.getPreview(40) }}</span>
<mat-list-item *ngFor="let message of messages" class="larger-mat-list-item">
<os-message-controls
class="dynamic-list-entry"
[message]="message"
[projector]="projector.projector"
(editEvent)="openMessagesDialog($event)"
>
</os-message-controls>
</mat-list-item>
</mat-list>
<mat-action-row>
<button type="button" mat-icon-button routerLink="/projectors/messages">
<mat-icon>edit</mat-icon>
<button type="button" mat-button (click)="openMessagesDialog()">
<mat-icon>add</mat-icon>
<span translate>Add message</span>
</button>
</mat-action-row>
</mat-expansion-panel>

View File

@ -123,7 +123,7 @@
}
.larger-mat-list-item {
height: 90px;
height: auto;
}
.larger-mat-list-item + .larger-mat-list-item {

View File

@ -1,8 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from '../../../../../e2e-imports.module';
import { ProjectorDetailComponent } from './projector-detail.component';
import { ProjectorModule } from '../../projector.module';
import { E2EImportsModule } from 'e2e-imports.module';
describe('ProjectorDetailComponent', () => {
let component: ProjectorDetailComponent;

View File

@ -18,13 +18,15 @@ import { CurrentSpeakerChyronSlideService } from '../../services/current-speaker
import { DurationService } from 'app/core/ui-services/duration.service';
import { ProjectorService } from 'app/core/core-services/projector.service';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { Projectable } from 'app/site/base/projectable';
import { ProjectorElement } from 'app/shared/models/core/projector';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { CountdownRepositoryService } from 'app/core/repositories/projector/countdown-repository.service';
import { ProjectorMessageRepositoryService } from 'app/core/repositories/projector/projector-message-repository.service';
import { ViewProjectorMessage } from 'app/site/projector/models/view-projector-message';
import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Projectable } from 'app/site/base/projectable';
import { MessageDialogComponent, MessageData } from '../message-dialog/message-dialog.component';
/**
* The projector detail view.
@ -177,7 +179,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
}
/**
* Opens the countdown dialog
* Opens the countdown dialog*
*
* @param viewCountdown optional existing countdown to edit
*/
@ -212,6 +214,37 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
});
}
/**
* opens the "edit/create" dialog for messages
*
* @param viewMessage an optional ViewProjectorMessage to edit. If empty, a new one was created
*/
public openMessagesDialog(viewMessage?: ViewProjectorMessage): void {
let messageData: MessageData = {
text: ''
};
if (viewMessage) {
messageData = {
text: viewMessage.message
};
}
const dialogRef = this.dialog.open(MessageDialogComponent, {
data: messageData,
maxHeight: '90vh',
width: '800px',
maxWidth: '90vw',
disableClose: true
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.submitMessage(result, viewMessage);
}
});
}
/**
* Function to send a countdown
*
@ -234,4 +267,22 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
this.countdownRepo.create(sendData).then(() => {}, this.raiseError);
}
}
/**
* Submit altered messages to the message repository
*
* @param data: The message to post
* @param viewMessage optional, set viewMessage to update an existing message
*/
public submitMessage(data: MessageData, viewMessage?: ViewProjectorMessage): void {
const sendData = new ProjectorMessage({
message: data.text
});
if (viewMessage) {
this.messageRepo.update(sendData, viewMessage).then(() => {}, this.raiseError);
} else {
this.messageRepo.create(sendData).then(() => {}, this.raiseError);
}
}
}

View File

@ -3,15 +3,6 @@
<div class="title-slot">
<h2 translate>Projectors</h2>
</div>
<!-- Menu -->
<div class="menu-slot">
<div *osPerms="'core.can_manage_projector'">
<button type="button" mat-icon-button [matMenuTriggerFor]="ellipsisMenu">
<mat-icon>more_vert</mat-icon>
</button>
</div>
</div>
</os-head-bar>
<mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1">
@ -172,11 +163,4 @@
</ng-container>
</os-meta-text-block>
</div>
</div>
<mat-menu #ellipsisMenu="matMenu">
<button type="button" mat-menu-item routerLink="/projectors/messages">
<mat-icon>note</mat-icon>
<span translate>Projector messages</span>
</button>
</mat-menu>
</div>

View File

@ -1,83 +0,0 @@
<os-head-bar [nav]="false" [goBack]="true" [mainButton]="true" (mainEvent)="onPlusButton()">
<!-- Title -->
<div class="title-slot">
<h2 translate>Messages</h2>
</div>
</os-head-bar>
<div class="head-spacer"></div>
<mat-card *ngIf="messageToCreate">
<mat-card-title translate>New message</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm"
(keydown)="onKeyDownCreate($event)">
<p>
<editor formControlName="message" [init]="tinyMceSettings"></editor>
</p>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="create()">
<span translate>Save</span>
</button>
<button mat-button (click)="onCancelCreate()">
<span translate>Cancel</span>
</button>
</mat-card-actions>
</mat-card>
<mat-accordion class="os-card">
<mat-expansion-panel *ngFor="let message of messages" (opened)="openId = message.id"
(closed)="panelClosed(message)" [expanded]="openId === message.id" multiple="false">
<!-- Projector button and countdown description-->
<mat-expansion-panel-header>
<mat-panel-title>
<div class="header-container">
<div class="header-projector-button">
<os-projector-button [object]="message"></os-projector-button>
</div>
<div class="header-name">
{{ message.getPreview() }}
</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="updateForm"
*ngIf="editId === message.id"
(keydown)="onKeyDownUpdate($event)">
<h5 translate>Edit message</h5>
<p>
<editor formControlName="message" [init]="tinyMceSettings"></editor>
</p>
</form>
<ng-container *ngIf="editId !== message.id">
<div class="message" *ngIf="message.message" [innerHTML]="getSafeMessage(message)"></div>
<div *ngIf="!message.message" class="no-content" translate>No messages</div>
</ng-container>
<mat-action-row>
<button *ngIf="editId !== message.id" mat-button class="on-transition-fade" (click)="onEditButton(message)"
mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="editId === message.id" mat-button class="on-transition-fade" (click)="onCancelUpdate()"
mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<button *ngIf="editId === message.id" mat-button class="on-transition-fade" (click)="onSaveButton(message)"
mat-icon-button>
<mat-icon>save</mat-icon>
</button>
<button mat-button class='on-transition-fade' (click)=onDeleteButton(message) mat-icon-button>
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
<mat-card *ngIf="messages.length === 0">
<mat-card-content>
<div class="no-content" translate>No messages</div>
</mat-card-content>
</mat-card>

View File

@ -1,42 +0,0 @@
.head-spacer {
width: 100%;
height: 60px;
line-height: 60px;
text-align: right;
background: white; /* TODO: remove this and replace with theme */
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
mat-card {
margin-bottom: 20px;
}
.header-container {
display: grid;
grid-template-rows: auto;
grid-template-columns: 40px 1fr;
width: 100%;
> div {
grid-row-start: 1;
grid-row-end: span 1;
grid-column-end: span 2;
}
.header-projector-button {
grid-column-start: 1;
}
.header-name {
grid-column-start: 2;
padding: 10px;
}
}
.message {
text-align: center;
::ng-deep p {
margin: 0 0 10px;
}
}

View File

@ -1,192 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Title, SafeHtml, DomSanitizer } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { FormGroup, FormBuilder } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from '../../../base/base-view';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { ViewProjectorMessage } from '../../models/view-projector-message';
import { ProjectorMessageRepositoryService } from 'app/core/repositories/projector/projector-message-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
/**
* List view for the projector messages.
*/
@Component({
selector: 'os-projector-message-list',
templateUrl: './projector-message-list.component.html',
styleUrls: ['./projector-message-list.component.scss']
})
export class ProjectorMessageListComponent extends BaseViewComponent implements OnInit {
public messageToCreate: ProjectorMessage | null;
/**
* Source of the Data
*/
public messages: ViewProjectorMessage[] = [];
/**
* The current focussed formgroup
*/
public updateForm: FormGroup;
public createForm: FormGroup;
public openId: number | null;
public editId: number | null;
/**
*/
public constructor(
titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
private repo: ProjectorMessageRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService,
private santinizer: DomSanitizer
) {
super(titleService, translate, matSnackBar);
const form = {
message: ['']
};
this.createForm = this.formBuilder.group(form);
this.updateForm = this.formBuilder.group(form);
}
/**
* Init function.
*
* Sets the title and gets/observes messages from DataStore
*/
public ngOnInit(): void {
super.setTitle('Messages');
this.messages = this.repo.getSortedViewModelList();
this.repo.getViewModelListObservable().subscribe(messages => (this.messages = messages));
}
public getSafeMessage(message: ViewProjectorMessage): SafeHtml {
return this.santinizer.bypassSecurityTrustHtml(message.message);
}
/**
* Add a new message.
*/
public onPlusButton(): void {
if (!this.messageToCreate) {
this.createForm.reset();
this.createForm.setValue({
message: ''
});
this.messageToCreate = new ProjectorMessage();
}
}
/**
* Handler when clicking on create to create a new statute paragraph
*/
public create(): void {
if (this.createForm.valid) {
this.messageToCreate.patchValues(this.createForm.value as ProjectorMessage);
this.repo.create(this.messageToCreate).then(() => {
this.messageToCreate = null;
}, this.raiseError);
}
}
/**
* Executed on edit button
* @param message
*/
public onEditButton(message: ViewProjectorMessage): void {
this.editId = message.id;
this.updateForm.setValue({
message: message.message
});
}
/**
* Saves the message
* @param message The message to save
*/
public onSaveButton(message: ViewProjectorMessage): void {
if (this.updateForm.valid) {
this.repo.update(this.updateForm.value as Partial<ProjectorMessage>, message).then(() => {
this.openId = this.editId = null;
}, this.raiseError);
}
}
/**
* Is executed, when the delete button is pressed
*
* @param message The message to delete
*/
public async onDeleteButton(message: ViewProjectorMessage): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete the selected message?');
if (await this.promptService.open(title, null)) {
this.repo.delete(message).then(() => (this.openId = this.editId = null), this.raiseError);
}
}
/**
* Is executed when a mat-extension-panel is closed
*
* @param message the message in the panel
*/
public panelClosed(message: ViewProjectorMessage): void {
this.openId = null;
if (this.editId) {
this.onSaveButton(message);
}
}
/**
* clicking Shift and Enter will save automatically
* clicking Escape will cancel the process
*
* @param event has the code
*/
public onKeyDownCreate(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) {
this.create();
}
if (event.key === 'Escape') {
this.onCancelCreate();
}
}
/**
* Cancels the current form action
*/
public onCancelCreate(): void {
this.messageToCreate = null;
}
/**
* clicking Shift and Enter will save automatically
* clicking Escape will cancel the process
*
* @param event has the code
*/
public onKeyDownUpdate(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) {
const message = this.messages.find(x => x.id === this.editId);
this.onSaveButton(message);
}
if (event.key === 'Escape') {
this.onCancelUpdate();
}
}
/**
* Cancels the current form action
*/
public onCancelUpdate(): void {
this.editId = null;
}
}

View File

@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProjectorListComponent } from './components/projector-list/projector-list.component';
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
import { ProjectorMessageListComponent } from './components/projector-message-list/projector-message-list.component';
const routes: Routes = [
{
@ -13,10 +12,6 @@ const routes: Routes = [
{
path: 'detail/:id',
component: ProjectorDetailComponent
},
{
path: 'messages',
component: ProjectorMessageListComponent
}
];

View File

@ -5,19 +5,21 @@ 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 { ProjectorMessageListComponent } from './components/projector-message-list/projector-message-list.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';
@NgModule({
imports: [CommonModule, ProjectorRoutingModule, SharedModule],
declarations: [
ProjectorListComponent,
ProjectorDetailComponent,
ProjectorMessageListComponent,
CountdownControlsComponent,
CountdownDialogComponent
CountdownDialogComponent,
MessageControlsComponent,
MessageDialogComponent
],
entryComponents: [CountdownDialogComponent]
entryComponents: [CountdownDialogComponent, MessageDialogComponent]
})
export class ProjectorModule {}