Merge pull request #4585 from FinnStutzenstein/projectorListCleanup

Put projector cards in own components in the listview
This commit is contained in:
Finn Stutzenstein 2019-04-15 08:10:01 +02:00 committed by GitHub
commit e7624c0d1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 398 additions and 295 deletions

View File

@ -0,0 +1,113 @@
<os-meta-text-block showActionRow="false" *ngIf="projector">
<ng-container class="meta-text-block-title">
{{ projector.getTitle() | translate }}
</ng-container>
<ng-container class="meta-text-block-action-row" *ngIf="canManage">
<button mat-icon-button *ngIf="!isEditing" (click)="onEditButton()">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button *ngIf="isEditing" (click)="onCancelButton()">
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button *ngIf="isEditing" (click)="onSaveButton()">
<mat-icon>save</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDeleteButton()">
<mat-icon>delete</mat-icon>
</button>
</ng-container>
<ng-container class="meta-text-block-content">
<a class="no-markup" [routerLink]="['/projectors/detail', projector.id]">
<div class="projector">
<os-projector [projector]="projector"></os-projector>
</div>
</a>
<ng-container *ngIf="isEditing">
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
<!-- Name field -->
<mat-form-field>
<input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<h3 translate>Resolution and size</h3>
<!-- Aspect ratio field -->
<mat-radio-group formControlName="aspectRatio" [name]="projector.id">
<mat-radio-button *ngFor="let ratio of aspectRatiosKeys" [value]="ratio">
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
<mat-slider
[thumbLabel]="true"
min="800"
max="3840"
step="10"
(change)="widthSliderValueChanged($event)"
></mat-slider>
{{ updateForm.value.width }}
<!-- projection defaults -->
<h3 translate>Projection defaults</h3>
<mat-select formControlName="projectiondefaults_id" placeholder="{{ 'Projection defaults' | translate }}" [multiple]="true">
<mat-option *ngFor="let pd of projectionDefaults" [value]="pd.id">
{{ pd.getTitle() | translate }}
</mat-option>
</mat-select>
<!-- colors -->
<mat-form-field>
<span translate>Background color</span>
<input matInput formControlName="background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header background color</span>
<input matInput formControlName="header_background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header font color</span>
<input matInput formControlName="header_font_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_font_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Headline color</span>
<input matInput formControlName="header_h1_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_h1_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<!-- checkboxes -->
<div>
<mat-checkbox formControlName="show_header_footer">
<span translate>Show header and footer</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_title">
<span translate>Show title</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_logo">
<span translate>Show logo</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="clock">
<span translate>Show clock</span>
</mat-checkbox>
</div>
</form>
</ng-container>
</ng-container>
</os-meta-text-block>

View File

@ -0,0 +1,19 @@
.projector {
width: 320px;
color: black;
border: 1px solid lightgrey;
}
form {
margin-top: 10px;
}
::ng-deep mat-card {
margin: 0;
}
.no-markup {
/* Do not let the a tag ruin the projector */
color: inherit;
text-decoration: inherit;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from '../../../../../e2e-imports.module';
import { ProjectorModule } from '../../projector.module';
import { ProjectorListEntryComponent } from './projector-list-entry.component';
describe('ProjectorListEntryComponent', () => {
let component: ProjectorListEntryComponent;
let fixture: ComponentFixture<ProjectorListEntryComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule, ProjectorModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectorListEntryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,229 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatSliderChange } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { ViewProjector } from '../../models/view-projector';
import { Projector } from 'app/shared/models/core/projector';
import { BaseViewComponent } from 'app/site/base/base-view';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ClockSlideService } from '../../services/clock-slide.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
/**
* All supported aspect rations for projectors.
*/
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10
};
/**
* List for all projectors.
*/
@Component({
selector: 'os-projector-list-entry',
templateUrl: './projector-list-entry.component.html',
styleUrls: ['./projector-list-entry.component.scss']
})
export class ProjectorListEntryComponent extends BaseViewComponent implements OnInit {
/**
* The update form. Will be refreahed for each projector. Just one update
* form can be shown per time.
*/
public updateForm: FormGroup;
/**
* Saves, if this projector currently is edited.
*/
public isEditing = false;
/**
* All ProjectionDefaults to select from.
*/
public projectionDefaults: ViewProjectionDefault[];
/**
* All aspect ratio keys/strings for the UI.
*/
public aspectRatiosKeys: string[];
/**
* The projector shown by this entry.
*/
@Input()
public projector: ViewProjector;
/**
* Helper to check manage permissions
*
* @returns true if the user can manage projectors
*/
public get canManage(): boolean {
return this.operator.hasPerms('core.can_manage_projector');
}
/**
* Constructor. Initializes the update form.
*
* @param titleService
* @param translate
* @param matSnackBar
* @param repo
* @param formBuilder
* @param promptService
* @param clockSlideService
* @param operator OperatorService
*/
public constructor(
titleService: Title,
protected translate: TranslateService, // protected required for ng-translate-extract
matSnackBar: MatSnackBar,
private repo: ProjectorRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService,
private clockSlideService: ClockSlideService,
private operator: OperatorService,
private projectionDefaultRepo: ProjectionDefaultRepositoryService
) {
super(titleService, translate, matSnackBar);
this.aspectRatiosKeys = Object.keys(aspectRatios);
this.updateForm = this.formBuilder.group({
name: ['', Validators.required],
aspectRatio: ['', Validators.required],
width: [0, Validators.required],
projectiondefaults_id: [[]],
clock: [true],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
header_h1_color: ['', Validators.required],
show_header_footer: [],
show_title: [],
show_logo: []
});
}
/**
* Watches all projectiondefaults.
*/
public ngOnInit(): void {
this.projectionDefaults = this.projectionDefaultRepo.getViewModelList();
this.subscriptions.push(
this.projectionDefaultRepo.getViewModelListObservable().subscribe(pds => (this.projectionDefaults = pds))
);
}
/**
* Event on Key Down in update form.
*
* @param event the keyboard event
* @param the current view in scope
*/
public keyDownFunction(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.onSaveButton();
}
if (event.key === 'Escape') {
this.onCancelButton();
}
}
/**
* Calculates the aspect ratio of the given projector.
* If no matching ratio is found, the first ratio is returned.
*
* @param projector The projector to check
* @returns the found ratio key.
*/
public getAspectRatioKey(): string {
const ratio = this.projector.width / this.projector.height;
const RATIO_ENVIRONMENT = 0.05;
const foundRatioKey = Object.keys(aspectRatios).find(key => {
const value = aspectRatios[key];
return value >= ratio - RATIO_ENVIRONMENT && value <= ratio + RATIO_ENVIRONMENT;
});
if (!foundRatioKey) {
return Object.keys(aspectRatios)[0];
} else {
return foundRatioKey;
}
}
/**
* Starts editing for the given projector.
*/
public onEditButton(): void {
if (this.isEditing) {
return;
}
this.isEditing = true;
this.updateForm.reset();
this.updateForm.patchValue(this.projector.projector);
this.updateForm.patchValue({
name: this.translate.instant(this.projector.name),
aspectRatio: this.getAspectRatioKey(),
clock: this.clockSlideService.isProjectedOn(this.projector)
});
}
/**
* Cancels the current editing.
*/
public onCancelButton(): void {
this.isEditing = false;
}
/**
* Saves the projector
*
* @param projector The projector to save.
*/
public async onSaveButton(): Promise<void> {
const updateProjector: Partial<Projector> = this.updateForm.value;
updateProjector.height = Math.round(
this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio]
);
try {
await this.clockSlideService.setProjectedOn(this.projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, this.projector);
this.isEditing = false;
} catch (e) {
this.raiseError(e);
}
}
/**
* Delete the projector.
*/
public async onDeleteButton(): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this projector?');
if (await this.promptService.open(title, this.projector.name)) {
this.repo.delete(this.projector).then(null, this.raiseError);
}
}
/**
* Eventhandler for slider changes. Directly saves the new aspect ratio.
*
* @param event The slider value
*/
public widthSliderValueChanged(event: MatSliderChange): void {
const aspectRatio = this.getAspectRatioKey();
const updateProjector: Partial<Projector> = {
width: event.value
};
updateProjector.height = Math.round(event.value / aspectRatios[aspectRatio]);
this.repo.update(updateProjector, this.projector).then(null, this.raiseError);
}
}

View File

@ -45,120 +45,7 @@
</mat-card> </mat-card>
<div id="card-wrapper"> <div id="card-wrapper">
<div class="projector-card" *ngFor="let projector of projectors"> <div class="projector-card" *ngFor="let projector of projectors; trackBy: trackByIndex">
<os-meta-text-block showActionRow="false"> <os-projector-list-entry [projector]="projector"></os-projector-list-entry>
<ng-container class="meta-text-block-title">
{{ projector.name | translate }}
</ng-container>
<ng-container class="meta-text-block-action-row" *ngIf="canManage">
<button mat-icon-button *ngIf="editId !== projector.id" (click)="onEditButton(projector)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button *ngIf="editId === projector.id" (click)="onCancelButton(projector)">
<mat-icon>close</mat-icon>
</button>
<button mat-icon-button *ngIf="editId === projector.id" (click)="onSaveButton(projector)">
<mat-icon>save</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDeleteButton(projector)">
<mat-icon>delete</mat-icon>
</button>
</ng-container>
<ng-container class="meta-text-block-content">
<a class="no-markup" [routerLink]="['/projectors/detail', projector.id]">
<div class="projector">
<os-projector [projector]="projector"></os-projector>
</div>
</a>
<ng-container *ngIf="editId === projector.id">
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
<!-- Name field -->
<mat-form-field>
<input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<h3 translate>Resolution and size</h3>
<!-- Aspect ratio field -->
<mat-radio-group formControlName="aspectRatio" [name]="projector.id">
<mat-radio-button *ngFor="let ratio of aspectRatiosKeys" [value]="ratio">
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
<mat-slider
[thumbLabel]="true"
formControlName="width"
min="800"
max="3840"
step="10"
(change)="widthSliderValueChanged(projector, $event)"
></mat-slider>
{{ updateForm.value.width }}
<!-- projection defaults -->
<h3 translate>Projection defaults</h3>
<mat-select formControlName="projectiondefaults_id" placeholder="{{ 'Projection defaults' | translate }}" [multiple]="true">
<mat-option *ngFor="let pd of projectionDefaults" [value]="pd.id">
{{ pd.getTitle() | translate }}
</mat-option>
</mat-select>
<!-- colors -->
<mat-form-field>
<span translate>Background color</span>
<input matInput formControlName="background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header background color</span>
<input matInput formControlName="header_background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header font color</span>
<input matInput formControlName="header_font_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_font_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Headline color</span>
<input matInput formControlName="header_h1_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_h1_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<!-- checkboxes -->
<div>
<mat-checkbox formControlName="show_header_footer">
<span translate>Show header and footer</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_title">
<span translate>Show title</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_logo">
<span translate>Show logo</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="clock">
<span translate>Show clock</span>
</mat-checkbox>
</div>
</form>
</ng-container>
</ng-container>
</os-meta-text-block>
</div> </div>
</div> </div>

View File

@ -6,25 +6,5 @@
width: 350px; width: 350px;
margin: 10px; margin: 10px;
float: left; float: left;
.projector {
width: 320px;
color: black;
border: 1px solid lightgrey;
}
form {
margin-top: 10px;
}
::ng-deep mat-card {
margin: 0;
}
.no-markup {
/* Do not let the a tag ruin the projector */
color: inherit;
text-decoration: inherit;
}
} }
} }

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar, MatSelectChange, MatSliderChange } from '@angular/material'; import { MatSnackBar, MatSelectChange } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -9,20 +9,7 @@ import { ProjectorRepositoryService } from 'app/core/repositories/projector/proj
import { ViewProjector } from '../../models/view-projector'; import { ViewProjector } from '../../models/view-projector';
import { Projector } from 'app/shared/models/core/projector'; import { Projector } from 'app/shared/models/core/projector';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { ClockSlideService } from '../../services/clock-slide.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
/**
* All supported aspect rations for projectors.
*/
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10
};
/** /**
* List for all projectors. * List for all projectors.
@ -43,17 +30,6 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
*/ */
public createForm: FormGroup; public createForm: FormGroup;
/**
* The update form. Will be refreahed for each projector. Just one update
* form can be shown per time.
*/
public updateForm: FormGroup;
/**
* The id of the currently edited projector.
*/
public editId: number | null = null;
/** /**
* All aspect ratio keys/strings for the UI. * All aspect ratio keys/strings for the UI.
*/ */
@ -64,8 +40,6 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
*/ */
public projectors: ViewProjector[]; public projectors: ViewProjector[];
public projectionDefaults: ViewProjectionDefault[];
/** /**
* Helper to check manage permissions * Helper to check manage permissions
* *
@ -93,32 +67,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private repo: ProjectorRepositoryService, private repo: ProjectorRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private promptService: PromptService, private operator: OperatorService
private clockSlideService: ClockSlideService,
private operator: OperatorService,
private projectionDefaultRepo: ProjectionDefaultRepositoryService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
this.aspectRatiosKeys = Object.keys(aspectRatios);
this.createForm = this.formBuilder.group({ this.createForm = this.formBuilder.group({
name: ['', Validators.required] name: ['', Validators.required]
}); });
this.updateForm = this.formBuilder.group({
name: ['', Validators.required],
aspectRatio: ['', Validators.required],
width: [0, Validators.required],
projectiondefaults_id: [[]],
clock: [true],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
header_h1_color: ['', Validators.required],
show_header_footer: [],
show_title: [],
show_logo: []
});
} }
/** /**
@ -128,8 +83,6 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
super.setTitle('Projectors'); super.setTitle('Projectors');
this.projectors = this.repo.getViewModelList(); this.projectors = this.repo.getViewModelList();
this.repo.getViewModelListObservable().subscribe(projectors => (this.projectors = projectors)); this.repo.getViewModelListObservable().subscribe(projectors => (this.projectors = projectors));
this.projectionDefaults = this.projectionDefaultRepo.getViewModelList();
this.projectionDefaultRepo.getViewModelListObservable().subscribe(pds => (this.projectionDefaults = pds));
} }
/** /**
@ -159,110 +112,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
* Event on Key Down in update or create form. * Event on Key Down in update or create form.
* *
* @param event the keyboard event * @param event the keyboard event
* @param the current view in scope
*/ */
public keyDownFunction(event: KeyboardEvent, projector?: ViewProjector): void { public keyDownFunction(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) { if (event.key === 'Enter') {
if (projector) { this.create();
this.onSaveButton(projector);
} else {
this.create();
}
} }
if (event.key === 'Escape') { if (event.key === 'Escape') {
if (projector) { this.projectorToCreate = null;
this.onCancelButton(projector);
} else {
this.projectorToCreate = null;
}
}
}
/**
* Calculates the aspect ratio of the given projector.
* If no matching ratio is found, the first ratio is returned.
*
* @param projector The projector to check
* @returns the found ratio key.
*/
public getAspectRatioKey(projector: ViewProjector): string {
const ratio = projector.width / projector.height;
const RATIO_ENVIRONMENT = 0.05;
const foundRatioKey = Object.keys(aspectRatios).find(key => {
const value = aspectRatios[key];
return value >= ratio - RATIO_ENVIRONMENT && value <= ratio + RATIO_ENVIRONMENT;
});
if (!foundRatioKey) {
return Object.keys(aspectRatios)[0];
} else {
return foundRatioKey;
}
}
/**
* Starts editing for the given projector.
*
* @param projector The projector to edit
*/
public onEditButton(projector: ViewProjector): void {
if (this.editId !== null) {
return;
}
this.editId = projector.id;
this.updateForm.reset();
this.updateForm.patchValue(projector.projector);
this.updateForm.patchValue({
name: this.translate.instant(projector.name),
aspectRatio: this.getAspectRatioKey(projector),
clock: this.clockSlideService.isProjectedOn(projector)
});
}
/**
* Cancels the current editing.
* @param projector the projector
*/
public onCancelButton(projector: ViewProjector): void {
if (projector.id !== this.editId) {
return;
}
this.editId = null;
}
/**
* Saves the projector
*
* @param projector The projector to save.
*/
public async onSaveButton(projector: ViewProjector): Promise<void> {
if (projector.id !== this.editId || !this.updateForm.valid) {
return;
}
const updateProjector: Partial<Projector> = this.updateForm.value;
updateProjector.height = Math.round(
this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio]
);
try {
await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, projector);
this.editId = null;
} catch (e) {
this.raiseError(e);
}
}
/**
* Delete the projector.
*
* @param projector The projector to delete
*/
public async onDeleteButton(projector: ViewProjector): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this projector?');
const content = projector.name;
if (await this.promptService.open(title, content)) {
this.repo.delete(projector).then(null, this.raiseError);
} }
} }
@ -275,13 +131,4 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
}); });
Promise.all(promises).then(null, this.raiseError); Promise.all(promises).then(null, this.raiseError);
} }
public widthSliderValueChanged(projector: ViewProjector, event: MatSliderChange): void {
const aspectRatio = this.getAspectRatioKey(projector);
const updateProjector: Partial<Projector> = {
width: event.value
};
updateProjector.height = Math.round(event.value / aspectRatios[aspectRatio]);
this.repo.update(updateProjector, projector).then(null, this.raiseError);
}
} }

View File

@ -9,11 +9,13 @@ import { CountdownControlsComponent } from './components/countdown-controls/coun
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 { ProjectorListEntryComponent } from './components/projector-list-entry/projector-list-entry.component';
@NgModule({ @NgModule({
imports: [CommonModule, ProjectorRoutingModule, SharedModule], imports: [CommonModule, ProjectorRoutingModule, SharedModule],
declarations: [ declarations: [
ProjectorListComponent, ProjectorListComponent,
ProjectorListEntryComponent,
ProjectorDetailComponent, ProjectorDetailComponent,
CountdownControlsComponent, CountdownControlsComponent,
CountdownDialogComponent, CountdownDialogComponent,