Countdown slide and controls
This commit is contained in:
parent
34feac553b
commit
2801e28704
@ -52,9 +52,9 @@ export class ProjectorService {
|
||||
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
|
||||
): IdentifiableProjectorElement {
|
||||
if (isProjectable(obj)) {
|
||||
return obj.getSlide().getBasicProjectorElement();
|
||||
return obj.getSlide().getBasicProjectorElement({});
|
||||
} else if (isProjectorElementBuildDeskriptor(obj)) {
|
||||
return obj.getBasicProjectorElement();
|
||||
return obj.getBasicProjectorElement({});
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
@ -133,7 +133,9 @@ export class ProjectorService {
|
||||
const element = this.getProjectorElement(obj);
|
||||
|
||||
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);
|
||||
await this.projectRequest(projector, projector.elements);
|
||||
} else {
|
||||
|
@ -8,6 +8,7 @@ import { ViewCountdown } from 'app/site/projector/models/view-countdown';
|
||||
import { Countdown } from 'app/shared/models/core/countdown';
|
||||
import { ViewModelStoreService } from 'app/core/core-services/view-model-store.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ServertimeService } from 'app/core/core-services/servertime.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -18,7 +19,8 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
|
||||
mapperService: CollectionStringMapperService,
|
||||
viewModelStoreService: ViewModelStoreService,
|
||||
private dataSend: DataSendService,
|
||||
private translate: TranslateService
|
||||
private translate: TranslateService,
|
||||
private servertimeService: ServertimeService
|
||||
) {
|
||||
super(DS, mapperService, viewModelStoreService, Countdown);
|
||||
}
|
||||
@ -41,7 +43,21 @@ export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Co
|
||||
await this.dataSend.updateModel(update);
|
||||
}
|
||||
|
||||
public async delete(viewCountdown: ViewCountdown): Promise<void> {
|
||||
await this.dataSend.deleteModel(viewCountdown.countdown);
|
||||
public async delete(countdown: ViewCountdown): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ import { Injectable } from '@angular/core';
|
||||
* // will also result in 70
|
||||
* 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
|
||||
* const c = this.durationService.stringToDuration('01:10b');
|
||||
* ```
|
||||
@ -20,6 +23,9 @@ import { Injectable } from '@angular/core';
|
||||
* ```ts
|
||||
* // will result in 01:10 h
|
||||
* const a = this.durationService.durationToString(70);
|
||||
*
|
||||
* // will result in 00:30 m (30 is interpreted as seconds)
|
||||
* const a = this.durationService.durationToString(30);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({
|
||||
@ -32,13 +38,16 @@ export class DurationService {
|
||||
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
|
||||
* @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 {
|
||||
const splitDuration = durationText.replace('h', '').split(':');
|
||||
public stringToDuration(durationText: string, suffix: 'h' | 'm' = 'h'): number {
|
||||
const splitDuration = durationText.replace(suffix, '').split(':');
|
||||
let time: number;
|
||||
if (splitDuration.length > 1 && !isNaN(+splitDuration[0]) && !isNaN(+splitDuration[1])) {
|
||||
time = +splitDuration[0] * 60 + +splitDuration[1];
|
||||
@ -54,31 +63,18 @@ export class DurationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a duration number (given in minutes)
|
||||
* To a string in HH:MM format
|
||||
* Converts a duration number (given in minutes or seconds)
|
||||
*
|
||||
* @param duration value in minutes
|
||||
* @returns a more human readable time representation
|
||||
*/
|
||||
public durationToString(duration: number): string {
|
||||
const hours = Math.floor(duration / 60);
|
||||
const minutes = `0${Math.floor(duration - hours * 60)}`.slice(-2);
|
||||
if (!isNaN(+hours) && !isNaN(+minutes)) {
|
||||
return `${hours}:${minutes} h`;
|
||||
public durationToString(duration: number, suffix: 'h' | 'm' = 'h'): string {
|
||||
const major = Math.floor(duration / 60);
|
||||
const minor = `0${duration % 60}`.slice(-2);
|
||||
if (!isNaN(+major) && !isNaN(+minor)) {
|
||||
return `${major}:${minor} ${suffix}`;
|
||||
} else {
|
||||
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)}`;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
<div id="countdown" [ngClass]="{
|
||||
'negative': seconds <= 0,
|
||||
'warning_time': seconds <= warningTime && seconds > 0 }">
|
||||
{{ time }}
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,15 +17,15 @@
|
||||
<div *ngFor="let option of options">
|
||||
<div *ngIf="isDecisionOption(option)">
|
||||
<mat-checkbox
|
||||
[checked]="projectorElement[option.key]"
|
||||
(change)="projectorElement[option.key] = !projectorElement[option.key]"
|
||||
[checked]="optionValues[option.key]"
|
||||
(change)="optionValues[option.key] = !optionValues[option.key]"
|
||||
>
|
||||
{{ option.displayName | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="isChoiceOption(option)">
|
||||
<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">
|
||||
{{ choice.displayName | translate }}
|
||||
</mat-radio-button>
|
||||
|
@ -26,7 +26,7 @@ export type ProjectionDialogReturnType = [Projector[], IdentifiableProjectorElem
|
||||
export class ProjectionDialogComponent {
|
||||
public projectors: Projector[];
|
||||
private selectedProjectors: Projector[] = [];
|
||||
public projectorElement: IdentifiableProjectorElement;
|
||||
public optionValues: object = {};
|
||||
public options: SlideOptions;
|
||||
|
||||
public constructor(
|
||||
@ -52,11 +52,9 @@ export class ProjectionDialogComponent {
|
||||
}
|
||||
}
|
||||
|
||||
this.projectorElement = this.projectorElementBuildDescriptor.getBasicProjectorElement();
|
||||
|
||||
// Set option defaults
|
||||
this.projectorElementBuildDescriptor.slideOptions.forEach(option => {
|
||||
this.projectorElement[option.key] = option.default;
|
||||
this.optionValues[option.key] = option.default;
|
||||
});
|
||||
|
||||
this.options = this.projectorElementBuildDescriptor.slideOptions;
|
||||
@ -88,7 +86,9 @@ export class ProjectionDialogComponent {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -4,8 +4,9 @@
|
||||
|
||||
&.content {
|
||||
width: calc(100% - 100px);
|
||||
margin-left: 50px;
|
||||
margin-right: 50px;
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,10 +5,13 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { SlideManager } from 'app/slides/services/slide-manager.service';
|
||||
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 { 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,
|
||||
* and loading of slides.
|
||||
@ -31,24 +34,31 @@ export class SlideContainerComponent extends BaseComponent {
|
||||
private _slideData: SlideData<object>;
|
||||
|
||||
@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 (!data || data.error) {
|
||||
if (!slideData || hasError(slideData) || (slideData.data && hasError(slideData.data))) {
|
||||
// clear slide container:
|
||||
if (this.slide) {
|
||||
this.slide.clear();
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
console.error(data.error);
|
||||
let error;
|
||||
if (hasError(slideData)) {
|
||||
error = slideData.error;
|
||||
} else if (slideData.data && hasError(slideData.data)) {
|
||||
error = slideData.data.error;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._slideData = data;
|
||||
if (this.previousSlideName !== data.element.name) {
|
||||
this.slideChanged(data.element.name);
|
||||
this.previousSlideName = data.element.name;
|
||||
this._slideData = slideData;
|
||||
if (this.previousSlideName !== slideData.element.name) {
|
||||
this.slideChanged(slideData.element);
|
||||
this.previousSlideName = slideData.element.name;
|
||||
}
|
||||
this.setDataForComponent();
|
||||
}
|
||||
@ -88,7 +98,7 @@ export class SlideContainerComponent extends BaseComponent {
|
||||
/**
|
||||
* 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.
|
||||
@ -137,9 +147,19 @@ export class SlideContainerComponent extends BaseComponent {
|
||||
*
|
||||
* @param slideName The slide to load.
|
||||
*/
|
||||
private slideChanged(slideName: string): void {
|
||||
this.slideOptions = this.slideManager.getSlideOptions(slideName);
|
||||
this.slideManager.getSlideFactory(slideName).then(slideFactory => {
|
||||
private slideChanged(element: ProjectorElement): void {
|
||||
const options = this.slideManager.getSlideConfiguration(element.name);
|
||||
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.slideRef = this.slide.createComponent(slideFactory);
|
||||
this.setDataForComponent();
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
export interface ProjectorElementOptions {
|
||||
/**
|
||||
* Additional data.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A projectorelement must have a name and optional attributes.
|
||||
* 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.
|
||||
*/
|
||||
export interface ProjectorElement {
|
||||
export interface ProjectorElement extends ProjectorElementOptions {
|
||||
/**
|
||||
* The name of the element.
|
||||
*/
|
||||
@ -16,11 +23,6 @@ export interface ProjectorElement {
|
||||
* DO NOT read additional data (name is save).
|
||||
*/
|
||||
error?: string;
|
||||
|
||||
/**
|
||||
* Additional data.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IdentifiableProjectorElement extends ProjectorElement {
|
||||
|
@ -82,6 +82,7 @@ import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-b
|
||||
import { OpenSlidesTranslateModule } from '../core/translate/openslides-translate-module';
|
||||
import { ProjectorComponent } from './components/projector/projector.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.
|
||||
@ -201,7 +202,8 @@ import { SlideContainerComponent } from './components/slide-container/slide-cont
|
||||
ProjectorComponent,
|
||||
SlideContainerComponent,
|
||||
OwlDateTimeModule,
|
||||
OwlNativeDateTimeModule
|
||||
OwlNativeDateTimeModule,
|
||||
CountdownTimeComponent
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -227,7 +229,8 @@ import { SlideContainerComponent } from './components/slide-container/slide-cont
|
||||
ResizedDirective,
|
||||
MetaTextBlockComponent,
|
||||
ProjectorComponent,
|
||||
SlideContainerComponent
|
||||
SlideContainerComponent,
|
||||
CountdownTimeComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
|
@ -342,6 +342,6 @@ export class SpeakerListComponent extends BaseViewComponent implements OnInit {
|
||||
const duration = Math.floor(
|
||||
(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')}`;
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ export class ViewTopic extends BaseAgendaViewModel {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
name: Topic.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
getIdentifiers: () => ['name', 'id']
|
||||
|
@ -81,7 +81,7 @@ export class ViewAssignment extends BaseAgendaViewModel {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
name: Assignment.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
getIdentifiers: () => ['name', 'id']
|
||||
|
@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorElementBuildDeskriptor {
|
||||
@ -15,7 +15,7 @@ export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorEle
|
||||
export interface ProjectorElementBuildDeskriptor {
|
||||
slideOptions: SlideOptions;
|
||||
projectionDefaultName?: string;
|
||||
getBasicProjectorElement(): IdentifiableProjectorElement;
|
||||
getBasicProjectorElement(options: ProjectorElementOptions): IdentifiableProjectorElement;
|
||||
|
||||
/**
|
||||
* The title to show in the projection dialog
|
||||
|
@ -1,10 +1,14 @@
|
||||
export interface SlideDecisionOption {
|
||||
interface BaseSlideOption {
|
||||
key: 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 }[];
|
||||
}
|
||||
|
||||
|
@ -561,7 +561,7 @@ export class ViewMotion extends BaseAgendaViewModel implements Searchable {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
name: Motion.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
getIdentifiers: () => ['name', 'id']
|
||||
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -18,6 +18,13 @@
|
||||
<span translate>Required</span>
|
||||
</mat-hint>
|
||||
</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>
|
||||
@ -45,8 +52,10 @@
|
||||
<div class="header-name">
|
||||
{{ countdown.description }}
|
||||
</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"
|
||||
@ -60,11 +69,15 @@
|
||||
<span translate>Required</span>
|
||||
</mat-hint>
|
||||
</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>
|
||||
<ng-container *ngIf="editId !== countdown.id">
|
||||
TODO: Show countdown time etc.
|
||||
</ng-container>
|
||||
<mat-action-row>
|
||||
<button *ngIf="editId !== countdown.id" mat-button class="on-transition-fade" (click)="onEditButton(countdown)"
|
||||
mat-icon-button>
|
||||
|
@ -14,7 +14,7 @@ mat-card {
|
||||
.header-container {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
grid-template-columns: 40px 1fr;
|
||||
grid-template-columns: 40px 1fr 2fr;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
@ -25,10 +25,17 @@ mat-card {
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ 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;
|
||||
@ -10,7 +11,7 @@ describe('CountdownListComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [CountdownListComponent]
|
||||
declarations: [CountdownListComponent, CountdownControlsComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
|
@ -10,9 +10,10 @@ 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 the statute paragraphs.
|
||||
* List view for countdowns.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-countdown-list',
|
||||
@ -37,20 +38,20 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
|
||||
public openId: number | null;
|
||||
public editId: number | null;
|
||||
|
||||
/**
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: CountdownRepositoryService,
|
||||
private formBuilder: FormBuilder,
|
||||
private promptService: PromptService
|
||||
private promptService: PromptService,
|
||||
private durationService: DurationService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
const form = {
|
||||
description: ['', Validators.required]
|
||||
description: ['', Validators.required],
|
||||
default_time: ['', Validators.required]
|
||||
};
|
||||
this.createForm = this.formBuilder.group(form);
|
||||
this.updateForm = this.formBuilder.group(form);
|
||||
@ -75,7 +76,8 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
|
||||
if (!this.countdownToCreate) {
|
||||
this.createForm.reset();
|
||||
this.createForm.setValue({
|
||||
description: ''
|
||||
description: '',
|
||||
default_time: '1:00 m'
|
||||
});
|
||||
this.countdownToCreate = new Countdown();
|
||||
}
|
||||
@ -86,7 +88,17 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
|
||||
*/
|
||||
public create(): void {
|
||||
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.countdownToCreate = null;
|
||||
}, this.raiseError);
|
||||
@ -101,7 +113,8 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
|
||||
this.editId = countdown.id;
|
||||
|
||||
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 {
|
||||
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.raiseError);
|
||||
}
|
||||
@ -123,7 +147,7 @@ export class CountdownListComponent extends BaseViewComponent implements OnInit
|
||||
* @param countdown The countdown to delete
|
||||
*/
|
||||
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)) {
|
||||
this.repo.delete(countdown).then(() => (this.openId = this.editId = null), this.raiseError);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { ProjectorMessageListComponent } from './projector-message-list.component';
|
||||
|
||||
describe('CountdownListComponent', () => {
|
||||
describe('ProjectorMessageListComponent', () => {
|
||||
let component: ProjectorMessageListComponent;
|
||||
let fixture: ComponentFixture<ProjectorMessageListComponent>;
|
||||
|
||||
|
@ -16,6 +16,18 @@ export class ViewCountdown extends BaseProjectableViewModel {
|
||||
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 {
|
||||
return this.countdown.description;
|
||||
}
|
||||
@ -38,13 +50,19 @@ export class ViewCountdown extends BaseProjectableViewModel {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
stable: true,
|
||||
name: Countdown.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
getIdentifiers: () => ['name', 'id']
|
||||
}),
|
||||
slideOptions: [],
|
||||
slideOptions: [
|
||||
{
|
||||
key: 'fullscreen',
|
||||
displayName: 'Fullscreen',
|
||||
default: false
|
||||
}
|
||||
],
|
||||
projectionDefaultName: 'countdowns',
|
||||
getTitle: () => this.getTitle()
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export class ViewProjectorMessage extends BaseProjectableViewModel {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
stable: true,
|
||||
name: ProjectorMessage.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
|
@ -10,6 +10,7 @@ import { ProjectorDataService } from './services/projector-data.service';
|
||||
import { CurrentListOfSpeakersSlideService } from './services/current-list-of-of-speakers-slide.service';
|
||||
import { CountdownListComponent } from './components/countdown-list/countdown-list.component';
|
||||
import { ProjectorMessageListComponent } from './components/projector-message-list/projector-message-list.component';
|
||||
import { CountdownControlsComponent } from './components/countdown-controls/countdown-controls.component';
|
||||
|
||||
@NgModule({
|
||||
providers: [ClockSlideService, ProjectorDataService, CurrentListOfSpeakersSlideService],
|
||||
@ -18,7 +19,8 @@ import { ProjectorMessageListComponent } from './components/projector-message-li
|
||||
ProjectorListComponent,
|
||||
ProjectorDetailComponent,
|
||||
CountdownListComponent,
|
||||
ProjectorMessageListComponent
|
||||
ProjectorMessageListComponent,
|
||||
CountdownControlsComponent
|
||||
]
|
||||
})
|
||||
export class ProjectorModule {}
|
||||
|
@ -22,12 +22,10 @@ export class CurrentListOfSpeakersSlideService {
|
||||
private slideManager: SlideManager
|
||||
) {
|
||||
this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => {
|
||||
if (projector) {
|
||||
if (projector && this.currentItemIds[projector.id]) {
|
||||
const item = this.getCurrentAgendaItemIdForProjector(projector);
|
||||
if (this.currentItemIds[projector.id]) {
|
||||
this.currentItemIds[projector.id].next(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -188,7 +188,7 @@ export class ViewUser extends BaseProjectableViewModel implements Searchable {
|
||||
|
||||
public getSlide(): ProjectorElementBuildDeskriptor {
|
||||
return {
|
||||
getBasicProjectorElement: () => ({
|
||||
getBasicProjectorElement: options => ({
|
||||
name: User.COLLECTIONSTRING,
|
||||
id: this.id,
|
||||
getIdentifiers: () => ['name', 'id']
|
||||
|
49
client/src/app/slides/all-slide-configurations.ts
Normal file
49
client/src/app/slides/all-slide-configurations.ts
Normal 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
|
||||
}
|
||||
];
|
@ -12,8 +12,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'topics/topic',
|
||||
path: 'topics/topic',
|
||||
loadChildren: './slides/agenda/topic/topics-topic-slide.module#TopicsTopicSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true,
|
||||
verboseName: 'Topic',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -22,8 +20,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'motions/motion',
|
||||
path: 'motions/motion',
|
||||
loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true,
|
||||
verboseName: 'Motion',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -32,8 +28,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'users/user',
|
||||
path: 'users/user',
|
||||
loadChildren: './slides/users/user/users-user-slide.module#UsersUserSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true,
|
||||
verboseName: 'Participant',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -42,8 +36,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'core/clock',
|
||||
path: 'core/clock',
|
||||
loadChildren: './slides/core/clock/clock-slide.module#ClockSlideModule',
|
||||
scaleable: false,
|
||||
scrollable: false,
|
||||
verboseName: 'Clock',
|
||||
elementIdentifiers: ['name'],
|
||||
canBeMappedToModel: false
|
||||
@ -52,8 +44,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'core/countdown',
|
||||
path: 'core/countdown',
|
||||
loadChildren: './slides/core/countdown/countdown-slide.module#CountdownSlideModule',
|
||||
scaleable: false,
|
||||
scrollable: false,
|
||||
verboseName: 'Countdown',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -62,8 +52,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'core/projector-message',
|
||||
path: 'core/projector-message',
|
||||
loadChildren: './slides/core/projector-message/projector-message-slide.module#ProjectorMessageSlideModule',
|
||||
scaleable: false,
|
||||
scrollable: false,
|
||||
verboseName: 'Message',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
@ -73,8 +61,6 @@ export const allSlides: SlideManifest[] = [
|
||||
path: 'agenda/current-list-of-speakers',
|
||||
loadChildren:
|
||||
'./slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module#AgendaCurrentListOfSpeakersSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true,
|
||||
verboseName: 'Current list of speakers',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: false
|
||||
@ -84,8 +70,6 @@ export const allSlides: SlideManifest[] = [
|
||||
path: 'agenda/current-list-of-speakers-overlay',
|
||||
loadChildren:
|
||||
'./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',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: false
|
||||
@ -94,8 +78,6 @@ export const allSlides: SlideManifest[] = [
|
||||
slide: 'assignments/assignment',
|
||||
path: 'assignments/assignment',
|
||||
loadChildren: './slides/assignments/assignment/assignment-slide.module#AssignmentSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true,
|
||||
verboseName: 'Election',
|
||||
elementIdentifiers: ['name', 'id'],
|
||||
canBeMappedToModel: true
|
||||
|
@ -15,6 +15,8 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
|
||||
|
||||
private servertimeSubscription: Subscription | null = null;
|
||||
|
||||
private clockInterval: any;
|
||||
|
||||
public constructor(private servertimeService: ServertimeService) {
|
||||
super();
|
||||
}
|
||||
@ -26,7 +28,7 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
|
||||
.subscribe(() => this.updateClock());
|
||||
|
||||
// Update clock every 10 seconds.
|
||||
setInterval(() => this.updateClock(), 10 * 1000);
|
||||
this.clockInterval = setInterval(() => this.updateClock(), 10 * 1000);
|
||||
}
|
||||
|
||||
private updateClock(): void {
|
||||
@ -42,5 +44,8 @@ export class ClockSlideComponent extends BaseSlideComponent<{}> implements OnIni
|
||||
if (this.servertimeSubscription) {
|
||||
this.servertimeSubscription.unsubscribe();
|
||||
}
|
||||
if (this.clockInterval) {
|
||||
clearInterval(this.clockInterval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
export interface CountdownSlideData {
|
||||
error: string;
|
||||
description: string;
|
||||
countdown_time: number;
|
||||
running: boolean;
|
||||
warning_time: number;
|
||||
}
|
||||
|
@ -1,3 +1,11 @@
|
||||
<div id="outer">
|
||||
COUNTDOWN
|
||||
<div class="countdown overlay" *ngIf="data && !data.element.fullscreen">
|
||||
<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>
|
||||
|
@ -1,10 +1,42 @@
|
||||
#outer {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
.countdown {
|
||||
z-index: 8;
|
||||
|
||||
&.fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
background-color: green;
|
||||
height: 30px;
|
||||
margin: 10px;
|
||||
margin-top: 100px;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { CountdownSlideData } from './countdown-slide-data';
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
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_MANIFESTS } from '../slide-manifest';
|
||||
import { BaseSlideComponent } from '../base-slide-component';
|
||||
import { ProjectorElement, IdentifiableProjectorElement } from 'app/shared/models/core/projector';
|
||||
import { allSlidesDynamicConfiguration } from '../all-slide-configurations';
|
||||
|
||||
/**
|
||||
* Cares about loading slides dynamically.
|
||||
@ -12,6 +13,7 @@ import { ProjectorElement, IdentifiableProjectorElement } from 'app/shared/model
|
||||
@Injectable()
|
||||
export class SlideManager {
|
||||
private loadedSlides: { [name: string]: SlideManifest } = {};
|
||||
private loadedSlideConfigurations: { [name: string]: SlideDynamicConfiguration & Slide } = {};
|
||||
|
||||
public constructor(
|
||||
@Inject(SLIDE_MANIFESTS) private manifests: SlideManifest[],
|
||||
@ -21,6 +23,9 @@ export class SlideManager {
|
||||
this.manifests.forEach(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
|
||||
* @returns SlideOptions for the requested slide.
|
||||
*/
|
||||
public getSlideOptions(slideName: string): SlideOptions {
|
||||
return this.getManifest(slideName);
|
||||
public getSlideConfiguration(slideName: string): SlideDynamicConfiguration {
|
||||
if (!this.loadedSlideConfigurations[slideName]) {
|
||||
throw new Error(`Could not find slide for "${slideName}"`);
|
||||
}
|
||||
return this.loadedSlideConfigurations[slideName];
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public getIdentifialbeProjectorElement(element: ProjectorElement): IdentifiableProjectorElement {
|
||||
const identifiableElement: IdentifiableProjectorElement = element as IdentifiableProjectorElement;
|
||||
const identifiers = this.getManifest(element.name).elementIdentifiers.map(x => x); // map to copy.
|
||||
|
@ -1,27 +1,32 @@
|
||||
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.
|
||||
*/
|
||||
export interface SlideOptions {
|
||||
export interface SlideDynamicConfiguration {
|
||||
/**
|
||||
* Should this slide be scrollable?
|
||||
*/
|
||||
scrollable: boolean;
|
||||
scrollable: BooleanOrFunction;
|
||||
|
||||
/**
|
||||
* Should this slide be scaleable?
|
||||
*/
|
||||
scaleable: boolean;
|
||||
scaleable: BooleanOrFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is similar to router entries, so we can trick the router. Keep slideName and
|
||||
* path in sync.
|
||||
*/
|
||||
export interface SlideManifest extends SlideOptions {
|
||||
slide: string;
|
||||
export interface SlideManifest extends Slide {
|
||||
path: string;
|
||||
loadChildren: string;
|
||||
verboseName: string;
|
||||
|
@ -1,6 +1,11 @@
|
||||
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
|
||||
@ -23,9 +28,16 @@ def countdown_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any
|
||||
countdown_id = element.get("id") or 1
|
||||
|
||||
try:
|
||||
return all_data["core/countdown"][countdown_id]
|
||||
countdown = all_data["core/countdown"][countdown_id]
|
||||
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]:
|
||||
@ -44,7 +56,7 @@ def message_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]:
|
||||
try:
|
||||
return all_data["core/projector-message"][message_id]
|
||||
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]:
|
||||
|
Loading…
Reference in New Issue
Block a user