Merge pull request #4227 from FinnStutzenstein/clos

Projector reference for CLOS
This commit is contained in:
Finn Stutzenstein 2019-02-01 10:10:47 +01:00 committed by GitHub
commit 44851af172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 177 additions and 40 deletions

View File

@ -56,6 +56,7 @@ export class Projector extends BaseModel<Projector> {
public name: string; public name: string;
public width: number; public width: number;
public height: number; public height: number;
public reference_projector_id: number;
public projectiondefaults: ProjectionDefault[]; public projectiondefaults: ProjectionDefault[];
public constructor(input?: any) { public constructor(input?: any) {

View File

@ -1,4 +1,4 @@
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { OpenSlidesComponent } from '../../openslides.component'; import { OpenSlidesComponent } from '../../openslides.component';
import { BaseViewModel } from './base-view-model'; import { BaseViewModel } from './base-view-model';
@ -24,6 +24,11 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
*/ */
protected readonly viewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>([]); protected readonly viewModelListSubject: BehaviorSubject<V[]> = new BehaviorSubject<V[]>([]);
/**
* Observable subject for any changes of view models.
*/
protected readonly generalViewModelSubject: Subject<V> = new Subject<V>();
/** /**
* Construction routine for the base repository * Construction routine for the base repository
* *
@ -149,6 +154,13 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
return this.viewModelListSubject.asObservable().pipe(auditTime(1)); return this.viewModelListSubject.asObservable().pipe(auditTime(1));
} }
/**
* This observable fires every time an object is changed in the repository.
*/
public getGeneralViewModelObservable(): Observable<V> {
return this.generalViewModelSubject.asObservable();
}
/** /**
* Updates the ViewModel observable using a ViewModel corresponding to the id * Updates the ViewModel observable using a ViewModel corresponding to the id
*/ */
@ -156,6 +168,7 @@ export abstract class BaseRepository<V extends BaseViewModel, M extends BaseMode
if (this.viewModelSubjects[id]) { if (this.viewModelSubjects[id]) {
this.viewModelSubjects[id].next(this.viewModelStore[id]); this.viewModelSubjects[id].next(this.viewModelStore[id]);
} }
this.generalViewModelSubject.next(this.viewModelStore[id]);
} }
/** /**

View File

@ -73,6 +73,20 @@
<span translate>Required</span> <span translate>Required</span>
</mat-hint> </mat-hint>
</mat-form-field> </mat-form-field>
<!-- Reference projector for the current list of speakers -->
<h3 translate>Current list fo speakers reference</h3>
<mat-form-field>
<mat-select formControlName="reference_projector_id" placeholder="{{ 'Reference projector' | translate }}">
<mat-option [value]="projector.id">
<span translate>self</span>
</mat-option>
<mat-option *ngFor="let refProjector of getReferenceProjectorsFor(projector)" [value]="refProjector.id">
<span>{{ refProjector.getTitle() | translate }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<h3 translate>Resolution and size</h3> <h3 translate>Resolution and size</h3>
<!-- Aspect ratio field --> <!-- Aspect ratio field -->
<mat-radio-group formControlName="aspectRatio" [name]="projector.id"> <mat-radio-group formControlName="aspectRatio" [name]="projector.id">
@ -82,6 +96,8 @@
</mat-radio-group> </mat-radio-group>
<mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider> <mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider>
{{ updateForm.value.width }} {{ updateForm.value.width }}
<!-- Clock -->
<div> <div>
<mat-checkbox formControlName="clock"> <mat-checkbox formControlName="clock">
<span translate>Show clock</span> <span translate>Show clock</span>

View File

@ -91,7 +91,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
name: ['', Validators.required], name: ['', Validators.required],
aspectRatio: ['', Validators.required], aspectRatio: ['', Validators.required],
width: [0, Validators.required], width: [0, Validators.required],
clock: [true] clock: [true],
reference_projector_id: []
}); });
} }
@ -184,11 +185,16 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
} }
this.editId = projector.id; this.editId = projector.id;
this.updateForm.reset(); this.updateForm.reset();
const reference_projector_id = projector.reference_projector_id
? projector.reference_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
}); });
} }
@ -215,7 +221,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
const updateProjector: Partial<Projector> = { 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
}; };
try { try {
await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock); await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock);
@ -237,4 +244,14 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
this.repo.delete(projector).then(null, this.raiseError); this.repo.delete(projector).then(null, this.raiseError);
} }
} }
/**
* Get all available reference projectors for the given projector. These
* projectors are all existing projectors exluding the given projector
*
* @returns all available reference projectors
*/
public getReferenceProjectorsFor(projector: ViewProjector): ViewProjector[] {
return this.repo.getViewModelList().filter(p => p.id !== projector.id);
}
} }

View File

@ -48,6 +48,10 @@ export class ViewProjector extends BaseViewModel {
return this.projector ? this.projector.scroll : null; return this.projector ? this.projector.scroll : null;
} }
public get reference_projector_id(): number {
return this.projector ? this.projector.reference_projector_id : null;
}
public constructor(projector?: Projector) { public constructor(projector?: Projector) {
super(); super();
this._projector = projector; this._projector = projector;

View File

@ -2,6 +2,10 @@ import { Injectable } from '@angular/core';
import { ProjectorService } from 'app/core/services/projector.service'; import { ProjectorService } from 'app/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 './projector-repository.service';
import { BehaviorSubject, Observable } from 'rxjs';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { AgendaBaseModel } from 'app/shared/models/base/agenda-base-model';
/** /**
*/ */
@ -9,8 +13,28 @@ import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
providedIn: 'root' providedIn: 'root'
}) })
export class CurrentListOfSpeakersSlideService { export class CurrentListOfSpeakersSlideService {
public constructor(private projectorService: ProjectorService) {} private currentItemIds: { [projectorId: number]: BehaviorSubject<number | null> } = {};
public constructor(
private projectorService: ProjectorService,
private projectorRepo: ProjectorRepositoryService,
private slideManager: SlideManager
) {
this.projectorRepo.getGeneralViewModelObservable().subscribe(projector => {
const itemId = this.getCurrentAgendaItemIdForProjector(projector);
if (this.currentItemIds[projector.id]) {
this.currentItemIds[projector.id].next(itemId);
}
});
}
/**
* Returns the basic projector element for the CLOS slide. If overlay=True, the projector element
* will be the overlay instead of the slide.
*
* @param overlay Wether to have a slide or overlay
* @returns the identifiable CLOS projector element.
*/
private getCurrentListOfSpeakersProjectorElement(overlay: boolean): IdentifiableProjectorElement { private getCurrentListOfSpeakersProjectorElement(overlay: boolean): IdentifiableProjectorElement {
return { return {
name: overlay ? 'agenda/current-list-of-speakers-overlay' : 'agenda/current-list-of-speakers', name: overlay ? 'agenda/current-list-of-speakers-overlay' : 'agenda/current-list-of-speakers',
@ -19,6 +43,51 @@ 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 getAgendaItemIdObservable(projector: ViewProjector): Observable<number | null> {
if (!this.currentItemIds[projector.id]) {
const itemId = this.getCurrentAgendaItemIdForProjector(projector);
this.currentItemIds[projector.id] = new BehaviorSubject<number | null>(itemId);
}
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): number | 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 model = this.projectorService.getModelFromProjectorElement(nonStableElement);
if (model instanceof AgendaBaseModel) {
// TODO: Use repositories associated to models
return (<any>model).agenda_item_id;
}
} catch (e) {
// make TypeScript silent.
}
}
return null;
}
/**
* Queries, if the slide/overlay is projected on the given projector.
*
* @param projector The projector
* @param overlay True, if we query for an overlay instead of the slide
* @returns if the slide/overlay is projected on the projector
*/
public isProjectedOn(projector: ViewProjector, overlay: boolean): boolean { public isProjectedOn(projector: ViewProjector, overlay: boolean): boolean {
return this.projectorService.isProjectedOn( return this.projectorService.isProjectedOn(
this.getCurrentListOfSpeakersProjectorElement(overlay), this.getCurrentListOfSpeakersProjectorElement(overlay),
@ -26,6 +95,12 @@ export class CurrentListOfSpeakersSlideService {
); );
} }
/**
* Toggle the projection state of the slide/overlay on the given projector
*
* @param projector The projector
* @param overlay Slide or overlay
*/
public async toggleOn(projector: ViewProjector, overlay: boolean): Promise<void> { public async toggleOn(projector: ViewProjector, overlay: boolean): Promise<void> {
const isClosProjected = this.isProjectedOn(projector, overlay); const isClosProjected = this.isProjectedOn(projector, overlay);
if (isClosProjected) { if (isClosProjected) {

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { HttpService } from 'app/core/services/http.service';
import { AgendaCurrentListOfSpeakersSlideData } from '../base/agenda-current-list-of-speakers-slide-data'; import { AgendaCurrentListOfSpeakersSlideData } from '../base/agenda-current-list-of-speakers-slide-data';
@Component({ @Component({
@ -9,15 +8,10 @@ import { AgendaCurrentListOfSpeakersSlideData } from '../base/agenda-current-lis
templateUrl: './agenda-current-list-of-speakers-overlay-slide.component.html', templateUrl: './agenda-current-list-of-speakers-overlay-slide.component.html',
styleUrls: ['./agenda-current-list-of-speakers-overlay-slide.component.scss'] styleUrls: ['./agenda-current-list-of-speakers-overlay-slide.component.scss']
}) })
export class AgendaCurrentListOfSpeakersOverlaySlideComponent export class AgendaCurrentListOfSpeakersOverlaySlideComponent extends BaseSlideComponent<
extends BaseSlideComponent<AgendaCurrentListOfSpeakersSlideData> AgendaCurrentListOfSpeakersSlideData
implements OnInit { > {
public constructor(private http: HttpService) { public constructor() {
super(); super();
console.log(this.http);
}
public ngOnInit(): void {
console.log('Hello from current list of speakers overlay');
} }
} }

View File

@ -14,7 +14,5 @@ export class AgendaCurrentListOfSpeakersSlideComponent extends BaseSlideComponen
super(); super();
} }
public ngOnInit(): void { public ngOnInit(): void {}
console.log('Hello from current list of speakers slide');
}
} }

View File

@ -1,20 +1,14 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { CoreCountdownSlideData } from './core-countdown-slide-data'; import { CoreCountdownSlideData } from './core-countdown-slide-data';
import { HttpService } from 'app/core/services/http.service';
@Component({ @Component({
selector: 'os-core-countdown-slide', selector: 'os-core-countdown-slide',
templateUrl: './core-countdown-slide.component.html', templateUrl: './core-countdown-slide.component.html',
styleUrls: ['./core-countdown-slide.component.scss'] styleUrls: ['./core-countdown-slide.component.scss']
}) })
export class CoreCountdownSlideComponent extends BaseSlideComponent<CoreCountdownSlideData> implements OnInit { export class CoreCountdownSlideComponent extends BaseSlideComponent<CoreCountdownSlideData> {
public constructor(private http: HttpService) { public constructor() {
super(); super();
console.log(this.http);
}
public ngOnInit(): void {
console.log('Hello from countdown slide');
} }
} }

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { MotionsMotionSlideData } from './motions-motion-slide-data'; import { MotionsMotionSlideData } from './motions-motion-slide-data';
@ -7,12 +7,8 @@ import { MotionsMotionSlideData } from './motions-motion-slide-data';
templateUrl: './motions-motion-slide.component.html', templateUrl: './motions-motion-slide.component.html',
styleUrls: ['./motions-motion-slide.component.scss'] styleUrls: ['./motions-motion-slide.component.scss']
}) })
export class MotionsMotionSlideComponent extends BaseSlideComponent<MotionsMotionSlideData> implements OnInit { export class MotionsMotionSlideComponent extends BaseSlideComponent<MotionsMotionSlideData> {
public constructor() { public constructor() {
super(); super();
} }
public ngOnInit(): void {
console.log('Hello from motion slide');
}
} }

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component'; import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { UsersUserSlideData } from './users-user-slide-data'; import { UsersUserSlideData } from './users-user-slide-data';
@ -7,12 +7,8 @@ import { UsersUserSlideData } from './users-user-slide-data';
templateUrl: './users-user-slide.component.html', templateUrl: './users-user-slide.component.html',
styleUrls: ['./users-user-slide.component.scss'] styleUrls: ['./users-user-slide.component.scss']
}) })
export class UsersUserSlideComponent extends BaseSlideComponent<UsersUserSlideData> implements OnInit { export class UsersUserSlideComponent extends BaseSlideComponent<UsersUserSlideData> {
public constructor() { public constructor() {
super(); super();
} }
public ngOnInit(): void {
console.log('Hello from user slide');
}
} }

View File

@ -0,0 +1,24 @@
# Generated by Django 2.1.5 on 2019-01-31 10:24
from django.db import migrations, models
import openslides.utils.models
class Migration(migrations.Migration):
dependencies = [("core", "0015_auto_20190122_1216")]
operations = [
migrations.AddField(
model_name="projector",
name="reference_projector",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=openslides.utils.models.SET_NULL_AND_AUTOUPDATE,
related_name="references",
to="core.Projector",
),
)
]

View File

@ -84,6 +84,14 @@ class Projector(RESTModelMixin, models.Model):
name = models.CharField(max_length=255, unique=True, blank=True) name = models.CharField(max_length=255, unique=True, blank=True)
reference_projector = models.ForeignKey(
"self",
on_delete=SET_NULL_AND_AUTOUPDATE,
null=True,
blank=True,
related_name="references",
)
class Meta: class Meta:
""" """
Contains general permissions that can not be placed in a specific app. Contains general permissions that can not be placed in a specific app.

View File

@ -97,6 +97,7 @@ class ProjectorSerializer(ModelSerializer):
"name", "name",
"width", "width",
"height", "height",
"reference_projector",
"projectiondefaults", "projectiondefaults",
) )
read_only_fields = ("scale", "scroll") read_only_fields = ("scale", "scroll")