Countdown slide and controls

This commit is contained in:
FinnStutzenstein 2019-02-14 16:02:18 +01:00
parent 34feac553b
commit 2801e28704
42 changed files with 600 additions and 141 deletions

View File

@ -52,9 +52,9 @@ export class ProjectorService {
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
): IdentifiableProjectorElement { ): IdentifiableProjectorElement {
if (isProjectable(obj)) { if (isProjectable(obj)) {
return obj.getSlide().getBasicProjectorElement(); return obj.getSlide().getBasicProjectorElement({});
} else if (isProjectorElementBuildDeskriptor(obj)) { } else if (isProjectorElementBuildDeskriptor(obj)) {
return obj.getBasicProjectorElement(); return obj.getBasicProjectorElement({});
} else { } else {
return obj; return obj;
} }
@ -133,7 +133,9 @@ export class ProjectorService {
const element = this.getProjectorElement(obj); const element = this.getProjectorElement(obj);
if (element.stable) { if (element.stable) {
// Just add this stable element // remove the same element, if it is currently projected
projector.removeElements(element);
// Add this stable element
projector.addElement(element); projector.addElement(element);
await this.projectRequest(projector, projector.elements); await this.projectRequest(projector, projector.elements);
} else { } else {

View File

@ -8,6 +8,7 @@ import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Countdown } from 'app/shared/models/core/countdown'; import { Countdown } from 'app/shared/models/core/countdown';
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service'; import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ServertimeService } from 'app/core/core-services/servertime.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -18,7 +19,8 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
mapperService: CollectionStringMapperService, mapperService: CollectionStringMapperService,
viewModelStoreService: ViewModelStoreService, viewModelStoreService: ViewModelStoreService,
private dataSend: DataSendService, private dataSend: DataSendService,
private translate: TranslateService private translate: TranslateService,
private servertimeService: ServertimeService
) { ) {
super(DS, mapperService, viewModelStoreService, Countdown); super(DS, mapperService, viewModelStoreService, Countdown);
} }
@ -41,7 +43,21 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
await this.dataSend.updateModel(update); await this.dataSend.updateModel(update);
} }
public async delete(viewCountdown: ViewCountdown): Promise<void> { public async delete(countdown: ViewCountdown): Promise<void> {
await this.dataSend.deleteModel(viewCountdown.countdown); await this.dataSend.deleteModel(countdown.countdown);
}
public async start(countdown: ViewCountdown): Promise<void> {
const endTime = this.servertimeService.getServertime() / 1000 + countdown.countdown_time;
await this.update({ running: true, countdown_time: endTime }, countdown);
}
public async stop(countdown: ViewCountdown): Promise<void> {
await this.update({ running: false, countdown_time: countdown.default_time }, countdown);
}
public async pause(countdown: ViewCountdown): Promise<void> {
const endTime = countdown.countdown_time - this.servertimeService.getServertime() / 1000;
await this.update({ running: false, countdown_time: endTime }, countdown);
} }
} }

View File

@ -12,6 +12,9 @@ import { Injectable } from '@angular/core';
* // will also result in 70 * // will also result in 70
* const b = this.durationService.stringToDuration('01:10'); * const b = this.durationService.stringToDuration('01:10');
* *
* // will also result in 89 (interpret as seconds)
* const b = this.durationService.stringToDuration('01:20 m', 'm');
*
* // will result in 0 * // will result in 0
* const c = this.durationService.stringToDuration('01:10b'); * const c = this.durationService.stringToDuration('01:10b');
* ``` * ```
@ -20,6 +23,9 @@ import { Injectable } from '@angular/core';
* ```ts * ```ts
* // will result in 01:10 h * // will result in 01:10 h
* const a = this.durationService.durationToString(70); * const a = this.durationService.durationToString(70);
*
* // will result in 00:30 m (30 is interpreted as seconds)
* const a = this.durationService.durationToString(30);
* ``` * ```
*/ */
@Injectable({ @Injectable({
@ -32,13 +38,16 @@ export class DurationService {
public constructor() {} public constructor() {}
/** /**
* Transform a duration string to duration in minutes. * Transform a duration string to duration in minutes or seconds. This depends on the
* provided suffix for the input.
* *
* @param durationText the text to be transformed into a duration * @param durationText the text to be transformed into a duration
* @returns time in minutes or 0 if values are below 0 or no parsable numbers * @param suffix may be 'h' or 'm' for hour or minute. This character will be removed
* from the duration text.
* @returns time in minutes or seconds or 0 if values are below 0 or no parsable numbers
*/ */
public stringToDuration(durationText: string): number { public stringToDuration(durationText: string, suffix: 'h' | 'm' = 'h'): number {
const splitDuration = durationText.replace('h', '').split(':'); const splitDuration = durationText.replace(suffix, '').split(':');
let time: number; let time: number;
if (splitDuration.length > 1 && !isNaN(+splitDuration[0]) && !isNaN(+splitDuration[1])) { if (splitDuration.length > 1 && !isNaN(+splitDuration[0]) && !isNaN(+splitDuration[1])) {
time = +splitDuration[0] * 60 + +splitDuration[1]; time = +splitDuration[0] * 60 + +splitDuration[1];
@ -54,31 +63,18 @@ export class DurationService {
} }
/** /**
* Converts a duration number (given in minutes) * Converts a duration number (given in minutes or seconds)
* To a string in HH:MM format
* *
* @param duration value in minutes * @param duration value in minutes
* @returns a more human readable time representation * @returns a more human readable time representation
*/ */
public durationToString(duration: number): string { public durationToString(duration: number, suffix: 'h' | 'm' = 'h'): string {
const hours = Math.floor(duration / 60); const major = Math.floor(duration / 60);
const minutes = `0${Math.floor(duration - hours * 60)}`.slice(-2); const minor = `0${duration % 60}`.slice(-2);
if (!isNaN(+hours) && !isNaN(+minutes)) { if (!isNaN(+major) && !isNaN(+minor)) {
return `${hours}:${minutes} h`; return `${major}:${minor} ${suffix}`;
} else { } else {
return ''; return '';
} }
} }
/**
* Converts a duration number (given in seconds)o a string in `MMM:SS` format
*
* @param time value in seconds
* @returns a more human readable time representation
*/
public secondDurationToString(time: number): string {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${`0${seconds}`.slice(-2)}`;
}
} }

View File

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

View File

@ -0,0 +1,15 @@
#countdown {
font-weight: bold;
padding: 10px;
display: inline-block;
min-width: 75px;
text-align: right;
&.warning_time {
color: #ed940d;
}
&.negative {
color: #cc0000;
}
}

View File

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

View File

@ -0,0 +1,99 @@
import { Component, OnDestroy, Input } from '@angular/core';
import { ServertimeService } from 'app/core/core-services/servertime.service';
export interface CountdownData {
running: boolean;
countdown_time: number;
}
/**
* Displays the countdown time.
*/
@Component({
selector: 'os-countdown-time',
templateUrl: './countdown-time.component.html',
styleUrls: ['./countdown-time.component.scss']
})
export class CountdownTimeComponent implements OnDestroy {
/**
* The time in seconds to make the countdown orange, is the countdown is below this value.
*/
@Input()
public warningTime: number;
/**
* The amount of seconds to display
*/
public seconds: number;
/**
* String formattet seconds.
*/
public time: string;
/**
* The updateinterval.
*/
private countdownInterval: any;
private _countdown: CountdownData;
/**
* The needed data for the countdown.
*/
@Input()
public set countdown(data: CountdownData) {
this._countdown = data;
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
}
if (data) {
this.updateCountdownTime();
this.countdownInterval = setInterval(() => this.updateCountdownTime(), 500);
}
}
public get countdown(): CountdownData {
return this._countdown;
}
public constructor(private servertimeService: ServertimeService) {}
/**
* Updates the countdown time and string format it.
*/
private updateCountdownTime(): void {
if (this.countdown.running) {
this.seconds = Math.floor(this.countdown.countdown_time - this.servertimeService.getServertime() / 1000);
} else {
this.seconds = this.countdown.countdown_time;
}
const negative = this.seconds < 0;
let seconds = this.seconds;
if (negative) {
seconds = -seconds;
}
const time = new Date(seconds * 1000);
const m = '0' + time.getMinutes();
const s = '0' + time.getSeconds();
this.time = m.slice(-2) + ':' + s.slice(-2);
if (negative) {
this.time = '-' + this.time;
}
}
/**
* Clear all pending intervals.
*/
public ngOnDestroy(): void {
if (this.countdownInterval) {
clearInterval(this.countdownInterval);
}
}
}

View File

@ -17,15 +17,15 @@
<div *ngFor="let option of options"> <div *ngFor="let option of options">
<div *ngIf="isDecisionOption(option)"> <div *ngIf="isDecisionOption(option)">
<mat-checkbox <mat-checkbox
[checked]="projectorElement[option.key]" [checked]="optionValues[option.key]"
(change)="projectorElement[option.key] = !projectorElement[option.key]" (change)="optionValues[option.key] = !optionValues[option.key]"
> >
{{ option.displayName | translate }} {{ option.displayName | translate }}
</mat-checkbox> </mat-checkbox>
</div> </div>
<div *ngIf="isChoiceOption(option)"> <div *ngIf="isChoiceOption(option)">
<h3>{{ option.displayName | translate }}</h3> <h3>{{ option.displayName | translate }}</h3>
<mat-radio-group [name]="option.key" [(ngModel)]="projectorElement[option.key]"> <mat-radio-group [name]="option.key" [(ngModel)]="optionValues[option.key]">
<mat-radio-button *ngFor="let choice of option.choices" [value]="choice.value"> <mat-radio-button *ngFor="let choice of option.choices" [value]="choice.value">
{{ choice.displayName | translate }} {{ choice.displayName | translate }}
</mat-radio-button> </mat-radio-button>

View File

@ -26,7 +26,7 @@ export type ProjectionDialogReturnType = [Projector[], IdentifiableProjectorElem
export class ProjectionDialogComponent { export class ProjectionDialogComponent {
public projectors: Projector[]; public projectors: Projector[];
private selectedProjectors: Projector[] = []; private selectedProjectors: Projector[] = [];
public projectorElement: IdentifiableProjectorElement; public optionValues: object = {};
public options: SlideOptions; public options: SlideOptions;
public constructor( public constructor(
@ -52,11 +52,9 @@ export class ProjectionDialogComponent {
} }
} }
this.projectorElement = this.projectorElementBuildDescriptor.getBasicProjectorElement();
// Set option defaults // Set option defaults
this.projectorElementBuildDescriptor.slideOptions.forEach(option => { this.projectorElementBuildDescriptor.slideOptions.forEach(option => {
this.projectorElement[option.key] = option.default; this.optionValues[option.key] = option.default;
}); });
this.options = this.projectorElementBuildDescriptor.slideOptions; this.options = this.projectorElementBuildDescriptor.slideOptions;
@ -88,7 +86,9 @@ export class ProjectionDialogComponent {
} }
public onOk(): void { public onOk(): void {
this.dialogRef.close([this.selectedProjectors, this.projectorElement]); let element = this.projectorElementBuildDescriptor.getBasicProjectorElement(this.optionValues);
element = { ...element, ...this.optionValues };
this.dialogRef.close([this.selectedProjectors, element]);
} }
public onCancel(): void { public onCancel(): void {

View File

@ -4,8 +4,9 @@
&.content { &.content {
width: calc(100% - 100px); width: calc(100% - 100px);
margin-left: 50px; position: absolute;
margin-right: 50px; left: 50px;
top: 0;
} }
} }

View File

@ -5,10 +5,13 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { SlideManager } from 'app/slides/services/slide-manager.service'; import { SlideManager } from 'app/slides/services/slide-manager.service';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { SlideOptions } from 'app/slides/slide-manifest';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { SlideData } from 'app/site/projector/services/projector-data.service'; import { SlideData } from 'app/site/projector/services/projector-data.service';
import { ProjectorElement } from 'app/shared/models/core/projector';
function hasError(obj: object): obj is { error: string } {
return (<{ error: string }>obj).error !== undefined;
}
/** /**
* Container for one slide. Cares about the position (scale, scroll) in the projector, * Container for one slide. Cares about the position (scale, scroll) in the projector,
* and loading of slides. * and loading of slides.
@ -31,24 +34,31 @@ export class SlideContainerComponent extends BaseComponent {
private _slideData: SlideData<object>; private _slideData: SlideData<object>;
@Input() @Input()
public set slideData(data: SlideData<object>) { public set slideData(slideData: SlideData<object>) {
// If there is no ata or an error, clear and exit. // If there is no ata or an error, clear and exit.
if (!data || data.error) { if (!slideData || hasError(slideData) || (slideData.data && hasError(slideData.data))) {
// clear slide container: // clear slide container:
if (this.slide) { if (this.slide) {
this.slide.clear(); this.slide.clear();
} }
if (data.error) { let error;
console.error(data.error); if (hasError(slideData)) {
error = slideData.error;
} else if (slideData.data && hasError(slideData.data)) {
error = slideData.data.error;
}
if (error) {
console.log(error);
} }
return; return;
} }
this._slideData = data; this._slideData = slideData;
if (this.previousSlideName !== data.element.name) { if (this.previousSlideName !== slideData.element.name) {
this.slideChanged(data.element.name); this.slideChanged(slideData.element);
this.previousSlideName = data.element.name; this.previousSlideName = slideData.element.name;
} }
this.setDataForComponent(); this.setDataForComponent();
} }
@ -88,7 +98,7 @@ export class SlideContainerComponent extends BaseComponent {
/** /**
* The current slideoptions. * The current slideoptions.
*/ */
public slideOptions: SlideOptions = { scaleable: false, scrollable: false }; public slideOptions: { scaleable: boolean; scrollable: boolean } = { scaleable: false, scrollable: false };
/** /**
* Styles for scaling and scrolling. * Styles for scaling and scrolling.
@ -137,9 +147,19 @@ export class SlideContainerComponent extends BaseComponent {
* *
* @param slideName The slide to load. * @param slideName The slide to load.
*/ */
private slideChanged(slideName: string): void { private slideChanged(element: ProjectorElement): void {
this.slideOptions = this.slideManager.getSlideOptions(slideName); const options = this.slideManager.getSlideConfiguration(element.name);
this.slideManager.getSlideFactory(slideName).then(slideFactory => { if (typeof options.scaleable === 'boolean') {
this.slideOptions.scaleable = options.scaleable;
} else {
this.slideOptions.scaleable = options.scaleable(element);
}
if (typeof options.scrollable === 'boolean') {
this.slideOptions.scrollable = options.scrollable;
} else {
this.slideOptions.scrollable = options.scrollable(element);
}
this.slideManager.getSlideFactory(element.name).then(slideFactory => {
this.slide.clear(); this.slide.clear();
this.slideRef = this.slide.createComponent(slideFactory); this.slideRef = this.slide.createComponent(slideFactory);
this.setDataForComponent(); this.setDataForComponent();

View File

@ -1,11 +1,18 @@
import { BaseModel } from '../base/base-model'; import { BaseModel } from '../base/base-model';
export interface ProjectorElementOptions {
/**
* Additional data.
*/
[key: string]: any;
}
/** /**
* A projectorelement must have a name and optional attributes. * A projectorelement must have a name and optional attributes.
* error is listed here, because this might be set by the server, if * error is listed here, because this might be set by the server, if
* something is wrong and I want you to be sensible about this. * something is wrong and I want you to be sensible about this.
*/ */
export interface ProjectorElement { export interface ProjectorElement extends ProjectorElementOptions {
/** /**
* The name of the element. * The name of the element.
*/ */
@ -16,11 +23,6 @@ export interface ProjectorElement {
* DO NOT read additional data (name is save). * DO NOT read additional data (name is save).
*/ */
error?: string; error?: string;
/**
* Additional data.
*/
[key: string]: any;
} }
export interface IdentifiableProjectorElement extends ProjectorElement { export interface IdentifiableProjectorElement extends ProjectorElement {

View File

@ -82,6 +82,7 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b
import { OpenSlidesTranslateModule } from '../core/translate/openslides-translate-module'; import { OpenSlidesTranslateModule } from '../core/translate/openslides-translate-module';
import { ProjectorComponent } from './components/projector/projector.component'; import { ProjectorComponent } from './components/projector/projector.component';
import { SlideContainerComponent } from './components/slide-container/slide-container.component'; import { SlideContainerComponent } from './components/slide-container/slide-container.component';
import { CountdownTimeComponent } from './components/contdown-time/countdown-time.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -201,7 +202,8 @@ import { SlideContainerComponent } from './components/slide-container/slide-cont
ProjectorComponent, ProjectorComponent,
SlideContainerComponent, SlideContainerComponent,
OwlDateTimeModule, OwlDateTimeModule,
OwlNativeDateTimeModule OwlNativeDateTimeModule,
CountdownTimeComponent
], ],
declarations: [ declarations: [
PermsDirective, PermsDirective,
@ -227,7 +229,8 @@ import { SlideContainerComponent } from './components/slide-container/slide-cont
ResizedDirective, ResizedDirective,
MetaTextBlockComponent, MetaTextBlockComponent,
ProjectorComponent, ProjectorComponent,
SlideContainerComponent SlideContainerComponent,
CountdownTimeComponent
], ],
providers: [ providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter }, { provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -342,6 +342,6 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
const duration = Math.floor( const duration = Math.floor(
(new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000 (new Date(speaker.end_time).valueOf() - new Date(speaker.begin_time).valueOf()) / 1000
); );
return `${this.durationService.secondDurationToString(duration)} ${this.translate.instant('minutes')}`; return `${this.durationService.durationToString(duration, 'm')} ${this.translate.instant('minutes')}`;
} }
} }

View File

@ -97,7 +97,7 @@ export class ViewTopic extends BaseAgendaViewModel {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
name: Topic.COLLECTIONSTRING, name: Topic.COLLECTIONSTRING,
id: this.id, id: this.id,
getIdentifiers: () => ['name', 'id'] getIdentifiers: () => ['name', 'id']

View File

@ -81,7 +81,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
name: Assignment.COLLECTIONSTRING, name: Assignment.COLLECTIONSTRING,
id: this.id, id: this.id,
getIdentifiers: () => ['name', 'id'] getIdentifiers: () => ['name', 'id']

View File

@ -1,5 +1,5 @@
import { Displayable } from 'app/site/base/displayable'; import { Displayable } from 'app/site/base/displayable';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector'; import { IdentifiableProjectorElement, ProjectorElementOptions } from 'app/shared/models/core/projector';
import { SlideOptions } from './slide-options'; import { SlideOptions } from './slide-options';
export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorElementBuildDeskriptor { export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorElementBuildDeskriptor {
@ -15,7 +15,7 @@ export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorEle
export interface ProjectorElementBuildDeskriptor { export interface ProjectorElementBuildDeskriptor {
slideOptions: SlideOptions; slideOptions: SlideOptions;
projectionDefaultName?: string; projectionDefaultName?: string;
getBasicProjectorElement(): IdentifiableProjectorElement; getBasicProjectorElement(options: ProjectorElementOptions): IdentifiableProjectorElement;
/** /**
* The title to show in the projection dialog * The title to show in the projection dialog

View File

@ -1,10 +1,14 @@
export interface SlideDecisionOption { interface BaseSlideOption {
key: string; key: string;
displayName: string; displayName: string;
default: string;
} }
export interface SlideChoiceOption extends SlideDecisionOption { export interface SlideDecisionOption extends BaseSlideOption {
default: boolean;
}
export interface SlideChoiceOption extends BaseSlideOption {
default: string;
choices: { value: string; displayName: string }[]; choices: { value: string; displayName: string }[];
} }

View File

@ -561,7 +561,7 @@ export class ViewMotion extends BaseAgendaViewModel implements Searchable {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
name: Motion.COLLECTIONSTRING, name: Motion.COLLECTIONSTRING,
id: this.id, id: this.id,
getIdentifiers: () => ['name', 'id'] getIdentifiers: () => ['name', 'id']

View File

@ -0,0 +1,12 @@
<div *ngIf="countdown">
<os-countdown-time [countdown]="countdown" [warningTime]="warningTime"></os-countdown-time>
<button type="button" [disabled]="!canStop()" mat-icon-button (click)="stop($event)">
<mat-icon>stop</mat-icon>
</button>
<button *ngIf="!countdown.running" type="button" mat-icon-button (click)="start($event)">
<mat-icon>play_arrow</mat-icon>
</button>
<button *ngIf="countdown.running" type="button" mat-icon-button (click)="pause($event)">
<mat-icon>pause</mat-icon>
</button>
</div>

View File

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

View File

@ -0,0 +1,67 @@
import { Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from '../../../base/base-view';
import { ViewCountdown } from '../../models/view-countdown';
import { CountdownRepositoryService } from 'app/core/repositories/projector/countdown-repository.service';
import { ConfigService } from 'app/core/ui-services/config.service';
@Component({
selector: 'os-countdown-controls',
templateUrl: './countdown-controls.component.html'
})
export class CountdownControlsComponent extends BaseViewComponent {
@Input()
public countdown: ViewCountdown;
/**
* The time in seconds to make the countdown orange, is the countdown is below this value.
*/
public warningTime: number;
public constructor(
titleService: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: CountdownRepositoryService,
private configService: ConfigService
) {
super(titleService, translate, matSnackBar);
this.configService.get<number>('agenda_countdown_warning_time').subscribe(time => (this.warningTime = time));
}
/**
* Start the countdown
*/
public start(event: Event): void {
event.stopPropagation();
this.repo.start(this.countdown).catch(this.raiseError);
}
/**
* Pause the countdown
*/
public pause(event: Event): void {
event.stopPropagation();
this.repo.pause(this.countdown).catch(this.raiseError);
}
/**
* Stop the countdown
*/
public stop(event: Event): void {
event.stopPropagation();
this.repo.stop(this.countdown).catch(this.raiseError);
}
/**
* One can stop the countdown, if it is running or not resetted.
*/
public canStop(): boolean {
return this.countdown.running || this.countdown.countdown_time !== this.countdown.default_time;
}
}

View File

@ -18,6 +18,13 @@
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
</mat-form-field> </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> </p>
</form> </form>
</mat-card-content> </mat-card-content>
@ -45,8 +52,10 @@
<div class="header-name"> <div class="header-name">
{{ countdown.description }} {{ countdown.description }}
</div> </div>
<div class="header-controls">
<os-countdown-controls [countdown]="countdown"></os-countdown-controls>
</div>
</div> </div>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<form [formGroup]="updateForm" <form [formGroup]="updateForm"
@ -60,11 +69,15 @@
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
</mat-form-field> </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> </p>
</form> </form>
<ng-container *ngIf="editId !== countdown.id">
TODO: Show countdown time etc.
</ng-container>
<mat-action-row> <mat-action-row>
<button *ngIf="editId !== countdown.id" mat-button class="on-transition-fade" (click)="onEditButton(countdown)" <button *ngIf="editId !== countdown.id" mat-button class="on-transition-fade" (click)="onEditButton(countdown)"
mat-icon-button> mat-icon-button>

View File

@ -14,7 +14,7 @@ mat-card {
.header-container { .header-container {
display: grid; display: grid;
grid-template-rows: auto; grid-template-rows: auto;
grid-template-columns: 40px 1fr; grid-template-columns: 40px 1fr 2fr;
width: 100%; width: 100%;
> div { > div {
@ -25,10 +25,17 @@ mat-card {
.header-projector-button { .header-projector-button {
grid-column-start: 1; grid-column-start: 1;
grid-column-end: 2;
} }
.header-name { .header-name {
grid-column-start: 2; grid-column-start: 2;
grid-column-end: 3;
padding: 10px; padding: 10px;
} }
.header-controls {
grid-column-start: 3;
grid-column-end: 4;
}
} }

View File

@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CountdownListComponent } from './countdown-list.component'; import { CountdownListComponent } from './countdown-list.component';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { CountdownControlsComponent } from '../countdown-controls/countdown-controls.component';
describe('CountdownListComponent', () => { describe('CountdownListComponent', () => {
let component: CountdownListComponent; let component: CountdownListComponent;
@ -10,7 +11,7 @@ describe('CountdownListComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [E2EImportsModule], imports: [E2EImportsModule],
declarations: [CountdownListComponent] declarations: [CountdownListComponent, CountdownControlsComponent]
}).compileComponents(); }).compileComponents();
})); }));

View File

@ -10,9 +10,10 @@ 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 { Countdown } from 'app/shared/models/core/countdown'; import { Countdown } from 'app/shared/models/core/countdown';
import { DurationService } from 'app/core/ui-services/duration.service';
/** /**
* List view for the statute paragraphs. * List view for countdowns.
*/ */
@Component({ @Component({
selector: 'os-countdown-list', selector: 'os-countdown-list',
@ -37,20 +38,20 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
public openId: number | null; public openId: number | null;
public editId: number | null; public editId: number | null;
/**
*/
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private repo: CountdownRepositoryService, private repo: CountdownRepositoryService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private promptService: PromptService private promptService: PromptService,
private durationService: DurationService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
const form = { const form = {
description: ['', Validators.required] description: ['', Validators.required],
default_time: ['', Validators.required]
}; };
this.createForm = this.formBuilder.group(form); this.createForm = this.formBuilder.group(form);
this.updateForm = this.formBuilder.group(form); this.updateForm = this.formBuilder.group(form);
@ -75,7 +76,8 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
if (!this.countdownToCreate) { if (!this.countdownToCreate) {
this.createForm.reset(); this.createForm.reset();
this.createForm.setValue({ this.createForm.setValue({
description: '' description: '',
default_time: '1:00 m'
}); });
this.countdownToCreate = new Countdown(); this.countdownToCreate = new Countdown();
} }
@ -86,7 +88,17 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
*/ */
public create(): void { public create(): void {
if (this.createForm.valid) { if (this.createForm.valid) {
this.countdownToCreate.patchValues(this.createForm.value as Countdown); 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,
default_time: default_time
};
newValues.countdown_time = default_time;
this.countdownToCreate.patchValues(newValues);
this.repo.create(this.countdownToCreate).then(() => { this.repo.create(this.countdownToCreate).then(() => {
this.countdownToCreate = null; this.countdownToCreate = null;
}, this.raiseError); }, this.raiseError);
@ -101,7 +113,8 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
this.editId = countdown.id; this.editId = countdown.id;
this.updateForm.setValue({ this.updateForm.setValue({
description: countdown.description description: countdown.description,
default_time: this.durationService.durationToString(countdown.default_time, 'm')
}); });
} }
@ -111,7 +124,18 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
*/ */
public onSaveButton(countdown: ViewCountdown): void { public onSaveButton(countdown: ViewCountdown): void {
if (this.updateForm.valid) { if (this.updateForm.valid) {
this.repo.update(this.updateForm.value as Partial<Countdown>, countdown).then(() => { let default_time = this.durationService.stringToDuration(this.updateForm.value.default_time, 'm');
if (default_time === 0) {
default_time = 60;
}
const newValues: Partial<Countdown> = {
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.openId = this.editId = null;
}, this.raiseError); }, this.raiseError);
} }
@ -123,7 +147,7 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
* @param countdown The countdown to delete * @param countdown The countdown to delete
*/ */
public async onDeleteButton(countdown: ViewCountdown): Promise<void> { public async onDeleteButton(countdown: ViewCountdown): Promise<void> {
const content = this.translate.instant('Delete') + ` ${countdown.description}?`; const content = this.translate.instant('Delete countdown') + ` ${countdown.description}?`;
if (await this.promptService.open('Are you sure?', content)) { if (await this.promptService.open('Are you sure?', content)) {
this.repo.delete(countdown).then(() => (this.openId = this.editId = null), this.raiseError); this.repo.delete(countdown).then(() => (this.openId = this.editId = null), this.raiseError);
} }

View File

@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module'; import { E2EImportsModule } from 'e2e-imports.module';
import { ProjectorMessageListComponent } from './projector-message-list.component'; import { ProjectorMessageListComponent } from './projector-message-list.component';
describe('CountdownListComponent', () => { describe('ProjectorMessageListComponent', () => {
let component: ProjectorMessageListComponent; let component: ProjectorMessageListComponent;
let fixture: ComponentFixture<ProjectorMessageListComponent>; let fixture: ComponentFixture<ProjectorMessageListComponent>;

View File

@ -16,6 +16,18 @@ export class ViewCountdown extends BaseProjectableViewModel {
return this.countdown.id; return this.countdown.id;
} }
public get running(): boolean {
return this.countdown.running;
}
public get default_time(): number {
return this.countdown.default_time;
}
public get countdown_time(): number {
return this.countdown.countdown_time;
}
public get description(): string { public get description(): string {
return this.countdown.description; return this.countdown.description;
} }
@ -38,13 +50,19 @@ export class ViewCountdown extends BaseProjectableViewModel {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
stable: true, stable: true,
name: Countdown.COLLECTIONSTRING, name: Countdown.COLLECTIONSTRING,
id: this.id, id: this.id,
getIdentifiers: () => ['name', 'id'] getIdentifiers: () => ['name', 'id']
}), }),
slideOptions: [], slideOptions: [
{
key: 'fullscreen',
displayName: 'Fullscreen',
default: false
}
],
projectionDefaultName: 'countdowns', projectionDefaultName: 'countdowns',
getTitle: () => this.getTitle() getTitle: () => this.getTitle()
}; };

View File

@ -39,7 +39,7 @@ export class ViewProjectorMessage extends BaseProjectableViewModel {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
stable: true, stable: true,
name: ProjectorMessage.COLLECTIONSTRING, name: ProjectorMessage.COLLECTIONSTRING,
id: this.id, id: this.id,

View File

@ -10,6 +10,7 @@ import { ProjectorDataService } from './services/projector-data.service';
import { CurrentListOfSpeakersSlideService } from './services/current-list-of-of-speakers-slide.service'; import { CurrentListOfSpeakersSlideService } from './services/current-list-of-of-speakers-slide.service';
import { CountdownListComponent } from './components/countdown-list/countdown-list.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';
@NgModule({ @NgModule({
providers: [ClockSlideService, ProjectorDataService, CurrentListOfSpeakersSlideService], providers: [ClockSlideService, ProjectorDataService, CurrentListOfSpeakersSlideService],
@ -18,7 +19,8 @@ import { ProjectorMessageListComponent } from './components/projector-message-li
ProjectorListComponent, ProjectorListComponent,
ProjectorDetailComponent, ProjectorDetailComponent,
CountdownListComponent, CountdownListComponent,
ProjectorMessageListComponent ProjectorMessageListComponent,
CountdownControlsComponent
] ]
}) })
export class ProjectorModule {} export class ProjectorModule {}

View File

@ -22,11 +22,9 @@ export class CurrentListOfSpeakersSlideService {
private slideManager: SlideManager private slideManager: SlideManager
) { ) {
this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => { this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => {
if (projector) { if (projector && this.currentItemIds[projector.id]) {
const item = this.getCurrentAgendaItemIdForProjector(projector); const item = this.getCurrentAgendaItemIdForProjector(projector);
if (this.currentItemIds[projector.id]) { this.currentItemIds[projector.id].next(item);
this.currentItemIds[projector.id].next(item);
}
} }
}); });
} }

View File

@ -188,7 +188,7 @@ export class ViewUser extends BaseProjectableViewModel implements Searchable {
public getSlide(): ProjectorElementBuildDeskriptor { public getSlide(): ProjectorElementBuildDeskriptor {
return { return {
getBasicProjectorElement: () => ({ getBasicProjectorElement: options => ({
name: User.COLLECTIONSTRING, name: User.COLLECTIONSTRING,
id: this.id, id: this.id,
getIdentifiers: () => ['name', 'id'] getIdentifiers: () => ['name', 'id']

View File

@ -0,0 +1,49 @@
import { SlideDynamicConfiguration, Slide } from './slide-manifest';
export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[] = [
{
slide: 'topics/topic',
scaleable: true,
scrollable: true
},
{
slide: 'motions/motion',
scaleable: true,
scrollable: true
},
{
slide: 'users/user',
scaleable: true,
scrollable: true
},
{
slide: 'core/clock',
scaleable: false,
scrollable: false
},
{
slide: 'core/countdown',
scaleable: false,
scrollable: false
},
{
slide: 'core/projector-message',
scaleable: false,
scrollable: false
},
{
slide: 'agenda/current-list-of-speakers',
scaleable: true,
scrollable: true
},
{
slide: 'agenda/current-list-of-speakers-overlay',
scaleable: false,
scrollable: false
},
{
slide: 'assignments/assignment',
scaleable: true,
scrollable: true
}
];

View File

@ -12,8 +12,6 @@ export const allSlides: SlideManifest[] = [
slide: 'topics/topic', slide: 'topics/topic',
path: 'topics/topic', path: 'topics/topic',
loadChildren: './slides/agenda/topic/topics-topic-slide.module#TopicsTopicSlideModule', loadChildren: './slides/agenda/topic/topics-topic-slide.module#TopicsTopicSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Topic', verboseName: 'Topic',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -22,8 +20,6 @@ export const allSlides: SlideManifest[] = [
slide: 'motions/motion', slide: 'motions/motion',
path: 'motions/motion', path: 'motions/motion',
loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule', loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Motion', verboseName: 'Motion',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -32,8 +28,6 @@ export const allSlides: SlideManifest[] = [
slide: 'users/user', slide: 'users/user',
path: 'users/user', path: 'users/user',
loadChildren: './slides/users/user/users-user-slide.module#UsersUserSlideModule', loadChildren: './slides/users/user/users-user-slide.module#UsersUserSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Participant', verboseName: 'Participant',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -42,8 +36,6 @@ export const allSlides: SlideManifest[] = [
slide: 'core/clock', slide: 'core/clock',
path: 'core/clock', path: 'core/clock',
loadChildren: './slides/core/clock/clock-slide.module#ClockSlideModule', loadChildren: './slides/core/clock/clock-slide.module#ClockSlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Clock', verboseName: 'Clock',
elementIdentifiers: ['name'], elementIdentifiers: ['name'],
canBeMappedToModel: false canBeMappedToModel: false
@ -52,8 +44,6 @@ export const allSlides: SlideManifest[] = [
slide: 'core/countdown', slide: 'core/countdown',
path: 'core/countdown', path: 'core/countdown',
loadChildren: './slides/core/countdown/countdown-slide.module#CountdownSlideModule', loadChildren: './slides/core/countdown/countdown-slide.module#CountdownSlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Countdown', verboseName: 'Countdown',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -62,8 +52,6 @@ export const allSlides: SlideManifest[] = [
slide: 'core/projector-message', slide: 'core/projector-message',
path: 'core/projector-message', path: 'core/projector-message',
loadChildren: './slides/core/projector-message/projector-message-slide.module#ProjectorMessageSlideModule', loadChildren: './slides/core/projector-message/projector-message-slide.module#ProjectorMessageSlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Message', verboseName: 'Message',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true
@ -73,8 +61,6 @@ export const allSlides: SlideManifest[] = [
path: 'agenda/current-list-of-speakers', path: 'agenda/current-list-of-speakers',
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module#AgendaCurrentListOfSpeakersSlideModule', './slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module#AgendaCurrentListOfSpeakersSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Current list of speakers', verboseName: 'Current list of speakers',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false canBeMappedToModel: false
@ -84,8 +70,6 @@ export const allSlides: SlideManifest[] = [
path: 'agenda/current-list-of-speakers-overlay', path: 'agenda/current-list-of-speakers-overlay',
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module#AgendaCurrentListOfSpeakersOverlaySlideModule', './slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module#AgendaCurrentListOfSpeakersOverlaySlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Current list of speakers overlay', verboseName: 'Current list of speakers overlay',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false canBeMappedToModel: false
@ -94,8 +78,6 @@ export const allSlides: SlideManifest[] = [
slide: 'assignments/assignment', slide: 'assignments/assignment',
path: 'assignments/assignment', path: 'assignments/assignment',
loadChildren: './slides/assignments/assignment/assignment-slide.module#AssignmentSlideModule', loadChildren: './slides/assignments/assignment/assignment-slide.module#AssignmentSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Election', verboseName: 'Election',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true canBeMappedToModel: true

View File

@ -15,6 +15,8 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
private servertimeSubscription: Subscription | null = null; private servertimeSubscription: Subscription | null = null;
private clockInterval: any;
public constructor(private servertimeService: ServertimeService) { public constructor(private servertimeService: ServertimeService) {
super(); super();
} }
@ -26,7 +28,7 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
.subscribe(() => this.updateClock()); .subscribe(() => this.updateClock());
// Update clock every 10 seconds. // Update clock every 10 seconds.
setInterval(() => this.updateClock(), 10 * 1000); this.clockInterval = setInterval(() => this.updateClock(), 10 * 1000);
} }
private updateClock(): void { private updateClock(): void {
@ -42,5 +44,8 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
if (this.servertimeSubscription) { if (this.servertimeSubscription) {
this.servertimeSubscription.unsubscribe(); this.servertimeSubscription.unsubscribe();
} }
if (this.clockInterval) {
clearInterval(this.clockInterval);
}
} }
} }

View File

@ -1,3 +1,6 @@
export interface CountdownSlideData { export interface CountdownSlideData {
error: string; description: string;
countdown_time: number;
running: boolean;
warning_time: number;
} }

View File

@ -1,3 +1,11 @@
<div id="outer"> <div class="countdown overlay" *ngIf="data && !data.element.fullscreen">
COUNTDOWN <os-countdown-time [countdown]="data.data" [warningTime]="data.data.warning_time"></os-countdown-time>
<div class="description">
{{ data.data.description }}
</div>
</div>
<div class="countdown fullscreen" *ngIf="data && data.element.fullscreen">
<div>
<os-countdown-time [countdown]="data.data" [warningTime]="data.data.warning_time"></os-countdown-time>
</div>
</div> </div>

View File

@ -1,10 +1,42 @@
#outer { .countdown {
position: absolute; z-index: 8;
right: 0;
top: 0; &.fullscreen {
background-color: green; position: fixed;
height: 30px; top: 0;
margin: 10px; left: 0;
margin-top: 100px; width: 100%;
z-index: 2; height: 100%;
z-index: 20;
display: grid;
& > div {
justify-content: center;
align-content: center;
font-size: 10vw;
display: inline-grid;
}
}
&.overlay {
position: relative;
float: right;
margin: 100px 10px 10px 10px;
padding: 10px 40px 7px 10px;
min-height: 72px;
min-width: 180px;
font-size: 3.7em;
font-weight: bold;
text-align: right;
border-radius: 7px 7px 7px 7px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
.description {
font-weight: normal;
font-size: 18px;
padding-right: 6px;
}
}
} }

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CountdownSlideData } from './countdown-slide-data'; import { CountdownSlideData } from './countdown-slide-data';

View File

@ -1,10 +1,11 @@
import { Injectable, Inject, Injector, NgModuleFactoryLoader, ComponentFactory, Type } from '@angular/core'; import { Injectable, Inject, Injector, NgModuleFactoryLoader, ComponentFactory, Type } from '@angular/core';
import { SlideManifest, SlideOptions } from '../slide-manifest'; import { SlideManifest, SlideDynamicConfiguration, Slide } from '../slide-manifest';
import { SLIDE } from '../slide-token'; import { SLIDE } from '../slide-token';
import { SLIDE_MANIFESTS } from '../slide-manifest'; import { SLIDE_MANIFESTS } from '../slide-manifest';
import { BaseSlideComponent } from '../base-slide-component'; import { BaseSlideComponent } from '../base-slide-component';
import { ProjectorElement, IdentifiableProjectorElement } from 'app/shared/models/core/projector'; import { ProjectorElement, IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { allSlidesDynamicConfiguration } from '../all-slide-configurations';
/** /**
* Cares about loading slides dynamically. * Cares about loading slides dynamically.
@ -12,6 +13,7 @@ import { ProjectorElement, IdentifiableProjectorElement } from 'app/shared/model
@Injectable() @Injectable()
export class SlideManager { export class SlideManager {
private loadedSlides: { [name: string]: SlideManifest } = {}; private loadedSlides: { [name: string]: SlideManifest } = {};
private loadedSlideConfigurations: { [name: string]: SlideDynamicConfiguration & Slide } = {};
public constructor( public constructor(
@Inject(SLIDE_MANIFESTS) private manifests: SlideManifest[], @Inject(SLIDE_MANIFESTS) private manifests: SlideManifest[],
@ -21,6 +23,9 @@ export class SlideManager {
this.manifests.forEach(slideManifest => { this.manifests.forEach(slideManifest => {
this.loadedSlides[slideManifest.slide] = slideManifest; this.loadedSlides[slideManifest.slide] = slideManifest;
}); });
allSlidesDynamicConfiguration.forEach(config => {
this.loadedSlideConfigurations[config.slide] = config;
});
} }
/** /**
@ -42,12 +47,13 @@ export class SlideManager {
* @param slideName The slide * @param slideName The slide
* @returns SlideOptions for the requested slide. * @returns SlideOptions for the requested slide.
*/ */
public getSlideOptions(slideName: string): SlideOptions { public getSlideConfiguration(slideName: string): SlideDynamicConfiguration {
return this.getManifest(slideName); if (!this.loadedSlideConfigurations[slideName]) {
throw new Error(`Could not find slide for "${slideName}"`);
}
return this.loadedSlideConfigurations[slideName];
} }
/**
*/
public getIdentifialbeProjectorElement(element: ProjectorElement): IdentifiableProjectorElement { public getIdentifialbeProjectorElement(element: ProjectorElement): IdentifiableProjectorElement {
const identifiableElement: IdentifiableProjectorElement = element as IdentifiableProjectorElement; const identifiableElement: IdentifiableProjectorElement = element as IdentifiableProjectorElement;
const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy. const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy.

View File

@ -1,27 +1,32 @@
import { InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector'; import { IdentifiableProjectorElement, ProjectorElement } from 'app/shared/models/core/projector';
type BooleanOrFunction = boolean | ((element: ProjectorElement) => boolean);
export interface Slide {
slide: string;
}
/** /**
* Slides can have these options. * Slides can have these options.
*/ */
export interface SlideOptions { export interface SlideDynamicConfiguration {
/** /**
* Should this slide be scrollable? * Should this slide be scrollable?
*/ */
scrollable: boolean; scrollable: BooleanOrFunction;
/** /**
* Should this slide be scaleable? * Should this slide be scaleable?
*/ */
scaleable: boolean; scaleable: BooleanOrFunction;
} }
/** /**
* Is similar to router entries, so we can trick the router. Keep slideName and * Is similar to router entries, so we can trick the router. Keep slideName and
* path in sync. * path in sync.
*/ */
export interface SlideManifest extends SlideOptions { export interface SlideManifest extends Slide {
slide: string;
path: string; path: string;
loadChildren: string; loadChildren: string;
verboseName: string; verboseName: string;

View File

@ -1,6 +1,11 @@
from typing import Any, Dict from typing import Any, Dict
from ..utils.projector import AllData, register_projector_slide from ..utils.projector import (
AllData,
ProjectorElementException,
get_config,
register_projector_slide,
)
# Important: All functions have to be prune. This means, that thay can only # Important: All functions have to be prune. This means, that thay can only
@ -23,9 +28,16 @@ def countdown_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any
countdown_id = element.get("id") or 1 countdown_id = element.get("id") or 1
try: try:
return all_data["core/countdown"][countdown_id] countdown = all_data["core/countdown"][countdown_id]
except KeyError: except KeyError:
return {"error": f"Countdown {countdown_id} does not exist"} raise ProjectorElementException(f"Countdown {countdown_id} does not exist")
return {
"description": countdown["description"],
"running": countdown["running"],
"countdown_time": countdown["countdown_time"],
"warning_time": get_config(all_data, "agenda_countdown_warning_time"),
}
def message_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: def message_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
@ -44,7 +56,7 @@ def message_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
try: try:
return all_data["core/projector-message"][message_id] return all_data["core/projector-message"][message_id]
except KeyError: except KeyError:
return {"error": f"Message {message_id} does not exist"} raise ProjectorElementException(f"Message {message_id} does not exist")
def clock_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: def clock_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: