Merge pull request #4389 from FinnStutzenstein/chyron

current speaker chyron
This commit is contained in:
Emanuel Schütze 2019-02-26 22:36:10 +01:00 committed by GitHub
commit 23c1857fa6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 521 additions and 183 deletions

View File

@ -1,10 +1,10 @@
<div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container> <div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container>
<div id="projector" [ngStyle]="projectorStyle"> <div id="projector" [ngStyle]="projectorStyle">
<div id="header" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter"> <div id="header" [ngStyle]="headerFooterStyle" *ngIf="projector && projector.show_header_footer">
<!-- projector logo --> <!-- projector logo -->
<img *ngIf="enableLogo && projectorLogo" src="{{ projectorLogo }}" class="projector-logo-main" /> <img *ngIf="enableLogo && projectorLogo" src="{{ projectorLogo }}" class="projector-logo-main" />
<div *ngIf="enableTitle" id="eventdata"> <div *ngIf="projector.show_title" id="eventdata">
<div <div
*ngIf="eventName" *ngIf="eventName"
class="event-name" class="event-name"
@ -19,7 +19,7 @@
<os-slide-container [slideData]="slide" [scroll]="scroll" [scale]="scale"></os-slide-container> <os-slide-container [slideData]="slide" [scroll]="scroll" [scale]="scale"></os-slide-container>
</div> </div>
<div id="footer" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter"> <div id="footer" [ngStyle]="headerFooterStyle" *ngIf="projector && projector.show_header_footer">
<div class="footertext"> <div class="footertext">
<span *ngIf="eventDate"> {{ eventDate }} </span> <span *ngIf="eventDate"> {{ eventDate }} </span>
<span *ngIf="eventDate && eventLocation"> | </span> <span *ngIf="eventDate && eventLocation"> | </span>

View File

@ -52,6 +52,9 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
) { ) {
this.updateScaling(); this.updateScaling();
} }
this.projectorStyle['background-color'] = projector.background_color;
this.headerFooterStyle.color = projector.header_font_color;
this.headerFooterStyle['background-color'] = projector.header_background_color;
} }
} }
@ -94,7 +97,8 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
/** /**
* Dynamic style attributes for the header and footer. * Dynamic style attributes for the header and footer.
*/ */
public headerFooterStyle: { 'background-color': string; color: string } = { public headerFooterStyle: { 'background-image': string; 'background-color': string; color: string } = {
'background-image': 'none',
'background-color': 'blue', 'background-color': 'blue',
color: 'white' color: 'white'
}; };
@ -156,26 +160,6 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
private configService: ConfigService private configService: ConfigService
) { ) {
super(titleService, translate); super(titleService, translate);
// Get all important config variables.
// enable header/footer
this.configService
.get<boolean>('projector_enable_header_footer')
.subscribe(val => (this.enableHeaderAndFooter = val));
this.configService.get<boolean>('projector_enable_title').subscribe(val => (this.enableTitle = val));
// projector colors
this.configService
.get<string>('projector_header_fontcolor')
.subscribe(val => (this.headerFooterStyle.color = val));
this.configService
.get<string>('projector_header_backgroundcolor')
.subscribe(val => (this.headerFooterStyle['background-color'] = val));
this.configService
.get<string>('projector_background_color')
.subscribe(val => (this.projectorStyle['background-color'] = val));
// projector logo / background-image // projector logo / background-image
this.configService.get<boolean>('projector_enable_logo').subscribe(val => (this.enableLogo = val)); this.configService.get<boolean>('projector_enable_logo').subscribe(val => (this.enableLogo = val));
this.configService.get<{ path?: string }>('logo_projector_main').subscribe(val => { this.configService.get<{ path?: string }>('logo_projector_main').subscribe(val => {
@ -186,6 +170,8 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
this.configService.get<{ path?: string }>('logo_projector_header').subscribe(val => { this.configService.get<{ path?: string }>('logo_projector_header').subscribe(val => {
if (val && val.path) { if (val && val.path) {
this.headerFooterStyle['background-image'] = "url('" + val.path + "')"; this.headerFooterStyle['background-image'] = "url('" + val.path + "')";
} else {
this.headerFooterStyle['background-image'] = 'none';
} }
}); });

View File

@ -65,6 +65,13 @@ export class Projector extends BaseModel<Projector> {
public height: number; public height: number;
public reference_projector_id: number; public reference_projector_id: number;
public projectiondefaults: ProjectionDefault[]; public projectiondefaults: ProjectionDefault[];
public background_color: string;
public header_background_color: string;
public header_font_color: string;
public header_h1_color: string;
public show_header_footer: boolean;
public show_title: boolean;
public show_logo: boolean;
public constructor(input?: any) { public constructor(input?: any) {
super(Projector.COLLECTIONSTRING, input); super(Projector.COLLECTIONSTRING, input);

View File

@ -9,7 +9,6 @@ import { BehaviorSubject, Subscription } from 'rxjs';
import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { CurrentListOfSpeakersSlideService } from 'app/site/projector/services/current-list-of-of-speakers-slide.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service'; import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { PromptService } from 'app/core/ui-services/prompt.service'; import { PromptService } from 'app/core/ui-services/prompt.service';
@ -20,6 +19,7 @@ import { ViewProjector } from 'app/site/projector/models/view-projector';
import { ViewUser } from 'app/site/users/models/view-user'; import { ViewUser } from 'app/site/users/models/view-user';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { DurationService } from 'app/core/ui-services/duration.service'; import { DurationService } from 'app/core/ui-services/duration.service';
import { CurrentAgendaItemService } from 'app/site/projector/services/current-agenda-item.service';
/** /**
* The list of speakers for agenda items. * The list of speakers for agenda items.
@ -104,7 +104,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
* @param itemRepo Repository fpr agenda items * @param itemRepo Repository fpr agenda items
* @param op the current operator * @param op the current operator
* @param promptService * @param promptService
* @param currentListOfSpeakersService * @param currentAgendaItemService
* @param durationService helper for speech duration display * @param durationService helper for speech duration display
*/ */
public constructor( public constructor(
@ -116,7 +116,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
private itemRepo: ItemRepositoryService, private itemRepo: ItemRepositoryService,
private op: OperatorService, private op: OperatorService,
private promptService: PromptService, private promptService: PromptService,
private currentListOfSpeakersService: CurrentListOfSpeakersSlideService, private currentAgendaItemService: CurrentAgendaItemService,
private durationService: DurationService, private durationService: DurationService,
private userRepository: UserRepositoryService private userRepository: UserRepositoryService
) { ) {
@ -185,7 +185,7 @@ export class ListOfSpeakersComponent extends BaseViewComponent implements OnInit
this.viewItem = null; this.viewItem = null;
} }
this.projectorSubscription = this.currentListOfSpeakersService this.projectorSubscription = this.currentAgendaItemService
.getAgendaItemObservable(projector) .getAgendaItemObservable(projector)
.subscribe(item => { .subscribe(item => {
if (item) { if (item) {

View File

@ -131,6 +131,18 @@
</div> </div>
</div> </div>
<div>
<h4>Current speaker chyron</h4>
<mat-list>
<mat-list-item [ngClass]="{'projected': isChyronProjected()}">
<button type="button" mat-icon-button (click)="toggleChyron()">
<mat-icon>videocam</mat-icon>
</button>
<span translate>Current speaker chyron</span>
</mat-list-item>
</mat-list>
</div>
<div class="queue"> <div class="queue">
<h5 translate>Queue</h5> <h5 translate>Queue</h5>
<div cdkDropList class="drop-list" (cdkDropListDropped)="onSortingChange($event)"> <div cdkDropList class="drop-list" (cdkDropListDropped)="onSortingChange($event)">

View File

@ -21,6 +21,7 @@ import { ViewProjectorMessage } from 'app/site/projector/models/view-projector-m
import { ViewCountdown } from 'app/site/projector/models/view-countdown'; import { ViewCountdown } from 'app/site/projector/models/view-countdown';
import { Projectable } from 'app/site/base/projectable'; import { Projectable } from 'app/site/base/projectable';
import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service'; import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service';
import { CurrentSpeakerChyronSlideService } from '../../services/current-speaker-chyron-slide.service';
/** /**
* The projector detail view. * The projector detail view.
@ -59,7 +60,8 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
private slideManager: SlideManager, private slideManager: SlideManager,
private countdownRepo: CountdownRepositoryService, private countdownRepo: CountdownRepositoryService,
private messageRepo: ProjectorMessageRepositoryService, private messageRepo: ProjectorMessageRepositoryService,
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService,
private currentSpeakerChyronService: CurrentSpeakerChyronSlideService
) { ) {
super(titleService, translate, matSnackBar); super(titleService, translate, matSnackBar);
@ -148,4 +150,12 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public toggleClos(stable: boolean): void { public toggleClos(stable: boolean): void {
this.currentListOfSpeakersSlideService.toggleOn(this.projector, stable); this.currentListOfSpeakersSlideService.toggleOn(this.projector, stable);
} }
public isChyronProjected(): boolean {
return this.currentSpeakerChyronService.isProjectedOn(this.projector);
}
public toggleChyron(): void {
this.currentSpeakerChyronService.toggleOn(this.projector);
}
} }

View File

@ -97,7 +97,52 @@
<mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider> <mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider>
{{ updateForm.value.width }} {{ updateForm.value.width }}
<!-- Clock --> <!-- colors -->
<mat-form-field>
<span translate>Background color</span>
<input matInput formControlName="background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header background color</span>
<input matInput formControlName="header_background_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_background_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header font color</span>
<input matInput formControlName="header_font_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_font_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<mat-form-field>
<span translate>Header headline color</span>
<input matInput formControlName="header_h1_color" type="color" />
<mat-hint *ngIf="!updateForm.controls.header_h1_color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
<!-- checkboxes -->
<div>
<mat-checkbox formControlName="show_header_footer">
<span translate>Show header and footer</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_title">
<span translate>Show title</span>
</mat-checkbox>
</div>
<div>
<mat-checkbox formControlName="show_logo">
<span translate>Show logo</span>
</mat-checkbox>
</div>
<div> <div>
<mat-checkbox formControlName="clock"> <mat-checkbox formControlName="clock">
<span translate>Show clock</span> <span translate>Show clock</span>

View File

@ -105,7 +105,14 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
aspectRatio: ['', Validators.required], aspectRatio: ['', Validators.required],
width: [0, Validators.required], width: [0, Validators.required],
clock: [true], clock: [true],
reference_projector_id: [] reference_projector_id: [],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
header_h1_color: ['', Validators.required],
show_header_footer: [],
show_title: [],
show_logo: []
}); });
} }
@ -202,11 +209,21 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
const reference_projector_id = projector.reference_projector_id const reference_projector_id = projector.reference_projector_id
? projector.reference_projector_id ? projector.reference_projector_id
: projector.id; : projector.id;
this.updateForm.patchValue({ /*this.updateForm.patchValue({
name: projector.name, name: projector.name,
aspectRatio: this.getAspectRatioKey(projector), aspectRatio: this.getAspectRatioKey(projector),
width: projector.width, width: projector.width,
clock: this.clockSlideService.isProjectedOn(projector), clock: this.clockSlideService.isProjectedOn(projector),
reference_projector_id: reference_projector_id,
background_color: projector.background_color,
show_header_footer: projector.show_header_footer,
show_title: projector.show_title,
show_logo: projector.show_logo
});*/
this.updateForm.patchValue(projector.projector);
this.updateForm.patchValue({
aspectRatio: this.getAspectRatioKey(projector),
clock: this.clockSlideService.isProjectedOn(projector),
reference_projector_id: reference_projector_id reference_projector_id: reference_projector_id
}); });
} }
@ -231,12 +248,21 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
if (projector.id !== this.editId || !this.updateForm.valid) { if (projector.id !== this.editId || !this.updateForm.valid) {
return; return;
} }
const updateProjector: Partial<Projector> = { const updateProjector: Partial<Projector> = this.updateForm.value;
/*const updateProjector: Partial<Projector> = {
name: this.updateForm.value.name, name: this.updateForm.value.name,
width: this.updateForm.value.width, width: this.updateForm.value.width,
height: Math.round(this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio]), height: Math.round(this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio]),
reference_projector_id: this.updateForm.value.reference_projector_id reference_projector_id: this.updateForm.value.reference_projector_id,
}; background_color: this.updateForm.value.background_color,
show_header_footer: this.updateForm.value.show_header_footer,
show_title: this.updateForm.value.show_title,
show_logo: this.updateForm.value.show_logo
};*/
updateProjector.height = Math.round(
this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio]
);
try { try {
await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock); await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, projector); await this.repo.update(updateProjector, projector);

View File

@ -54,6 +54,34 @@ export class ViewProjector extends BaseViewModel {
return this.projector.reference_projector_id; return this.projector.reference_projector_id;
} }
public get background_color(): string {
return this.projector.background_color;
}
public get header_background_color(): string {
return this.projector.header_background_color;
}
public get header_font_color(): string {
return this.projector.header_font_color;
}
public get header_h1_color(): string {
return this.projector.header_h1_color;
}
public get show_header_footer(): boolean {
return this.projector.show_header_footer;
}
public get show_title(): boolean {
return this.projector.show_title;
}
public get show_logo(): boolean {
return this.projector.show_logo;
}
/** /**
* This is set by the repository * This is set by the repository
*/ */

View File

@ -0,0 +1,71 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ProjectorService } from 'app/core/core-services/projector.service';
import { ViewProjector } from '../models/view-projector';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
import { ViewItem } from 'app/site/agenda/models/view-item';
/**
* Observes the projector config for a given projector and returns a observable of the
* current view item displayed at on the projector.
*/
@Injectable({
providedIn: 'root'
})
export class CurrentAgendaItemService {
private currentItemIds: { [projectorId: number]: BehaviorSubject<ViewItem | null> } = {};
public constructor(
private projectorService: ProjectorService,
private projectorRepo: ProjectorRepositoryService,
private slideManager: SlideManager
) {
this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => {
if (projector && this.currentItemIds[projector.id]) {
const item = this.getCurrentAgendaItemIdForProjector(projector);
this.currentItemIds[projector.id].next(item);
}
});
}
/**
* Returns an observable for the agenda item id of the currently projected element on the
* given projector.
*
* @param projector The projector to observe.
* @returns An observalbe for the agenda item id. Null, if no element with an agenda item is shown.
*/
public getAgendaItemObservable(projector: ViewProjector): Observable<ViewItem | null> {
if (!this.currentItemIds[projector.id]) {
const item = this.getCurrentAgendaItemIdForProjector(projector);
this.currentItemIds[projector.id] = new BehaviorSubject<ViewItem | null>(item);
}
return this.currentItemIds[projector.id].asObservable();
}
/**
* Tries to get the agenda item id for one non stable element on the projector.
*
* @param projector The projector
* @returns The agenda item id or null, if there is no such projector element.
*/
private getCurrentAgendaItemIdForProjector(projector: ViewProjector): ViewItem | null {
const nonStableElements = projector.elements.filter(element => !element.stable);
if (nonStableElements.length > 0) {
const nonStableElement = this.slideManager.getIdentifialbeProjectorElement(nonStableElements[0]); // The normal case is just one non stable slide
try {
const viewModel = this.projectorService.getViewModelFromProjectorElement(nonStableElement);
if (viewModel instanceof BaseAgendaViewModel) {
return viewModel.getAgendaItem();
}
} catch (e) {
// make TypeScript silent.
}
}
return null;
}
}

View File

@ -1,33 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ProjectorService } from 'app/core/core-services/projector.service'; import { ProjectorService } from 'app/core/core-services/projector.service';
import { ViewProjector } from '../models/view-projector'; import { ViewProjector } from '../models/view-projector';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector'; import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { ProjectorRepositoryService } from 'app/core/repositories/projector/projector-repository.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { BaseAgendaViewModel } from 'app/site/base/base-agenda-view-model';
import { ViewItem } from 'app/site/agenda/models/view-item';
/** /**
* Handles the curent list of speakers slide. Manages the projection and provides
* function to check, if it is projected. Handles the overlay and slide.
*/ */
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class CurrentListOfSpeakersSlideService { export class CurrentListOfSpeakersSlideService {
private currentItemIds: { [projectorId: number]: BehaviorSubject<ViewItem | null> } = {}; public constructor(private projectorService: ProjectorService) {}
public constructor(
private projectorService: ProjectorService,
private projectorRepo: ProjectorRepositoryService,
private slideManager: SlideManager
) {
this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => {
if (projector && this.currentItemIds[projector.id]) {
const item = this.getCurrentAgendaItemIdForProjector(projector);
this.currentItemIds[projector.id].next(item);
}
});
}
/** /**
* Returns the basic projector element for the CLOS slide. If overlay=True, the projector element * Returns the basic projector element for the CLOS slide. If overlay=True, the projector element
@ -44,43 +29,6 @@ export class CurrentListOfSpeakersSlideService {
}; };
} }
/**
* Returns an observable for the agenda item id of the currently projected element on the
* given projector.
*
* @param projector The projector to observe.
* @returns An observalbe for the agenda item id. Null, if no element with an agenda item is shown.
*/
public getAgendaItemObservable(projector: ViewProjector): Observable<ViewItem | null> {
if (!this.currentItemIds[projector.id]) {
const item = this.getCurrentAgendaItemIdForProjector(projector);
this.currentItemIds[projector.id] = new BehaviorSubject<ViewItem | null>(item);
}
return this.currentItemIds[projector.id].asObservable();
}
/**
* Tries to get the agenda item id for one non stable element on the projector.
*
* @param projector The projector
* @returns The agenda item id or null, if there is no such projector element.
*/
private getCurrentAgendaItemIdForProjector(projector: ViewProjector): ViewItem | null {
const nonStableElements = projector.elements.filter(element => !element.stable);
if (nonStableElements.length > 0) {
const nonStableElement = this.slideManager.getIdentifialbeProjectorElement(nonStableElements[0]); // The normal case is just one non stable slide
try {
const viewModel = this.projectorService.getViewModelFromProjectorElement(nonStableElement);
if (viewModel instanceof BaseAgendaViewModel) {
return viewModel.getAgendaItem();
}
} catch (e) {
// make TypeScript silent.
}
}
return null;
}
/** /**
* Queries, if the slide/overlay is projected on the given projector. * Queries, if the slide/overlay is projected on the given projector.
* *

View File

@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { ProjectorService } from 'app/core/core-services/projector.service';
import { ViewProjector } from '../models/view-projector';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
/**
*/
@Injectable({
providedIn: 'root'
})
export class CurrentSpeakerChyronSlideService {
public constructor(private projectorService: ProjectorService) {}
/**
* Returns the basic projector element for the chyron slide
*
* @returns the identifiable chyron projector element.
*/
private getCurrentSpeakerChyronProjectorElement(): IdentifiableProjectorElement {
return {
name: 'agenda/current-speaker-chyron',
stable: true,
getIdentifiers: () => ['name']
};
}
/**
* Queries, if the slide is projected on the given projector.
*
* @param projector The projector
* @returns if the slide is projected on the projector
*/
public isProjectedOn(projector: ViewProjector): boolean {
return this.projectorService.isProjectedOn(this.getCurrentSpeakerChyronProjectorElement(), projector.projector);
}
/**
* Toggle the projection state of the slide on the given projector
*
* @param projector The projector
*/
public async toggleOn(projector: ViewProjector): Promise<void> {
const isClosProjected = this.isProjectedOn(projector);
if (isClosProjected) {
await this.projectorService.removeFrom(projector.projector, this.getCurrentSpeakerChyronProjectorElement());
} else {
await this.projectorService.projectOn(projector.projector, this.getCurrentSpeakerChyronProjectorElement());
}
}
}

View File

@ -16,6 +16,7 @@
<div *ngIf="data.data.current" class="currentSpeaker"> <div *ngIf="data.data.current" class="currentSpeaker">
<mat-icon>mic</mat-icon> <mat-icon>mic</mat-icon>
<span>{{ data.data.current.user }}</span> <span>{{ data.data.current.user }}</span>
<mat-icon *ngIf="data.data.current.marked">star</mat-icon>
</div> </div>
<!-- Next speakers --> <!-- Next speakers -->

View File

@ -0,0 +1,3 @@
export interface CurrentSpeakerChyronSlideData {
current_speaker?: string;
}

View File

@ -0,0 +1,5 @@
<div id="chyron" *ngIf="data">
<span id="inner">
{{ data.data.current_speaker }}
</span>
</div>

View File

@ -0,0 +1,19 @@
#chyron {
position: absolute;
left: 0;
bottom: 0;
z-index: 10;
width: 100%;
height: 100px;
font-size: 32px;
text-align: center;
line-height: 1.1;
display: table;
#inner {
display: table-cell;
vertical-align: middle;
padding-left: 20px;
padding-right: 20px;
}
}

View File

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

View File

@ -0,0 +1,15 @@
import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CurrentSpeakerChyronSlideData } from './current-speaker-chyron-slide-data';
@Component({
selector: 'os-current-speaker-chyron-speakers-slide',
templateUrl: './current-speaker-chyron-slide.component.html',
styleUrls: ['./current-speaker-chyron-slide.component.scss']
})
export class CurrentSpeakerChyronSlideComponent extends BaseSlideComponent<CurrentSpeakerChyronSlideData> {
public constructor() {
super();
}
}

View File

@ -0,0 +1,13 @@
import { CurrentSpeakerChyronSlideModule } from './current-speaker-chyron-slide.module';
describe('CurrentSpeakerChyronSlideModule', () => {
let currentSpeakerChyronSlideModule: CurrentSpeakerChyronSlideModule;
beforeEach(() => {
currentSpeakerChyronSlideModule = new CurrentSpeakerChyronSlideModule();
});
it('should create an instance', () => {
expect(currentSpeakerChyronSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { CurrentSpeakerChyronSlideComponent } from './current-speaker-chyron-slide.component';
@NgModule(makeSlideModule(CurrentSpeakerChyronSlideComponent))
export class CurrentSpeakerChyronSlideModule {}

View File

@ -72,6 +72,11 @@ export const allSlidesDynamicConfiguration: (SlideDynamicConfiguration & Slide)[
scaleable: false, scaleable: false,
scrollable: false scrollable: false
}, },
{
slide: 'agenda/current-speaker-chyron',
scaleable: false,
scrollable: false
},
{ {
slide: 'assignments/assignment', slide: 'assignments/assignment',
scaleable: true, scaleable: true,

View File

@ -78,7 +78,7 @@ export const allSlides: SlideManifest[] = [
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers/current-list-of-speakers-slide.module#CurrentListOfSpeakersSlideModule', './slides/agenda/current-list-of-speakers/current-list-of-speakers-slide.module#CurrentListOfSpeakersSlideModule',
verboseName: 'Current list of speakers', verboseName: 'Current list of speakers',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name'],
canBeMappedToModel: false canBeMappedToModel: false
}, },
{ {
@ -87,7 +87,16 @@ export const allSlides: SlideManifest[] = [
loadChildren: loadChildren:
'./slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.module#CurrentListOfSpeakersOverlaySlideModule', './slides/agenda/current-list-of-speakers-overlay/current-list-of-speakers-overlay-slide.module#CurrentListOfSpeakersOverlaySlideModule',
verboseName: 'Current list of speakers overlay', verboseName: 'Current list of speakers overlay',
elementIdentifiers: ['name', 'id'], elementIdentifiers: ['name'],
canBeMappedToModel: false
},
{
slide: 'agenda/current-speaker-chyron',
path: 'agenda/current-speaker-chyron',
loadChildren:
'./slides/agenda/current-speaker-chyron/current-speaker-chyron-slide.module#CurrentSpeakerChyronSlideModule',
verboseName: 'Current speaker chyron',
elementIdentifiers: ['name'],
canBeMappedToModel: false canBeMappedToModel: false
}, },
{ {

View File

@ -65,8 +65,14 @@
background-color: mat-color($background, app-bar); background-color: mat-color($background, app-bar);
} }
// drag & drop view /* drag & drop views */
.node-content-wrapper { .node-content-wrapper {
background-color: mat-color($background, app-bar); background-color: mat-color($background, app-bar);
} }
/* projector components */
#chyron {
background-color: mat-color($primary);
color: white; // TODO
}
} }

View File

@ -2,6 +2,10 @@
* { * {
font-family: customProjectorFont; font-family: customProjectorFont;
} }
.material-icons {
font-family: 'Material Icons';
}
h2 { h2 {
line-height: 40px; line-height: 40px;

View File

@ -1,5 +1,5 @@
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List from typing import Any, Dict, List, Union
from ..users.projector import get_user_name from ..users.projector import get_user_name
from ..utils.projector import ( from ..utils.projector import (
@ -149,31 +149,16 @@ def get_list_of_speakers_slide_data(all_data: AllData, item_id: int) -> Dict[str
} }
def current_list_of_speakers_slide( def get_current_item_id_for_projector(
all_data: AllData, element: Dict[str, Any], projector_id: int all_data: AllData, projector: Dict[str, Any]
) -> Dict[str, Any]: ) -> Union[int, None]:
""" """
The current list of speakers slide. Creates the data for the given projector. Search for elements, that do have an agenda item:
Try to get a model by the collection and id in the element. This
model needs to have a 'agenda_item_id'. This item must exist. The first
matching element is taken.
""" """
try: elements = projector["elements"]
this_projector = all_data["core/projector"][projector_id]
except KeyError:
raise ProjectorElementException(f"Projector {projector_id} does not exist")
reference_projector_id = this_projector["reference_projector_id"] or projector_id
try:
reference_projector = all_data["core/projector"][reference_projector_id]
except KeyError:
raise ProjectorElementException(
f"Projector {reference_projector_id} does not exist"
)
# Search for elements, that do have an agenda item:
# Try to get a model by the collection and id in the element. This
# model needs to have a 'agenda_item_id'. This item must exist. The first
# matching element is taken.
elements = reference_projector["elements"]
item_id = None item_id = None
for element in elements: for element in elements:
if "id" not in element: if "id" not in element:
@ -193,12 +178,69 @@ def current_list_of_speakers_slide(
item_id = model["agenda_item_id"] item_id = model["agenda_item_id"]
break break
return item_id
def get_reference_projector(all_data: AllData, projector_id: int) -> Dict[str, Any]:
"""
Returns the reference projector to the given projector (by id)
"""
try:
this_projector = all_data["core/projector"][projector_id]
except KeyError:
raise ProjectorElementException(f"Projector {projector_id} does not exist")
reference_projector_id = this_projector["reference_projector_id"] or projector_id
try:
reference_projector = all_data["core/projector"][reference_projector_id]
except KeyError:
raise ProjectorElementException(
f"Projector {reference_projector_id} does not exist"
)
return reference_projector
def current_list_of_speakers_slide(
all_data: AllData, element: Dict[str, Any], projector_id: int
) -> Dict[str, Any]:
"""
The current list of speakers slide. Creates the data for the given projector.
"""
reference_projector = get_reference_projector(all_data, projector_id)
item_id = get_current_item_id_for_projector(all_data, reference_projector)
if item_id is None: # no element found if item_id is None: # no element found
return {} return {}
return get_list_of_speakers_slide_data(all_data, item_id) return get_list_of_speakers_slide_data(all_data, item_id)
def current_speaker_chyron_slide(
all_data: AllData, element: Dict[str, Any], projector_id: int
) -> Dict[str, Any]:
"""
Returns the username for the current speaker.
"""
reference_projector = get_reference_projector(all_data, projector_id)
item_id = get_current_item_id_for_projector(all_data, reference_projector)
if item_id is None: # no element found
return {}
# get item
try:
item = all_data["agenda/item"][item_id]
except KeyError:
raise ProjectorElementException(f"Item {item_id} does not exist")
# find current speaker
current_speaker = None
for speaker in item["speakers"]:
if speaker["begin_time"] is not None and speaker["end_time"] is None:
current_speaker = get_user_name(all_data, speaker["user_id"])
return {"current_speaker": current_speaker}
def register_projector_slides() -> None: def register_projector_slides() -> None:
register_projector_slide("agenda/item-list", item_list_slide) register_projector_slide("agenda/item-list", item_list_slide)
register_projector_slide("agenda/list-of-speakers", list_of_speakers_slide) register_projector_slide("agenda/list-of-speakers", list_of_speakers_slide)
@ -208,3 +250,6 @@ def register_projector_slides() -> None:
register_projector_slide( register_projector_slide(
"agenda/current-list-of-speakers-overlay", current_list_of_speakers_slide "agenda/current-list-of-speakers-overlay", current_list_of_speakers_slide
) )
register_projector_slide(
"agenda/current-speaker-chyron", current_speaker_chyron_slide
)

View File

@ -203,62 +203,6 @@ def get_config_variables():
group="Projector", group="Projector",
) )
yield ConfigVariable(
name="projector_enable_logo",
default_value=True,
input_type="boolean",
label="Show logo on projector",
help_text="You can replace the logo by uploading an image and set it as "
'the "Projector logo" in "files".',
weight=152,
group="Projector",
)
yield ConfigVariable(
name="projector_enable_title",
default_value=True,
input_type="boolean",
label="Show title and description of event on projector",
weight=155,
group="Projector",
)
yield ConfigVariable(
name="projector_enable_header_footer",
default_value=True,
input_type="boolean",
label="Display header and footer",
weight=157,
group="Projector",
)
yield ConfigVariable(
name="projector_header_backgroundcolor",
default_value="#317796",
input_type="colorpicker",
label="Background color of projector header and footer",
weight=160,
group="Projector",
)
yield ConfigVariable(
name="projector_header_fontcolor",
default_value="#F5F5F5",
input_type="colorpicker",
label="Font color of projector header and footer",
weight=165,
group="Projector",
)
yield ConfigVariable(
name="projector_h1_fontcolor",
default_value="#317796",
input_type="colorpicker",
label="Font color of projector headline",
weight=170,
group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name="projector_default_countdown", name="projector_default_countdown",
default_value=60, default_value=60,
@ -268,15 +212,6 @@ def get_config_variables():
group="Projector", group="Projector",
) )
yield ConfigVariable(
name="projector_background_color",
default_value="#FFFFFF",
input_type="colorpicker",
label="Backgroundolor of the projector",
weight=190,
group="Projector",
)
yield ConfigVariable( yield ConfigVariable(
name="projector_currentListOfSpeakers_reference", name="projector_currentListOfSpeakers_reference",
default_value=1, default_value=1,

View File

@ -0,0 +1,46 @@
# Generated by Django 2.1.5 on 2019-02-22 11:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0017_auto_20190219_2015")]
operations = [
migrations.AddField(
model_name="projector",
name="background_color",
field=models.CharField(default="#ffffff", max_length=7),
),
migrations.AddField(
model_name="projector",
name="header_background_color",
field=models.CharField(default="#317796", max_length=7),
),
migrations.AddField(
model_name="projector",
name="header_font_color",
field=models.CharField(default="#f5f5f5", max_length=7),
),
migrations.AddField(
model_name="projector",
name="header_h1_color",
field=models.CharField(default="#317796", max_length=7),
),
migrations.AddField(
model_name="projector",
name="show_header_footer",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="projector",
name="show_logo",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="projector",
name="show_title",
field=models.BooleanField(default=True),
),
]

View File

@ -82,6 +82,14 @@ class Projector(RESTModelMixin, models.Model):
width = models.PositiveIntegerField(default=1024) width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768) height = models.PositiveIntegerField(default=768)
background_color = models.CharField(max_length=7, default="#ffffff")
header_background_color = models.CharField(max_length=7, default="#317796")
header_font_color = models.CharField(max_length=7, default="#f5f5f5")
header_h1_color = models.CharField(max_length=7, default="#317796")
show_header_footer = models.BooleanField(default=True)
show_title = models.BooleanField(default=True)
show_logo = models.BooleanField(default=True)
name = models.CharField(max_length=255, unique=True, blank=True) name = models.CharField(max_length=255, unique=True, blank=True)
reference_projector = models.ForeignKey( reference_projector = models.ForeignKey(

View File

@ -99,6 +99,13 @@ class ProjectorSerializer(ModelSerializer):
"height", "height",
"reference_projector", "reference_projector",
"projectiondefaults", "projectiondefaults",
"background_color",
"header_background_color",
"header_font_color",
"header_h1_color",
"show_header_footer",
"show_title",
"show_logo",
) )
read_only_fields = ("scale", "scroll") read_only_fields = ("scale", "scroll")