Enhance projector list

- The projector list now scales to give a better overview
- selecting the projector for the CLOS reference is more intuitive
- editing and creating projectors now works over a dialog
- editing projectors is now possible from the detail page
- projector tiles look overall cleaner
- Editing the projector offers a preview
- no changes "on the fly"
- Dialog has apply button to allow saving without closing
- The slider has an input fild on the right side to allow the usage
  of specific values
This commit is contained in:
Sean Engelhardt 2019-10-23 12:22:52 +02:00
parent f62b506dee
commit 233961b466
17 changed files with 717 additions and 605 deletions

View File

@ -1,5 +1,5 @@
<div id="container" #container [osResized]="resizeSubject">
<div id="projector" [ngStyle]="projectorStyle">
<os-projector [projector]="projector"></os-projector>
<os-projector *ngIf="projector" [projector]="projector"></os-projector>
</div>
</div>

View File

@ -9,6 +9,7 @@ import { OfflineService } from 'app/core/core-services/offline.service';
import { ProjectorDataService, SlideData } from 'app/core/core-services/projector-data.service';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { Projector } from 'app/shared/models/core/projector';
import { ViewProjector } from 'app/site/projector/models/view-projector';
import { Size } from 'app/site/projector/size';
@ -27,40 +28,13 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
*/
private projectorId: number | null = null;
/**
* The projector. Accessors are below.
*/
private _projector: ViewProjector;
@Input()
public set projector(projector: ViewProjector) {
this._projector = projector;
// check, if ID changed:
const newId = projector ? projector.id : null;
if (this.projectorId !== newId) {
this.projectorIdChanged(this.projectorId, newId);
this.projectorId = newId;
this.setProjector(projector.projector);
}
// Update scaling, if projector is set.
if (projector) {
const oldSize: Size = { ...this.currentProjectorSize };
this.currentProjectorSize.height = projector.height;
this.currentProjectorSize.width = projector.width;
if (
oldSize.height !== this.currentProjectorSize.height ||
oldSize.width !== this.currentProjectorSize.width
) {
this.updateScaling();
}
this.css.projector.color = projector.color;
this.css.projector.backgroundColor = projector.background_color;
this.css.projector.H1Color = this.projector.header_h1_color;
this.css.headerFooter.color = projector.header_font_color;
this.css.headerFooter.backgroundColor = projector.header_background_color;
this.updateCSS();
}
}
private _projector: ViewProjector;
public get projector(): ViewProjector {
return this._projector;
@ -240,6 +214,39 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
this.offlineSubscription = this.offlineService.isOffline().subscribe(isOffline => (this.isOffline = isOffline));
}
/**
* Regular routine to set a projector
*
* @param projector
*/
public setProjector(projector: Projector): void {
// check, if ID changed:
const newId = projector ? projector.id : null;
if (this.projectorId !== newId) {
this.projectorIdChanged(this.projectorId, newId);
this.projectorId = newId;
}
// Update scaling, if projector is set.
if (projector) {
const oldSize: Size = { ...this.currentProjectorSize };
this.currentProjectorSize.height = projector.height;
this.currentProjectorSize.width = projector.width;
if (
oldSize.height !== this.currentProjectorSize.height ||
oldSize.width !== this.currentProjectorSize.width
) {
this.updateScaling();
}
this.css.projector.color = projector.color;
this.css.projector.backgroundColor = projector.background_color;
this.css.projector.H1Color = projector.header_h1_color;
this.css.headerFooter.color = projector.header_font_color;
this.css.headerFooter.backgroundColor = projector.header_background_color;
this.updateCSS();
}
}
/**
* Scales the projector to the right format.
*/

View File

@ -3,15 +3,15 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { E2EImportsModule } from 'e2e-imports.module';
import { MessageData, MessageDialogComponent } from './message-dialog.component';
import { MessageDialogComponent } from './message-dialog.component';
describe('MessageDialogComponent', () => {
let component: MessageDialogComponent;
let fixture: ComponentFixture<MessageDialogComponent>;
const dialogData: MessageData = {
text: ''
};
// const dialogData: MessageData = {
// text: ''
// };
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -21,7 +21,7 @@ describe('MessageDialogComponent', () => {
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: dialogData
useValue: {}
}
]
}).compileComponents();

View File

@ -1,4 +1,4 @@
<os-head-bar [nav]="false" [goBack]="true">
<os-head-bar [nav]="false" [hasMainButton]="true" mainButtonIcon="edit" [goBack]="true" (mainEvent)="editProjector()">
<!-- Title -->
<div class="title-slot">
<h2>{{ projector?.name | translate }}</h2>

View File

@ -20,7 +20,7 @@ import { SizeObject } from 'app/shared/components/tile/tile.component';
import { Countdown } from 'app/shared/models/core/countdown';
import { ProjectorElement } from 'app/shared/models/core/projector';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { infoDialogSettings, mediumDialogSettings } from 'app/shared/utils/dialog-settings';
import { infoDialogSettings, largeDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseViewComponent } from 'app/site/base/base-view';
import { Projectable } from 'app/site/base/projectable';
import { ViewCountdown } from 'app/site/projector/models/view-countdown';
@ -30,6 +30,7 @@ import { CountdownData, CountdownDialogComponent } from '../countdown-dialog/cou
import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service';
import { CurrentSpeakerChyronSlideService } from '../../services/current-speaker-chyron-slide.service';
import { MessageData, MessageDialogComponent } from '../message-dialog/message-dialog.component';
import { ProjectorEditDialogComponent } from '../projector-edit-dialog/projector-edit-dialog.component';
import { ViewProjector } from '../../models/view-projector';
/**
@ -106,18 +107,34 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public ngOnInit(): void {
this.route.params.subscribe(params => {
const projectorId = parseInt(params.id, 10) || 1;
this.subscriptions.push(
this.repo.getViewModelObservable(projectorId).subscribe(projector => {
if (projector) {
const title = projector.name;
super.setTitle(title);
this.projector = projector;
}
});
})
);
});
this.subscriptions.push(timer(0, 500).subscribe(() => this.cd.detectChanges()));
}
public editProjector(): void {
const dialogRef = this.dialog.open(ProjectorEditDialogComponent, {
data: this.projector,
...largeDialogSettings
});
dialogRef.afterClosed().subscribe(event => {
if (event) {
this.cd.detectChanges();
}
});
}
/**
* Change the scroll
*
@ -252,7 +269,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
const dialogRef = this.dialog.open(MessageDialogComponent, {
data: messageData,
...mediumDialogSettings
...largeDialogSettings
});
dialogRef.afterClosed().subscribe(result => {

View File

@ -0,0 +1,177 @@
<h1 mat-dialog-title>
<span translate>Edit Projector</span>
</h1>
<div class="settings-grid">
<form [formGroup]="updateForm" (ngSubmit)="onSubmitProjector()">
<div mat-dialog-content *ngIf="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="aspectRatio">
<mat-radio-button
*ngFor="let ratio of aspectRatiosKeys"
[value]="ratio"
(change)="aspectRatioChanged($event)"
>
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
<div class="spacer-top-20 grid-form">
<mat-slider
class="grid-start"
formControlName="width"
[thumbLabel]="true"
[min]="getMinWidth()"
[max]="maxResolution"
[step]="resolutionChangeStep"
[value]="updateForm.value.width"
></mat-slider>
<div class="grid-end">
<mat-form-field>
<input
matInput
type="number"
formControlName="width"
[min]="getMinWidth()"
[max]="maxResolution"
[step]="resolutionChangeStep"
[value]="updateForm.value.width"
/>
</mat-form-field>
</div>
</div>
<!-- checkboxes -->
<div>
<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>
</div>
<!-- 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 -->
<div class="spacer-top-10">
<!-- Template to streamline all the color forms -->
<ng-template #colorFormField let-title="title" let-form="form">
<div class="grid-form">
<div class="grid-start">
<mat-form-field class="color-picker-form">
<span>{{ title | translate }}</span>
<input matInput [formControlName]="form" type="color" />
</mat-form-field>
</div>
<div class="grid-end">
<button
type="button"
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField(form)"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Foreground color', form: 'color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Background color', form: 'background_color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Header background color', form: 'header_background_color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Header font color', form: 'header_font_color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Headline color', form: 'header_h1_color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Chyron background color', form: 'chyron_background_color' }"
>
</ng-template>
<ng-template
[ngTemplateOutlet]="colorFormField"
[ngTemplateOutletContext]="{ title: 'Chyron font color', form: 'chyron_font_color' }"
>
</ng-template>
</div>
</div>
<!-- Actions -->
<div mat-dialog-actions>
<mat-divider></mat-divider>
<button type="submit" mat-button color="primary">
<span translate>Save</span>
</button>
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
<button type="button" mat-button (click)="applyChanges()">
<span translate>Apply</span>
</button>
</div>
</form>
<div>
<h3 translate>Preview</h3>
<div>
<os-projector #preview *ngIf="previewProjector" [projector]="previewProjector"></os-projector>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
@import '~assets/styles/variables.scss';
form {
overflow: hidden;
}
.settings-grid {
display: grid;
grid-gap: 10px;
@include desktop {
grid-template-columns: 40% 60%;
}
.mat-form-field {
width: 100%;
}
}
.grid-form {
width: 100%;
display: grid;
grid-template-columns: auto 50px;
.grid-start {
grid-column-start: 1;
grid-column-end: 1;
width: 100%;
}
.grid-end {
grid-column-start: 2;
grid-column-end: 2;
margin: auto 0;
}
.color-picker-form {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
.mat-form-field-underline {
display: none;
}
}
.no-markup {
/* Do not let the a tag ruin the projector */
color: inherit;
text-decoration: inherit;
}
.mat-dialog-actions {
display: block;
}

View File

@ -0,0 +1,41 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { E2EImportsModule } from 'e2e-imports.module';
import { ProjectorEditDialogComponent } from './projector-edit-dialog.component';
describe('ProjectorEditDialogComponent', () => {
let component: ProjectorEditDialogComponent;
let fixture: ComponentFixture<ProjectorEditDialogComponent>;
/**
* A view model has to be injected here, hence it's currently not possbile (anymore)
* to mock the creation of view models
*/
const dialogData = null;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProjectorEditDialogComponent],
imports: [E2EImportsModule],
providers: [
{ provide: MatDialogRef, useValue: {} },
{
provide: MAT_DIALOG_DATA,
useValue: dialogData
}
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProjectorEditDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,258 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef, MatRadioChange, MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { auditTime } from 'rxjs/operators';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { ProjectorComponent } from 'app/shared/components/projector/projector.component';
import { Projector } from 'app/shared/models/core/projector';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ClockSlideService } from '../../services/clock-slide.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
import { ViewProjector } from '../../models/view-projector';
/**
* All supported aspect rations for projectors.
*/
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10,
'30:9': 30 / 9
};
const aspectRatio_30_9_MinWidth = 1150;
/**
* Dialog to edit the given projector
* Shows a preview
*/
@Component({
selector: 'os-projector-edit-dialog',
templateUrl: './projector-edit-dialog.component.html',
styleUrls: ['./projector-edit-dialog.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectorEditDialogComponent extends BaseViewComponent implements OnInit {
/**
* import the projector as view child, to determine when to update
* the preview.
*/
@ViewChild('preview', { static: false })
public preview: ProjectorComponent;
/**
* The update form. Will be refreahed for each projector. Just one update
* form can be shown per time.
*/
public updateForm: FormGroup;
/**
* All aspect ratio keys/strings for the UI.
*/
public aspectRatiosKeys: string[];
/**
* All ProjectionDefaults to select from.
*/
public projectionDefaults: ViewProjectionDefault[];
/**
* show a preview of the changes
*/
public previewProjector: Projector;
/**
* define the maximum resolution
*/
public maxResolution = 2000;
/**
* Define the step of resolution changes
*/
public resolutionChangeStep = 10;
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
formBuilder: FormBuilder,
@Inject(MAT_DIALOG_DATA) public projector: ViewProjector,
private dialogRef: MatDialogRef<ProjectorEditDialogComponent>,
private repo: ProjectorRepositoryService,
private projectionDefaultRepo: ProjectionDefaultRepositoryService,
private clockSlideService: ClockSlideService,
private cd: ChangeDetectorRef
) {
super(title, translate, matSnackBar);
this.aspectRatiosKeys = Object.keys(aspectRatios);
if (projector) {
this.previewProjector = new Projector(projector.getModel());
}
this.updateForm = formBuilder.group({
name: ['', Validators.required],
aspectRatio: ['', Validators.required],
width: [0, Validators.required],
projectiondefaults_id: [[]],
clock: [true],
color: ['', Validators.required],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
header_h1_color: ['', Validators.required],
chyron_background_color: ['', Validators.required],
chyron_font_color: ['', Validators.required],
show_header_footer: [],
show_title: [],
show_logo: []
});
// react to form changes
this.subscriptions.push(
this.updateForm.valueChanges.pipe(auditTime(100)).subscribe(() => {
this.onChangeForm();
})
);
}
/**
* Watches all projection defaults
*/
public ngOnInit(): void {
this.projectionDefaults = this.projectionDefaultRepo.getViewModelList();
this.subscriptions.push(
this.projectionDefaultRepo.getViewModelListObservable().subscribe(pds => (this.projectionDefaults = pds))
);
if (this.projector) {
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)
});
this.subscriptions.push(
this.repo.getViewModelObservable(this.projector.id).subscribe(update => {
// patches the projector with updated values
const projectorPatch = {};
Object.keys(this.updateForm.controls).forEach(ctrl => {
if (update[ctrl]) {
projectorPatch[ctrl] = update[ctrl];
}
});
this.updateForm.patchValue(projectorPatch);
})
);
}
}
/**
* Apply changes and close the dialog
*/
public async onSubmitProjector(): Promise<void> {
await this.applyChanges();
this.dialogRef.close(true);
}
/**
* Saves the current changes on the projector
*/
public async applyChanges(): Promise<void> {
const updateProjector: Partial<Projector> = this.updateForm.value;
updateProjector.height = this.calcHeight(this.updateForm.value.width, this.updateForm.value.aspectRatio);
try {
await this.clockSlideService.setProjectedOn(this.projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, this.projector);
} catch (e) {
this.raiseError(e);
}
}
/**
* React to form changes to update the preview
* @param previewUpdate
*/
public onChangeForm(): void {
if (this.previewProjector && this.projector) {
Object.assign(this.previewProjector, this.updateForm.value);
this.previewProjector.height = this.calcHeight(
this.updateForm.value.width,
this.updateForm.value.aspectRatio
);
this.preview.setProjector(this.previewProjector);
this.cd.markForCheck();
}
}
/**
* Helper to calc height
* @param width
* @param aspectRatio
*/
private calcHeight(width: number, aspectRatio: string): number {
return Math.round(width / aspectRatios[aspectRatio]);
}
/**
* Resets the given form field to the given default.
*/
public resetField(field: string): void {
const patchValue = {};
patchValue[field] = this.projector[field];
this.updateForm.patchValue(patchValue);
}
public aspectRatioChanged(event: MatRadioChange): void {
let width: number;
if (event.value === '30:9' && this.updateForm.value.width < aspectRatio_30_9_MinWidth) {
width = aspectRatio_30_9_MinWidth;
} else {
width = this.updateForm.value.width;
}
}
/**
* 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;
}
}
public getMinWidth(): number {
if (this.updateForm.value.aspectRatio === '30:9') {
return aspectRatio_30_9_MinWidth;
} else {
return 800;
}
}
}

View File

@ -1,18 +1,20 @@
<os-meta-text-block showActionRow="false" *ngIf="projector" [disableExpandControl]="true">
<os-meta-text-block class="projector-tile" showActionRow="false" *ngIf="projector" [disableExpandControl]="true">
<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()">
<ng-container class="meta-text-block-action-row" *osPerms="'core.can_manage_projector'">
<button
mat-icon-button
(click)="onSetAsClosRef()"
matTooltip="{{ 'Sets this projector as the reference for the current list of speakers' | translate }}"
>
<mat-icon *ngIf="this.projector.isReferenceProjector">star</mat-icon>
<mat-icon *ngIf="!this.projector.isReferenceProjector">star_border</mat-icon>
</button>
<button mat-icon-button (click)="editProjector()" matTooltip="{{ 'Edit this projector' | translate }}">
<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()">
<button mat-icon-button color="warn" (click)="onDeleteButton()" matTooltip="{{ 'Deletes this projector' | translate }}">
<mat-icon>delete</mat-icon>
</button>
</ng-container>
@ -22,221 +24,5 @@
<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"
(change)="aspectRatioChanged($event)"
>
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
<div class="spacer-top-20">
<mat-slider
[thumbLabel]="true"
[min]="getMinWidth()"
max="2000"
step="10"
value="{{ updateForm.value.width }}"
(change)="widthSliderValueChanged($event)"
></mat-slider>
{{ updateForm.value.width }}
</div>
<!-- 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 -->
<div class="color-field-wrapper">
<div class="form">
<mat-form-field>
<span translate>Foreground color</span>
<input matInput formControlName="color" type="color" />
<mat-hint *ngIf="!updateForm.controls.color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('color', '#000000')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<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>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('background_color', '#ffffff')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<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>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('header_background_color', '#317796')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<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>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('header_font_color', '#f5f5f5')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<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>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('header_h1_color', '#317796')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<mat-form-field>
<span translate>Chyron background color</span>
<input matInput formControlName="chyron_background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.chyron_background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('chyron_background_color', '#317796')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<mat-form-field>
<span translate>Chyron font color</span>
<input matInput formControlName="chyron_font_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.chyron_font_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</div>
<div class="reset-button">
<button
mat-icon-button
matTooltip="{{ 'Reset' | translate }}"
(click)="resetField('chyron_font_color', '#ffffff')"
>
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<!-- 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

@ -1,31 +1,21 @@
.projector-tile {
height: 100%;
display: block;
> div {
height: 100%;
.meta-text-block {
margin: 0 !important;
height: 100%;
}
}
}
.no-markup > div {
border: 1px solid lightgray;
}
.projector {
width: 320px;
color: black;
border: 1px solid lightgrey;
}
form {
margin-top: 10px;
}
.no-markup {
/* Do not let the a tag ruin the projector */
color: inherit;
text-decoration: inherit;
}
.color-field-wrapper {
width: 100%;
display: grid;
grid-template-columns: auto 30px;
.form {
grid-column-start: 1;
grid-column-end: 1;
}
.reset-button {
grid-column-start: 2;
grid-column-end: 2;
margin-top: 30px;
}
}

View File

@ -1,71 +1,33 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatRadioChange } from '@angular/material';
import { MatSliderChange } from '@angular/material/slider';
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { MatDialog } from '@angular/material';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { Projector } from 'app/shared/models/core/projector';
import { largeDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ClockSlideService } from '../../services/clock-slide.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
import { ProjectorEditDialogComponent } from '../projector-edit-dialog/projector-edit-dialog.component';
import { ViewProjector } from '../../models/view-projector';
/**
* All supported aspect rations for projectors.
*/
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10,
'30:9': 30 / 9
};
const aspectRatio_30_9_MinWidth = 1150;
/**
* List for all projectors.
*/
@Component({
selector: 'os-projector-list-entry',
templateUrl: './projector-list-entry.component.html',
styleUrls: ['./projector-list-entry.component.scss']
styleUrls: ['./projector-list-entry.component.scss'],
encapsulation: ViewEncapsulation.None
})
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 set projector(value: ViewProjector) {
this._projector = value;
this.updateForm.patchValue({ width: value.width });
}
public get projector(): ViewProjector {
@ -74,15 +36,6 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
private _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.
*
@ -100,142 +53,29 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
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
private dialogService: MatDialog
) {
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],
color: ['', Validators.required],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
header_h1_color: ['', Validators.required],
chyron_background_color: ['', Validators.required],
chyron_font_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;
}
}
public ngOnInit(): void {}
/**
* 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)
public editProjector(): void {
this.dialogService.open(ProjectorEditDialogComponent, {
data: this.projector,
...largeDialogSettings
});
}
/**
* Cancels the current editing.
* Handler to set the selected projector as CLOS reference
*/
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);
}
}
public aspectRatioChanged(event: MatRadioChange): void {
let width: number;
if (event.value === '30:9' && this.updateForm.value.width < aspectRatio_30_9_MinWidth) {
width = aspectRatio_30_9_MinWidth;
} else {
width = this.updateForm.value.width;
}
this.updateProjectorDimensions(width, event.value);
}
public getMinWidth(): number {
if (this.updateForm.value.aspectRatio === '30:9') {
return aspectRatio_30_9_MinWidth;
} else {
return 800;
}
public onSetAsClosRef(): void {
this.repo.setDefaultProjector(this.projector.id);
}
/**
@ -247,30 +87,4 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
this.repo.delete(this.projector).catch(this.raiseError);
}
}
/**
* Eventhandler for slider changes. Directly saves the new aspect ratio.
*
* @param event The slider value
*/
public widthSliderValueChanged(event: MatSliderChange): void {
this.updateProjectorDimensions(event.value, this.updateForm.value.aspectRatio);
}
private updateProjectorDimensions(width: number, aspectRatioKey: string): void {
const updateProjector: Partial<Projector> = {
width: width
};
updateProjector.height = Math.round(width / aspectRatios[aspectRatioKey]);
this.repo.update(updateProjector, this.projector).catch(this.raiseError);
}
/**
* Resets the given form field to the given default.
*/
public resetField(field: string, value: string): void {
const patchValue = {};
patchValue[field] = value;
this.updateForm.patchValue(patchValue);
}
}

View File

@ -1,51 +1,39 @@
<os-head-bar [nav]="true" [hasMainButton]="canManage" (mainEvent)="onPlusButton()">
<os-head-bar [nav]="true" [hasMainButton]="canManage" (mainEvent)="createNewProjector(projectorDialog)">
<!-- Title -->
<div class="title-slot">
<h2 translate>Projectors</h2>
</div>
</os-head-bar>
<mat-card *ngIf="!showCreateForm && projectors && projectors.length > 1">
<span translate> Reference projector for current list of speakers: </span>&nbsp;
<mat-form-field>
<mat-select
[disabled]="!!editId"
[value]="projectors.length ? projectors[0].reference_projector_id : null"
(selectionChange)="onSelectReferenceProjector($event)"
>
<mat-option *ngFor="let projector of projectors" [value]="projector.id">
{{ projector.getTitle() | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</mat-card>
<!-- Create projector dialog -->
<ng-template #projectorDialog>
<h1 mat-dialog-title>
<span translate>New Projector</span>
</h1>
<mat-card *ngIf="showCreateForm">
<mat-card-title translate>New Projector</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
<p>
<form [formGroup]="createForm">
<div mat-dialog-content>
<mat-form-field>
<input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!createForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="create()">
</div>
<div mat-dialog-actions>
<button type="submit" mat-button [disabled]="!createForm.valid" color="primary" [mat-dialog-close]="true">
<span translate>Create</span>
</button>
<button mat-button (click)="showCreateForm = false">
<button type="button" mat-button [mat-dialog-close]="null">
<span translate>Cancel</span>
</button>
</mat-card-actions>
</mat-card>
</div>
</form>
</ng-template>
<div id="card-wrapper">
<div class="projector-card" *ngFor="let projector of projectors; trackBy: trackByIndex">
<div class="projector-card" *ngFor="let projector of projectors | async; trackBy: trackByIndex">
<os-projector-list-entry [projector]="projector"></os-projector-list-entry>
</div>
</div>

View File

@ -1,10 +1,13 @@
#card-wrapper {
margin-top: 10px;
margin-left: 10px;
margin: 10px;
display: grid;
grid-gap: 10px;
// if there is only 1 and desktop size, use 0.5 fr
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
.projector-card {
width: 350px;
margin: 10px;
float: left;
width: 100%;
max-width: 100vmin;
max-height: 100vh;
}
}

View File

@ -5,19 +5,21 @@ import {
Component,
OnDestroy,
OnInit,
TemplateRef,
ViewEncapsulation
} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatSelectChange } from '@angular/material/select';
import { MatDialog } from '@angular/material';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { timer } from 'rxjs';
import { BehaviorSubject, timer } from 'rxjs';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { Projector } from 'app/shared/models/core/projector';
import { infoDialogSettings } from 'app/shared/utils/dialog-settings';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ViewProjector } from '../../models/view-projector';
@ -32,11 +34,6 @@ import { ViewProjector } from '../../models/view-projector';
encapsulation: ViewEncapsulation.None
})
export class ProjectorListComponent extends BaseViewComponent implements OnInit, AfterViewInit, OnDestroy {
/**
* This member is set, if the user is creating a new projector.
*/
public showCreateForm = false;
/**
* The create form.
*/
@ -50,7 +47,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
/**
* All projectors.
*/
public projectors: ViewProjector[];
public projectors: BehaviorSubject<ViewProjector[]>;
/**
* Helper to check manage permissions
@ -80,6 +77,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
private repo: ProjectorRepositoryService,
private formBuilder: FormBuilder,
private operator: OperatorService,
private dialogService: MatDialog,
private cd: ChangeDetectorRef
) {
super(titleService, translate, matSnackBar);
@ -103,8 +101,26 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
*/
public ngOnInit(): void {
super.setTitle('Projectors');
this.projectors = this.repo.getViewModelList();
this.repo.getViewModelListObservable().subscribe(projectors => (this.projectors = projectors));
this.projectors = this.repo.getViewModelListBehaviorSubject();
}
/**
* @param dialog
*/
public createNewProjector(dialog: TemplateRef<string>): void {
this.createForm.reset();
const dialogRef = this.dialogService.open(dialog, { ...infoDialogSettings, disableClose: true });
dialogRef.afterClosed().subscribe(result => {
if (result) {
const projectorToCreate: Partial<Projector> = {
name: this.createForm.value.name
};
this.repo.create(projectorToCreate).then(() => {
this.cd.detectChanges();
}, this.raiseError);
}
});
}
/**
@ -121,52 +137,4 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit,
super.ngOnDestroy();
this.cd.detach();
}
/**
* Opens the create form.
*/
public onPlusButton(): void {
if (!this.showCreateForm) {
this.showCreateForm = true;
this.createForm.setValue({ name: '' });
}
}
/**
* Creates the comment section from the create form.
*/
public create(): void {
if (this.createForm.valid && this.showCreateForm) {
const projector: Partial<Projector> = {
name: this.createForm.value.name,
reference_projector_id: this.projectors[0].reference_projector_id
};
this.repo.create(projector).then(() => {
this.showCreateForm = false;
this.cd.detectChanges();
}, this.raiseError);
}
}
/**
* Event on Key Down in update or create form.
*
* @param event the keyboard event
*/
public keyDownFunction(event: KeyboardEvent): void {
if (event.key === 'Enter') {
this.create();
}
if (event.key === 'Escape') {
this.showCreateForm = null;
}
}
/**
* Event handler when the reference projector is changed
* @param change the change event that contains the new id
*/
public onSelectReferenceProjector(change: MatSelectChange): void {
this.repo.setDefaultProjector(change.value).catch(this.raiseError);
}
}

View File

@ -16,6 +16,10 @@ export class ViewProjector extends BaseViewModel<Projector> {
public get non_stable_elements(): ProjectorElements {
return this.projector.elements.filter(element => !element.stable);
}
public get isReferenceProjector(): boolean {
return this.id === this.reference_projector_id;
}
}
interface IProjectorRelations {
referenceProjector: ViewProjector;

View File

@ -7,6 +7,7 @@ import { MessageControlsComponent } from './components/message-controls/message-
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 { ProjectorEditDialogComponent } from './components/projector-edit-dialog/projector-edit-dialog.component';
import { ProjectorListEntryComponent } from './components/projector-list-entry/projector-list-entry.component';
import { ProjectorListComponent } from './components/projector-list/projector-list.component';
import { ProjectorRoutingModule } from './projector-routing.module';
@ -22,13 +23,15 @@ import { SharedModule } from '../../shared/shared.module';
CountdownDialogComponent,
MessageControlsComponent,
MessageDialogComponent,
PresentationControlComponent
PresentationControlComponent,
ProjectorEditDialogComponent
],
entryComponents: [
CountdownDialogComponent,
MessageDialogComponent,
PresentationControlComponent,
ProjectorListEntryComponent
ProjectorListEntryComponent,
ProjectorEditDialogComponent
]
})
export class ProjectorModule {}