Projector improvements

- Moved projector configs to group "General"
- Removed unused projector config related to CLOS
- debounce projector data requests for many projectors
- add (foreground) color
- custom, modifiable CSS classes per projector to cascade dynamic styles into slides
This commit is contained in:
FinnStutzenstein 2019-07-10 12:57:48 +02:00
parent 8cb9892426
commit 23c704b5da
12 changed files with 279 additions and 144 deletions

View File

@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { Observable, BehaviorSubject, Subject } from 'rxjs';
import { WebsocketService } from 'app/core/core-services/websocket.service';
import { ProjectorElement, Projector } from 'app/shared/models/core/projector';
import { auditTime } from 'rxjs/operators';
export interface SlideData<T = { error?: string }, P extends ProjectorElement = ProjectorElement> {
data: T;
@ -36,12 +37,19 @@ export class ProjectorDataService {
*/
private currentProjectorData: { [id: number]: BehaviorSubject<ProjectorData | null> } = {};
/**
* When multiple projectory are requested, debounce these requests to just issue
* one request, with all the needed projectors.
*/
private readonly updateProjectorDataDebounceSubject = new Subject<void>();
/**
* Constructor.
*
* @param websocketService
*/
public constructor(private websocketService: WebsocketService) {
// Dispatch projector data.
this.websocketService.getOberservable('projector').subscribe((update: AllProjectorData) => {
Object.keys(update).forEach(_id => {
const id = parseInt(_id, 10);
@ -51,7 +59,16 @@ export class ProjectorDataService {
});
});
// The service need to re-register, if the websocket connection was lost.
this.websocketService.generalConnectEvent.subscribe(() => this.updateProjectorDataSubscription());
// With a bit of debounce, update the needed projectors.
this.updateProjectorDataDebounceSubject.pipe(auditTime(10)).subscribe(() => {
const allActiveProjectorIds = Object.keys(this.openProjectorInstances)
.map(id => parseInt(id, 10))
.filter(id => this.openProjectorInstances[id] > 0);
this.websocketService.send('listenToProjectors', { projector_ids: allActiveProjectorIds });
});
}
/**
@ -94,13 +111,10 @@ export class ProjectorDataService {
}
/**
* Gets initial data and keeps reuesting data.
* Requests to update the data subscription to the server.
*/
private updateProjectorDataSubscription(): void {
const allActiveProjectorIds = Object.keys(this.openProjectorInstances)
.map(id => parseInt(id, 10))
.filter(id => this.openProjectorInstances[id] > 0);
this.websocketService.send('listenToProjectors', { projector_ids: allActiveProjectorIds });
this.updateProjectorDataDebounceSubject.next();
}
/**

View File

@ -1,11 +1,11 @@
<div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container>
<div id="projector" [ngStyle]="projectorStyle">
<div id="container" class="projector-container" [osResized]="resizeSubject" #container>
<div id="projector" class="projector">
<div id="offline-indicator" *ngIf="isOffline()">
<mat-icon>
fiber_manual_record
</mat-icon>
</div>
<div id="header" [ngStyle]="headerFooterStyle" *ngIf="projector && projector.show_header_footer">
<div id="header" class="headerFooter" *ngIf="projector && projector.show_header_footer">
<!-- projector logo -->
<img *ngIf="projector.show_logo && projectorLogo" src="{{ projectorLogo }}" class="projector-logo-main" />
@ -29,7 +29,7 @@
></os-slide-container>
</div>
<div id="footer" [ngStyle]="headerFooterStyle" *ngIf="projector && projector.show_header_footer">
<div id="footer" class="headerFooter" *ngIf="projector && projector.show_header_footer">
<div class="footertext">
<span *ngIf="eventDate"> {{ eventDate }} </span>
<span *ngIf="eventDate && eventLocation"> | </span>

View File

@ -1,9 +1,3 @@
// should be better using @host binding, but does not work
// https://github.com/angular/angular/issues/9343
:host-context(.os-slide-container) {
--header-h1-color: #000;
}
#container {
background-color: lightgoldenrodyellow;
position: relative;
@ -31,11 +25,6 @@
font-size: 22px !important;
line-height: 24px !important;
// shadow pierce all children of projector to overwrite the h1 color
::ng-deep h1 {
color: var(--header-h1-color);
}
#header {
position: absolute;
top: 0;

View File

@ -53,12 +53,12 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
) {
this.updateScaling();
}
this.projectorStyle['background-color'] = projector.background_color;
this.headerFooterStyle.color = projector.header_font_color;
this.headerFooterStyle['background-color'] = projector.header_background_color;
// alter the local scope css variable with the header_h1_color
document.documentElement.style.setProperty('--header-h1-color', this.projector.header_h1_color);
this.css.projector.color = projector.color;
this.css.projector.backgroundColor = projector.background_color;
this.css.projector.H1Color = this.projector.header_h1_color;
this.css.headerFooter.color = projector.header_font_color;
this.css.headerFooter.backgroundColor = projector.header_background_color;
this.updateCSS();
}
}
@ -85,33 +85,54 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
private containerElement: ElementRef;
/**
* Dynamic style attributes for the projector.
* The css class assigned to this projector.
*/
public projectorStyle: {
transform?: string;
private projectorClass: string;
/**
* The styleelement for setting projector-specific styles.
*/
private styleElement?: HTMLStyleElement;
/**
* All current css rules for the projector. when updating this, call `updateCSS()` afterwards.
*/
public css: {
container: {
height: string;
};
projector: {
transform: string;
width: string;
height: string;
'background-color': string;
color: string;
backgroundColor: string;
H1Color: string;
};
headerFooter: {
color: string;
backgroundColor: string;
backgroundImage: string;
};
} = {
container: {
height: '0px'
},
projector: {
transform: 'none',
width: '0px',
height: '0px',
'background-color': 'white'
color: 'black',
backgroundColor: 'white',
H1Color: '#317796'
},
headerFooter: {
color: 'white',
backgroundColor: '#317796',
backgroundImage: 'none'
}
};
/**
* Dynamic style attributes for the header and footer.
*/
public headerFooterStyle: { 'background-image': string; 'background-color': string; color: string } = {
'background-image': 'none',
'background-color': 'blue',
color: 'white'
};
/**
* Dynamic style attributes for the container.
*/
public containerStyle: { height?: string } = {};
/**
* All slides to show on this projector
*/
@ -161,9 +182,21 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
private projectorDataService: ProjectorDataService,
private projectorRepository: ProjectorRepositoryService,
private configService: ConfigService,
private offlineService: OfflineService
private offlineService: OfflineService,
private elementRef: ElementRef
) {
super(titleService, translate);
this.projectorClass =
'projector-' +
Math.random()
.toString(36)
.substring(4);
this.elementRef.nativeElement.classList.add(this.projectorClass);
this.styleElement = document.createElement('style');
this.styleElement.appendChild(document.createTextNode('')); // Hack for WebKit to trigger update
document.head.appendChild(this.styleElement);
// projector logo / background-image
this.configService.get<{ path?: string }>('logo_projector_main').subscribe(val => {
if (val && val.path) {
@ -174,10 +207,11 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
});
this.configService.get<{ path?: string }>('logo_projector_header').subscribe(val => {
if (val && val.path) {
this.headerFooterStyle['background-image'] = "url('" + val.path + "')";
this.css.headerFooter.backgroundImage = "url('" + val.path + "')";
} else {
this.headerFooterStyle['background-image'] = 'none';
this.css.headerFooter.backgroundImage = 'none';
}
this.updateCSS();
});
// event data
@ -218,10 +252,39 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
if (isNaN(scale)) {
return;
}
this.projectorStyle.transform = 'scale(' + scale + ')';
this.projectorStyle.width = this.currentProjectorSize.width + 'px';
this.projectorStyle.height = this.currentProjectorSize.height + 'px';
this.containerStyle.height = Math.round(scale * this.currentProjectorSize.height) + 'px';
this.css.projector.transform = 'scale(' + scale + ')';
this.css.projector.width = this.currentProjectorSize.width + 'px';
this.css.projector.height = this.currentProjectorSize.height + 'px';
this.css.container.height = Math.round(scale * this.currentProjectorSize.height) + 'px';
this.updateCSS();
}
/**
* Update the css element with the current css settings in `this.css`.
*/
private updateCSS(): void {
if (!this.styleElement) {
return;
}
this.styleElement.innerHTML = `
.${this.projectorClass} .projector-container {
height: ${this.css.container.height};
}
.${this.projectorClass} .projector {
transform: ${this.css.projector.transform};
width: ${this.css.projector.width};
height: ${this.css.projector.height};
background-color: ${this.css.projector.backgroundColor};
color: ${this.css.projector.color};
}
.${this.projectorClass} .projector h1 {
color: ${this.css.projector.H1Color};
}
.${this.projectorClass} .headerFooter {
color: ${this.css.headerFooter.color};
background-color: ${this.css.headerFooter.backgroundColor};
background-image: ${this.css.headerFooter.backgroundImage};
}`;
}
/**
@ -262,5 +325,7 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
if (this.projectorId > 0) {
this.projectorDataService.projectorClosed(this.projectorId);
}
document.head.removeChild(this.styleElement);
this.styleElement = null;
}
}

View File

@ -68,6 +68,7 @@ export class Projector extends BaseModel<Projector> {
public height: number;
public reference_projector_id: number;
public projectiondefaults_id: number[];
public color: string;
public background_color: string;
public header_background_color: string;
public header_font_color: string;

View File

@ -35,14 +35,14 @@
<h3 translate>Resolution and size</h3>
<!-- Aspect ratio field -->
<mat-radio-group formControlName="aspectRatio" [name]="projector.id">
<mat-radio-button *ngFor="let ratio of aspectRatiosKeys" [value]="ratio">
<mat-radio-button *ngFor="let ratio of aspectRatiosKeys" [value]="ratio" (change)="aspectRatioChanged($event)">
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
<div class="spacer-top-20">
<mat-slider
[thumbLabel]="true"
min="800"
[min]="getMinWidth()"
max="2000"
step="10"
value="{{ updateForm.value.width }}"
@ -60,6 +60,23 @@
</mat-select>
<!-- colors -->
<div class="color-field-wrapper">
<div class="form">
<mat-form-field>
<span translate>Foreground color</span>
<input matInput formControlName="color" type="color" />
<mat-hint *ngIf="!updateForm.controls.color.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</div>
<div class="reset-button">
<button mat-icon-button matTooltip="{{ 'Reset' | translate }}" (click)="resetField('color', '#000000')">
<mat-icon>replay</mat-icon>
</button>
</div>
</div>
<div class="color-field-wrapper">
<div class="form">
<mat-form-field>

View File

@ -15,6 +15,7 @@ import { ClockSlideService } from '../../services/clock-slide.service';
import { OperatorService } from 'app/core/core-services/operator.service';
import { ViewProjectionDefault } from '../../models/view-projection-default';
import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projector/projection-default-repository.service';
import { MatRadioChange } from '@angular/material';
/**
* All supported aspect rations for projectors.
@ -22,9 +23,12 @@ import { ProjectionDefaultRepositoryService } from 'app/core/repositories/projec
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10
'16:10': 16 / 10,
'30:9': 30 / 9
};
const aspectRatio_30_9_MinWidth = 1150;
/**
* List for all projectors.
*/
@ -59,7 +63,16 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
* The projector shown by this entry.
*/
@Input()
public projector: ViewProjector;
public set projector(value: ViewProjector) {
this._projector = value;
this.updateForm.patchValue({ width: value.width });
}
public get projector(): ViewProjector {
return this._projector;
}
private _projector: ViewProjector;
/**
* Helper to check manage permissions
@ -103,6 +116,7 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
width: [0, Validators.required],
projectiondefaults_id: [[]],
clock: [true],
color: ['', Validators.required],
background_color: ['', Validators.required],
header_background_color: ['', Validators.required],
header_font_color: ['', Validators.required],
@ -206,6 +220,24 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
}
}
public aspectRatioChanged(event: MatRadioChange): void {
let width: number;
if (event.value === '30:9' && this.updateForm.value.width < aspectRatio_30_9_MinWidth) {
width = aspectRatio_30_9_MinWidth;
} else {
width = this.updateForm.value.width;
}
this.updateProjectorDimensions(width, event.value);
}
public getMinWidth(): number {
if (this.updateForm.value.aspectRatio === '30:9') {
return aspectRatio_30_9_MinWidth;
} else {
return 800;
}
}
/**
* Delete the projector.
*/
@ -222,11 +254,14 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
* @param event The slider value
*/
public widthSliderValueChanged(event: MatSliderChange): void {
const aspectRatio = this.getAspectRatioKey();
this.updateProjectorDimensions(event.value, this.updateForm.value.aspectRatio);
}
private updateProjectorDimensions(width: number, aspectRatioKey: string): void {
const updateProjector: Partial<Projector> = {
width: event.value
width: width
};
updateProjector.height = Math.round(event.value / aspectRatios[aspectRatio]);
updateProjector.height = Math.round(width / aspectRatios[aspectRatioKey]);
this.repo.update(updateProjector, this.projector).then(null, this.raiseError);
}

View File

@ -66,6 +66,10 @@ export class ViewProjector extends BaseViewModel<Projector> {
return this.projector.reference_projector_id;
}
public get color(): string {
return this.projector.color;
}
public get background_color(): string {
return this.projector.background_color;
}

View File

@ -131,75 +131,6 @@ def get_config_variables():
subgroup="System",
)
# General export settings
yield ConfigVariable(
name="general_csv_separator",
default_value=",",
label="Separator used for all csv exports and examples",
weight=142,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_csv_encoding",
default_value="utf-8",
input_type="choice",
label="Default encoding for all csv exports",
choices=(
{"value": "utf-8", "display_name": "UTF-8"},
{"value": "iso-8859-15", "display_name": "ISO-8859-15"},
),
weight=143,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_export_pdf_pagenumber_alignment",
default_value="center",
input_type="choice",
label="Page number alignment in PDF",
choices=(
{"value": "left", "display_name": "Left"},
{"value": "center", "display_name": "Center"},
{"value": "right", "display_name": "Right"},
),
weight=144,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_export_pdf_fontsize",
default_value="10",
input_type="choice",
label="Standard font size in PDF",
choices=(
{"value": "10", "display_name": "10"},
{"value": "11", "display_name": "11"},
{"value": "12", "display_name": "12"},
),
weight=146,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_export_pdf_pagesize",
default_value="A4",
input_type="choice",
label="Standard page size in PDF",
choices=(
{"value": "A4", "display_name": "DIN A4"},
{"value": "A5", "display_name": "DIN A5"},
),
weight=147,
group="General",
subgroup="Export",
)
# Projector
yield ConfigVariable(
@ -218,7 +149,8 @@ def get_config_variables():
{"value": "ru", "display_name": "русский"},
),
weight=150,
group="Projector",
group="General",
subgroup="Projector",
)
yield ConfigVariable(
@ -226,21 +158,81 @@ def get_config_variables():
default_value=60,
input_type="integer",
label="Predefined seconds of new countdowns",
weight=185,
group="Projector",
weight=152,
group="General",
subgroup="Projector",
)
# General export settings
yield ConfigVariable(
name="general_csv_separator",
default_value=",",
label="Separator used for all csv exports and examples",
weight=160,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="projector_currentListOfSpeakers_reference",
default_value=1,
input_type="integer",
label="Projector reference for list of speakers",
weight=201,
group="Projector",
hidden=True,
name="general_csv_encoding",
default_value="utf-8",
input_type="choice",
label="Default encoding for all csv exports",
choices=(
{"value": "utf-8", "display_name": "UTF-8"},
{"value": "iso-8859-15", "display_name": "ISO-8859-15"},
),
weight=162,
group="General",
subgroup="Export",
)
# Logos.
yield ConfigVariable(
name="general_export_pdf_pagenumber_alignment",
default_value="center",
input_type="choice",
label="Page number alignment in PDF",
choices=(
{"value": "left", "display_name": "Left"},
{"value": "center", "display_name": "Center"},
{"value": "right", "display_name": "Right"},
),
weight=164,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_export_pdf_fontsize",
default_value="10",
input_type="choice",
label="Standard font size in PDF",
choices=(
{"value": "10", "display_name": "10"},
{"value": "11", "display_name": "11"},
{"value": "12", "display_name": "12"},
),
weight=166,
group="General",
subgroup="Export",
)
yield ConfigVariable(
name="general_export_pdf_pagesize",
default_value="A4",
input_type="choice",
label="Standard page size in PDF",
choices=(
{"value": "A4", "display_name": "DIN A4"},
{"value": "A5", "display_name": "DIN A5"},
),
weight=168,
group="General",
subgroup="Export",
)
# Logos
yield ConfigVariable(
name="logos_available",
default_value=[

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.3 on 2019-07-10 10:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("core", "0024_auto_20190605_1105")]
operations = [
migrations.AddField(
model_name="projector",
name="color",
field=models.CharField(default="#000000", max_length=7),
)
]

View File

@ -77,6 +77,7 @@ class Projector(RESTModelMixin, models.Model):
width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768)
color = models.CharField(max_length=7, default="#000000")
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")

View File

@ -106,6 +106,7 @@ class ProjectorSerializer(ModelSerializer):
"height",
"reference_projector",
"projectiondefaults",
"color",
"background_color",
"header_background_color",
"header_font_color",