Merge pull request #4472 from tsiegleauq/countdown-control-component

Add Countdowns in projector detail
This commit is contained in:
Sean 2019-03-19 17:52:08 +01:00 committed by GitHub
commit 5fbf682754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 513 additions and 494 deletions

View File

@ -1,5 +1,9 @@
<div id="countdown" [ngClass]="{ <div
'negative': seconds <= 0, id="countdown"
'warning_time': seconds <= warningTime && seconds > 0 }"> [ngClass]="{
negative: seconds <= 0,
warning_time: seconds <= warningTime && seconds > 0
}"
>
{{ time }} {{ time }}
</div> </div>

View File

@ -10,9 +10,4 @@
color: mat-color($contrast, A200) !important; color: mat-color($contrast, A200) !important;
background-color: mat-color($contrast, 500) !important; background-color: mat-color($contrast, 500) !important;
} }
.mat-menu-item.projector-active,
.projector-active > .mat-icon {
color: mat-color($primary) !important;
}
} }

View File

@ -6,6 +6,7 @@ import {
isProjectable, isProjectable,
isProjectorElementBuildDeskriptor isProjectorElementBuildDeskriptor
} from 'app/site/base/projectable'; } from 'app/site/base/projectable';
import { Projector } from 'app/shared/models/core/projector';
import { ProjectorService } from 'app/core/core-services/projector.service'; import { ProjectorService } from 'app/core/core-services/projector.service';
import { ProjectionDialogService } from 'app/core/ui-services/projection-dialog.service'; import { ProjectionDialogService } from 'app/core/ui-services/projection-dialog.service';
@ -45,6 +46,12 @@ export class ProjectorButtonComponent implements OnInit {
@Input() @Input()
public menuItem = false; public menuItem = false;
/**
* Pre-define projection target
*/
@Input()
public projector: Projector;
/** /**
* The constructor * The constructor
*/ */
@ -68,7 +75,19 @@ export class ProjectorButtonComponent implements OnInit {
event.stopPropagation(); event.stopPropagation();
} }
if (this.object) { if (this.object) {
this.projectionDialogService.openProjectDialogFor(this.object); if (this.projector) {
// if the projection target was defines before
if (this.isProjected()) {
// remove the projected object
this.projectorService.removeFrom(this.projector, this.object);
} else {
// instantly project the object
this.projectorService.projectOn(this.projector, this.object);
}
} else {
// open the projection dialog
this.projectionDialogService.openProjectDialogFor(this.object);
}
} }
} }

View File

@ -1,12 +1,71 @@
<div *ngIf="countdown"> <mat-card *ngIf="countdown">
<os-countdown-time [countdown]="countdown" [warningTime]="warningTime"></os-countdown-time> <div class="grid-wrapper">
<button type="button" [disabled]="!canStop()" mat-icon-button (click)="stop($event)"> <div class="projector-button">
<mat-icon>stop</mat-icon> <os-projector-button [object]="countdown" [projector]="projector"></os-projector-button>
</button> </div>
<button *ngIf="!countdown.running" type="button" mat-icon-button (click)="start($event)">
<mat-icon>play_arrow</mat-icon> <div class="title">{{ countdown.getTitle() | translate }}</div>
</button>
<button *ngIf="countdown.running" type="button" mat-icon-button (click)="pause($event)"> <div class="action-buttons">
<mat-icon>pause</mat-icon> <button
</button> type="button"
</div> 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="timer">
<os-countdown-time
class="larger-countdown"
[countdown]="countdown"
[warningTime]="warningTime"
></os-countdown-time>
<div>
<button
type="button"
class="small-mat-icon-button"
mat-icon-button
[disabled]="!canStop()"
(click)="stop($event)"
>
<mat-icon>stop</mat-icon>
</button>
<button
type="button"
class="small-mat-icon-button"
mat-icon-button
(click)="start($event)"
*ngIf="!countdown.running"
>
<mat-icon>play_arrow</mat-icon>
</button>
<button
type="button"
class="small-mat-icon-button"
mat-icon-button
(click)="pause($event)"
*ngIf="countdown.running"
>
<mat-icon>pause</mat-icon>
</button>
</div>
</div>
</div>
</mat-card>

View File

@ -0,0 +1,62 @@
.grid-wrapper {
display: grid;
width: 100%;
grid-template-areas:
'project title buttons'
'project controls controls';
grid-gap: 10px;
grid-template-columns: min-content auto auto;
}
.projector-button {
grid-area: project;
margin: auto 10px auto 0;
}
.title {
grid-area: title;
padding: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-buttons {
grid-area: buttons;
text-align: right;
}
.timer {
grid-area: controls;
display: flex;
}
.larger-countdown {
font-size: 150%;
min-width: 60px;
margin-right: 10px;
}
// 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,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar } from '@angular/material';
@ -8,26 +8,60 @@ import { BaseViewComponent } from '../../../base/base-view';
import { ViewCountdown } from '../../models/view-countdown'; import { ViewCountdown } from '../../models/view-countdown';
import { CountdownRepositoryService } from 'app/core/repositories/projector/countdown-repository.service'; import { CountdownRepositoryService } from 'app/core/repositories/projector/countdown-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { Projector } from 'app/shared/models/core/projector';
import { ProjectionDialogService } from 'app/core/ui-services/projection-dialog.service';
/**
*
*/
@Component({ @Component({
selector: 'os-countdown-controls', selector: 'os-countdown-controls',
templateUrl: './countdown-controls.component.html' templateUrl: './countdown-controls.component.html',
styleUrls: ['./countdown-controls.component.scss']
}) })
export class CountdownControlsComponent extends BaseViewComponent { export class CountdownControlsComponent extends BaseViewComponent {
/**
* Countdown as input
*/
@Input() @Input()
public countdown: ViewCountdown; public countdown: ViewCountdown;
/**
* Edit event
*/
@Output()
public editEvent = new EventEmitter<ViewCountdown>();
/**
* Pre defined projection target (if any)
*/
@Input()
public projector: Projector;
/** /**
* The time in seconds to make the countdown orange, is the countdown is below this value. * The time in seconds to make the countdown orange, is the countdown is below this value.
*/ */
public warningTime: number; public warningTime: number;
/**
* Constructor
*
* @param titleService
* @param translate
* @param matSnackBar
* @param repo
* @param configService
* @param promptService
*/
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private repo: CountdownRepositoryService, private repo: CountdownRepositoryService,
private configService: ConfigService private configService: ConfigService,
private promptService: PromptService,
private projectionDialogService: ProjectionDialogService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
@ -64,4 +98,29 @@ export class CountdownControlsComponent extends BaseViewComponent {
public canStop(): boolean { public canStop(): boolean {
return this.countdown.running || this.countdown.countdown_time !== this.countdown.default_time; return this.countdown.running || this.countdown.countdown_time !== this.countdown.default_time;
} }
/**
* Fires an edit event
*/
public onEdit(): void {
this.editEvent.next(this.countdown);
}
/**
* Brings the projection dialog
*/
public onBringDialog(): void {
this.projectionDialogService.openProjectDialogFor(this.countdown);
}
/**
* On delete button
*/
public async onDelete(): Promise<void> {
const content =
this.translate.instant('Delete countdown') + ` ${this.translate.instant(this.countdown.title)}?`;
if (await this.promptService.open('Are you sure?', content)) {
this.repo.delete(this.countdown).then(() => {}, this.raiseError);
}
}
} }

View File

@ -0,0 +1,45 @@
<h1 mat-dialog-title>
<span translate>Countdown</span>
</h1>
<form [formGroup]="countdownForm">
<div mat-dialog-content>
<!-- Title field -->
<mat-form-field>
<input
matInput
osAutofocus
placeholder="{{ 'Countdown title' | translate }}"
formControlName="title"
required
/>
</mat-form-field>
<!-- Description field -->
<mat-form-field>
<input matInput placeholder="{{ 'Countdown description' | translate }}" formControlName="description" />
</mat-form-field>
<!-- Time field -->
<mat-form-field>
<input matInput placeholder="{{ 'Countdown time' | translate }}" formControlName="duration" required />
</mat-form-field>
</div>
<div mat-dialog-actions>
<!-- Submit countdown button -->
<button
type="submit"
mat-button
color="primary"
[mat-dialog-close]="countdownForm.value"
[disabled]="!countdownForm.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,3 @@
.mat-form-field {
width: 100%;
}

View File

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

View File

@ -0,0 +1,79 @@
import { Component, OnInit, Inject } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatSnackBar, MAT_DIALOG_DATA } from '@angular/material';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ConfigService } from 'app/core/ui-services/config.service';
import { DurationService } from 'app/core/ui-services/duration.service';
/**
* Countdown data for the form
*/
export interface CountdownData {
title: string;
description: string;
duration: string;
count?: number;
}
/**
* Dialog component for countdowns
*/
@Component({
selector: 'os-countdown-dialog',
templateUrl: './countdown-dialog.component.html',
styleUrls: ['./countdown-dialog.component.scss']
})
export class CountdownDialogComponent extends BaseViewComponent implements OnInit {
/**
* The form data
*/
public countdownForm: FormGroup;
/**
* Holds the default time which was set in the config
*/
private defaultTime: number;
/**
* Constructor
*
* @param title Title service. Required by parent
* @param matSnackBar Required by parent
* @param configService Read out config variables
* @param translate Required by parent
* @param formBuilder To build the form
* @param durationService Converts duration numbers to string
* @param data The mat dialog data, contains the values to display (if any)
*/
public constructor(
title: Title,
matSnackBar: MatSnackBar,
configService: ConfigService,
translate: TranslateService,
private formBuilder: FormBuilder,
private durationService: DurationService,
@Inject(MAT_DIALOG_DATA) public data: CountdownData
) {
super(title, translate, matSnackBar);
this.defaultTime = configService.instant<number>('projector_default_countdown');
}
/**
* Init. Creates the form
*/
public ngOnInit(): void {
const time = this.data.duration || this.durationService.durationToString(this.defaultTime, 'm');
const title = this.data.title || `${this.translate.instant('Countdown')} ${this.data.count + 1}`;
this.countdownForm = this.formBuilder.group({
title: [title, Validators.required],
description: [this.data.description],
// TODO: custom form validation. This needs to be a valid minute duration
duration: [time, Validators.required]
});
}
}

View File

@ -1,137 +0,0 @@
<os-head-bar [nav]="false" [goBack]="true" [mainButton]="true" (mainEvent)="onPlusButton()">
<!-- Title -->
<div class="title-slot">
<h2 translate>Countdowns</h2>
</div>
</os-head-bar>
<div class="head-spacer"></div>
<mat-card *ngIf="countdownToCreate">
<mat-card-title translate>New countdown</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm" (keydown)="onKeyDownCreate($event)">
<p>
<mat-form-field>
<input formControlName="title" matInput placeholder="{{ 'Title' | translate }}" required />
<mat-hint *ngIf="!createForm.controls.title.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
<p>
<mat-form-field>
<input formControlName="description" matInput placeholder="{{ 'Description' | translate }}" />
</mat-form-field>
</p>
<p>
<mat-form-field>
<input formControlName="default_time" matInput placeholder="{{ 'Time' | translate }}" required />
<mat-hint *ngIf="!createForm.controls.default_time.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</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 countdown of countdowns"
(opened)="openId = countdown.id"
(closed)="panelClosed(countdown)"
[expanded]="openId === countdown.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]="countdown"></os-projector-button>
</div>
<div class="header-name">
{{ countdown.getTitle() | translate }}
</div>
<div class="header-controls">
<os-countdown-controls [countdown]="countdown"></os-countdown-controls>
</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="updateForm" *ngIf="editId === countdown.id" (keydown)="onKeyDownUpdate($event)">
<h5 translate>Edit countdown</h5>
<p>
<mat-form-field>
<input formControlName="title" matInput placeholder="{{ 'Title' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.title.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
<p>
<mat-form-field>
<input
formControlName="description"
matInput
placeholder="{{ 'Description' | translate }}"
/>
</mat-form-field>
</p>
<p>
<mat-form-field>
<input formControlName="default_time" matInput placeholder="{{ 'Time' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.default_time.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
<mat-action-row>
<button
*ngIf="editId !== countdown.id"
mat-button
class="on-transition-fade"
(click)="onEditButton(countdown)"
mat-icon-button
>
<mat-icon>edit</mat-icon>
</button>
<button
*ngIf="editId === countdown.id"
mat-button
class="on-transition-fade"
(click)="onCancelUpdate()"
mat-icon-button
>
<mat-icon>close</mat-icon>
</button>
<button
*ngIf="editId === countdown.id"
mat-button
class="on-transition-fade"
(click)="onSaveButton(countdown)"
mat-icon-button
>
<mat-icon>save</mat-icon>
</button>
<button mat-button class="on-transition-fade" (click)="onDeleteButton(countdown)" mat-icon-button>
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
<mat-card *ngIf="countdowns.length === 0">
<mat-card-content>
<div class="no-content" translate>No countdowns</div>
</mat-card-content>
</mat-card>

View File

@ -1,41 +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 2fr;
width: 100%;
> div {
grid-row-start: 1;
grid-row-end: span 1;
grid-column-end: span 2;
}
.header-projector-button {
grid-column-start: 1;
grid-column-end: 2;
}
.header-name {
grid-column-start: 2;
grid-column-end: 3;
padding: 10px;
}
.header-controls {
grid-column-start: 3;
grid-column-end: 4;
}
}

View File

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

View File

@ -1,218 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { TranslateService } from '@ngx-translate/core';
import { PromptService } from 'app/core/ui-services/prompt.service';
import { BaseViewComponent } from '../../../base/base-view';
import { ViewCountdown } from '../../models/view-countdown';
import { CountdownRepositoryService } from 'app/core/repositories/projector/countdown-repository.service';
import { Countdown } from 'app/shared/models/core/countdown';
import { DurationService } from 'app/core/ui-services/duration.service';
/**
* List view for countdowns.
*/
@Component({
selector: 'os-countdown-list',
templateUrl: './countdown-list.component.html',
styleUrls: ['./countdown-list.component.scss']
})
export class CountdownListComponent extends BaseViewComponent implements OnInit {
public countdownToCreate: Countdown | null;
/**
* Source of the Data
*/
public countdowns: ViewCountdown[] = [];
/**
* 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: CountdownRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService,
private durationService: DurationService
) {
super(titleService, translate, matSnackBar);
const form = {
description: [''],
default_time: ['', Validators.required],
title: ['', Validators.required]
};
this.createForm = this.formBuilder.group(form);
this.updateForm = this.formBuilder.group(form);
}
/**
* Init function.
*
* Sets the title and gets/observes countdowns from DataStore
*/
public ngOnInit(): void {
super.setTitle('Countdowns');
this.repo.getViewModelListObservable().subscribe(newCountdowns => {
this.countdowns = newCountdowns;
});
}
/**
* Add a new countdown.
*/
public onPlusButton(): void {
if (!this.countdownToCreate) {
this.createForm.reset();
this.createForm.setValue({
description: '',
title: '',
default_time: '1:00 m'
});
this.countdownToCreate = new Countdown();
}
}
/**
* Handler when clicking on create to create a new statute paragraph
*/
public create(): void {
if (this.createForm.valid) {
let default_time = this.durationService.stringToDuration(this.createForm.value.default_time, 'm');
if (default_time === 0) {
default_time = 60;
}
const newValues: Partial<Countdown> = {
description: this.createForm.value.description,
title: this.createForm.value.title,
default_time: default_time
};
newValues.countdown_time = default_time;
this.countdownToCreate.patchValues(newValues);
this.repo.create(this.countdownToCreate).then(() => {
this.countdownToCreate = null;
}, this.raiseError);
}
}
/**
* Executed on edit button
* @param countdown
*/
public onEditButton(countdown: ViewCountdown): void {
this.editId = countdown.id;
this.updateForm.setValue({
description: countdown.description,
title: this.translate.instant(countdown.title),
default_time: this.durationService.durationToString(countdown.default_time, 'm')
});
}
/**
* Saves the countdown
* @param countdown The countdown to save
*/
public onSaveButton(countdown: ViewCountdown): void {
if (this.updateForm.valid) {
let default_time = this.durationService.stringToDuration(this.updateForm.value.default_time, 'm');
if (default_time === 0) {
default_time = 60;
}
const newValues: Partial<Countdown> = {
title: this.updateForm.value.title,
description: this.updateForm.value.description,
default_time: default_time
};
if (!countdown.running) {
newValues.countdown_time = default_time;
}
this.repo.update(newValues, countdown).then(() => {
this.openId = this.editId = null;
}, this.raiseError);
}
}
/**
* Is executed, when the delete button is pressed
*
* @param countdown The countdown to delete
*/
public async onDeleteButton(countdown: ViewCountdown): Promise<void> {
const title = this.translate.instant('Are you sure you want to delete this countdown?');
const content = countdown.title;
if (await this.promptService.open(title, content)) {
this.repo.delete(countdown).then(() => (this.openId = this.editId = null), this.raiseError);
}
}
/**
* Is executed when a mat-extension-panel is closed
*
* @param countdown the statute paragraph in the panel
*/
public panelClosed(countdown: ViewCountdown): void {
this.openId = null;
if (this.editId) {
this.onSaveButton(countdown);
}
}
/**
* 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.countdownToCreate = 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 countdown = this.countdowns.find(x => x.id === this.editId);
this.onSaveButton(countdown);
}
if (event.key === 'Escape') {
this.onCancelUpdate();
}
}
/**
* Cancels the current form action
*/
public onCancelUpdate(): void {
this.editId = null;
}
}

View File

@ -180,24 +180,28 @@
</mat-expansion-panel> </mat-expansion-panel>
<!-- countdowns --> <!-- countdowns -->
<mat-expansion-panel *ngIf="countdowns.length"> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<span translate>Countdowns</span> <span translate>Countdowns</span>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<mat-list> <mat-list>
<mat-list-item <mat-list-item
*ngFor="let countdown of countdowns" *ngFor="let countdown of countdowns"
class="larger-mat-list-item"
[ngClass]="{ projected: isProjected(countdown) }" [ngClass]="{ projected: isProjected(countdown) }"
> >
<button type="button" mat-icon-button (click)="project(countdown)"> <os-countdown-controls
<mat-icon>videocam</mat-icon> class="dynamic-list-entry"
</button> [countdown]="countdown"
{{ countdown.getTitle() | translate }} [projector]="projector.projector"
(editEvent)="openCountdownDialog($event)"
></os-countdown-controls>
</mat-list-item> </mat-list-item>
</mat-list> </mat-list>
<mat-action-row> <mat-action-row>
<button type="button" mat-icon-button routerLink="/projectors/countdowns"> <button type="button" mat-button (click)="openCountdownDialog()">
<mat-icon>edit</mat-icon> <mat-icon>add</mat-icon>
<span translate>Add countdown</span>
</button> </button>
</mat-action-row> </mat-action-row>
</mat-expansion-panel> </mat-expansion-panel>

View File

@ -121,3 +121,15 @@
.drop-list.cdk-drop-list-dragging .drop-list-entry:not(.cdk-drag-placeholder) { .drop-list.cdk-drop-list-dragging .drop-list-entry:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
} }
.larger-mat-list-item {
height: 90px;
}
.larger-mat-list-item + .larger-mat-list-item {
margin-top: 15px;
}
.dynamic-list-entry {
width: 100%;
}

View File

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material'; import { MatSnackBar, MatDialog } from '@angular/material';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -11,6 +11,11 @@ import {
} from 'app/core/repositories/projector/projector-repository.service'; } from 'app/core/repositories/projector/projector-repository.service';
import { ViewProjector } from '../../models/view-projector'; import { ViewProjector } from '../../models/view-projector';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { Countdown } from 'app/shared/models/core/countdown';
import { CountdownDialogComponent, CountdownData } from '../countdown-dialog/countdown-dialog.component';
import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service';
import { CurrentSpeakerChyronSlideService } from '../../services/current-speaker-chyron-slide.service';
import { DurationService } from 'app/core/ui-services/duration.service';
import { ProjectorService } from 'app/core/core-services/projector.service'; import { ProjectorService } from 'app/core/core-services/projector.service';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop'; import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { ProjectorElement } from 'app/shared/models/core/projector'; import { ProjectorElement } from 'app/shared/models/core/projector';
@ -20,8 +25,6 @@ import { ProjectorMessageRepositoryService } from 'app/core/repositories/project
import { ViewProjectorMessage } from 'app/site/projector/models/view-projector-message'; import { ViewProjectorMessage } from 'app/site/projector/models/view-projector-message';
import { ViewCountdown } from 'app/site/projector/models/view-countdown'; import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Projectable } from 'app/site/base/projectable'; import { Projectable } from 'app/site/base/projectable';
import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service';
import { CurrentSpeakerChyronSlideService } from '../../services/current-speaker-chyron-slide.service';
/** /**
* The projector detail view. * The projector detail view.
@ -61,6 +64,7 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private dialog: MatDialog,
private repo: ProjectorRepositoryService, private repo: ProjectorRepositoryService,
private route: ActivatedRoute, private route: ActivatedRoute,
private projectorService: ProjectorService, private projectorService: ProjectorService,
@ -68,7 +72,8 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
private countdownRepo: CountdownRepositoryService, private countdownRepo: CountdownRepositoryService,
private messageRepo: ProjectorMessageRepositoryService, private messageRepo: ProjectorMessageRepositoryService,
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService, private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
private currentSpeakerChyronService: CurrentSpeakerChyronSlideService private currentSpeakerChyronService: CurrentSpeakerChyronSlideService,
private durationService: DurationService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
@ -170,4 +175,63 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public toggleChyron(): void { public toggleChyron(): void {
this.currentSpeakerChyronService.toggleOn(this.projector); this.currentSpeakerChyronService.toggleOn(this.projector);
} }
/**
* Opens the countdown dialog
*
* @param viewCountdown optional existing countdown to edit
*/
public openCountdownDialog(viewCountdown?: ViewCountdown): void {
let countdownData: CountdownData = {
title: '',
description: '',
duration: '',
count: this.countdowns.length
};
if (viewCountdown) {
countdownData = {
title: viewCountdown.title,
description: viewCountdown.description,
duration: this.durationService.durationToString(viewCountdown.default_time, 'm')
};
}
const dialogRef = this.dialog.open(CountdownDialogComponent, {
data: countdownData,
maxHeight: '90vh',
width: '400px',
maxWidth: '90vw',
disableClose: true
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.submitCountdown(result, viewCountdown);
}
});
}
/**
* Function to send a countdown
*
* @param data the countdown data to send
* @param viewCountdown optional existing countdown to update
*/
public submitCountdown(data: CountdownData, viewCountdown?: ViewCountdown): void {
const defaultTime = this.durationService.stringToDuration(data.duration, 'm');
const sendData = new Countdown({
title: data.title,
description: data.description,
default_time: defaultTime,
countdown_time: viewCountdown && viewCountdown.running ? null : defaultTime
});
if (viewCountdown) {
this.countdownRepo.update(sendData, viewCountdown).then(() => {}, this.raiseError);
} else {
this.countdownRepo.create(sendData).then(() => {}, this.raiseError);
}
}
} }

View File

@ -16,10 +16,14 @@
<mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1"> <mat-card *ngIf="!projectorToCreate && projectors && projectors.length > 1">
<span translate> <span translate>
Reference projector for current list of speakers: Reference projector for current list of speakers: </span
</span>&nbsp; >&nbsp;
<mat-form-field> <mat-form-field>
<mat-select [disabled]="!!editId" [value]="projectors.length ? projectors[0].reference_projector_id : null" (selectionChange)="onSelectReferenceProjector($event)"> <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"> <mat-option *ngFor="let projector of projectors" [value]="projector.id">
{{ projector.getTitle() | translate }} {{ projector.getTitle() | translate }}
</mat-option> </mat-option>
@ -33,7 +37,7 @@
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)"> <form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
<p> <p>
<mat-form-field> <mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required> <input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!createForm.controls.name.valid"> <mat-hint *ngIf="!createForm.controls.name.valid">
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
@ -58,16 +62,16 @@
{{ projector.name | translate }} {{ projector.name | translate }}
</ng-container> </ng-container>
<ng-container class="meta-text-block-action-row" *ngIf="canManage"> <ng-container class="meta-text-block-action-row" *ngIf="canManage">
<button mat-icon-button *ngIf="editId !== projector.id" (click)=onEditButton(projector)> <button mat-icon-button *ngIf="editId !== projector.id" (click)="onEditButton(projector)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>
<button mat-icon-button *ngIf="editId === projector.id" (click)=onCancelButton(projector)> <button mat-icon-button *ngIf="editId === projector.id" (click)="onCancelButton(projector)">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
<button mat-icon-button *ngIf="editId === projector.id" (click)=onSaveButton(projector)> <button mat-icon-button *ngIf="editId === projector.id" (click)="onSaveButton(projector)">
<mat-icon>save</mat-icon> <mat-icon>save</mat-icon>
</button> </button>
<button mat-icon-button color="warn" (click)=onDeleteButton(projector)> <button mat-icon-button color="warn" (click)="onDeleteButton(projector)">
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
</button> </button>
</ng-container> </ng-container>
@ -81,7 +85,7 @@
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)"> <form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
<!-- Name field --> <!-- Name field -->
<mat-form-field> <mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required> <input formControlName="name" matInput placeholder="{{ 'Name' | translate }}" required />
<mat-hint *ngIf="!updateForm.controls.name.valid"> <mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
@ -94,7 +98,13 @@
{{ ratio }} {{ ratio }}
</mat-radio-button> </mat-radio-button>
</mat-radio-group> </mat-radio-group>
<mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider> <mat-slider
[thumbLabel]="true"
formControlName="width"
min="800"
max="3840"
step="10"
></mat-slider>
{{ updateForm.value.width }} {{ updateForm.value.width }}
<!-- colors --> <!-- colors -->
@ -160,8 +170,4 @@
<mat-icon>note</mat-icon> <mat-icon>note</mat-icon>
<span translate>Projector messages</span> <span translate>Projector messages</span>
</button> </button>
<button type="button" mat-menu-item routerLink="/projectors/countdowns">
<mat-icon>alarm</mat-icon>
<span translate>Countdowns</span>
</button>
</mat-menu> </mat-menu>

View File

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

View File

@ -5,18 +5,19 @@ import { ProjectorRoutingModule } from './projector-routing.module';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { ProjectorListComponent } from './components/projector-list/projector-list.component'; import { ProjectorListComponent } from './components/projector-list/projector-list.component';
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component'; import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
import { CountdownListComponent } from './components/countdown-list/countdown-list.component';
import { ProjectorMessageListComponent } from './components/projector-message-list/projector-message-list.component'; import { ProjectorMessageListComponent } from './components/projector-message-list/projector-message-list.component';
import { CountdownControlsComponent } from './components/countdown-controls/countdown-controls.component'; import { CountdownControlsComponent } from './components/countdown-controls/countdown-controls.component';
import { CountdownDialogComponent } from './components/countdown-dialog/countdown-dialog.component';
@NgModule({ @NgModule({
imports: [CommonModule, ProjectorRoutingModule, SharedModule], imports: [CommonModule, ProjectorRoutingModule, SharedModule],
declarations: [ declarations: [
ProjectorListComponent, ProjectorListComponent,
ProjectorDetailComponent, ProjectorDetailComponent,
CountdownListComponent,
ProjectorMessageListComponent, ProjectorMessageListComponent,
CountdownControlsComponent CountdownControlsComponent,
] CountdownDialogComponent
],
entryComponents: [CountdownDialogComponent]
}) })
export class ProjectorModule {} export class ProjectorModule {}

View File

@ -11,7 +11,8 @@
$foreground: map-get($theme, foreground); $foreground: map-get($theme, foreground);
$background: map-get($theme, background); $background: map-get($theme, background);
h1, h3.accent { h1,
h3.accent {
color: mat-color($primary); color: mat-color($primary);
} }
@ -19,7 +20,8 @@
color: mat-color($primary); color: mat-color($primary);
} }
.accent, .accent-text { .accent,
.accent-text {
color: mat-color($accent); color: mat-color($accent);
} }
@ -27,10 +29,6 @@
color: mat-color($primary); color: mat-color($primary);
} }
.projected .mat-icon {
color: mat-color($primary);
}
//custom table header for search button, filtering and more. Used in ListViews //custom table header for search button, filtering and more. Used in ListViews
.custom-table-header { .custom-table-header {
background: mat-color($background, background); background: mat-color($background, background);
@ -42,7 +40,9 @@
font-size: 75%; font-size: 75%;
display: block; display: block;
} }
.error, .warn {
.error,
.warn {
color: mat-color($warn); color: mat-color($warn);
} }

View File

@ -35,8 +35,4 @@ $openslides-primary: mat-palette($openslides-green);
$openslides-accent: mat-palette($mat-amber); $openslides-accent: mat-palette($mat-amber);
$openslides-warn: mat-palette($mat-red); $openslides-warn: mat-palette($mat-red);
$openslides-green-theme: mat-light-theme( $openslides-green-theme: mat-light-theme($openslides-primary, $openslides-accent, $openslides-warn);
$openslides-primary,
$openslides-accent,
$openslides-warn
)

View File

@ -651,7 +651,6 @@ button.mat-menu-item.selected {
z-index: 2; z-index: 2;
} }
.queue { .queue {
.mat-expansion-panel-body { .mat-expansion-panel-body {
padding: 0 !important; padding: 0 !important;