more work on projector, countdowns, clos

- splitted clos-slide and clos-overlay.
- Synchronize to server, more little changes
This commit is contained in:
FinnStutzenstein 2019-01-24 16:25:50 +01:00 committed by Emanuel Schütze
parent c52b82e77d
commit 965d23be50
107 changed files with 2102 additions and 343 deletions

View File

@ -1,15 +1,23 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { E2EImportsModule } from './../e2e-imports.module';
import { ServertimeService } from './core/services/servertime.service';
describe('AppComponent', () => {
let servertimeService;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
servertimeService = TestBed.get(ServertimeService);
spyOn(servertimeService, 'startScheduler').and.stub();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
expect(servertimeService.startScheduler).toHaveBeenCalled();
}));
});

View File

@ -4,6 +4,7 @@ import { OperatorService } from './core/services/operator.service';
import { LoginDataService } from './core/services/login-data.service';
import { ConfigService } from './core/services/config.service';
import { ConstantsService } from './core/services/constants.service';
import { ServertimeService } from './core/services/servertime.service';
/**
* Angular's global App Component
@ -30,7 +31,8 @@ export class AppComponent {
operator: OperatorService,
configService: ConfigService,
loginDataService: LoginDataService,
constantsService: ConstantsService // Needs to be started, so it can register itself to the WebsocketService
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
servertimeService: ServertimeService
) {
// manually add the supported languages
translate.addLangs(['en', 'de', 'cs']);
@ -42,6 +44,8 @@ export class AppComponent {
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
// change default JS functions
this.overloadArrayToString();
servertimeService.startScheduler();
}
/**

View File

@ -76,8 +76,6 @@ export class AutoupdateService extends OpenSlidesComponent {
* Handles the change ids of all autoupdates.
*/
private async storeResponse(autoupdate: AutoupdateFormat): Promise<void> {
console.log('got autoupdate', autoupdate);
if (autoupdate.all_data) {
await this.storeAllData(autoupdate);
} else {

View File

@ -407,7 +407,6 @@ export class DataStoreService {
* @param changeId The changeId from the update. If it's the highest change id seen, it will be set into the cache.
*/
public async flushToStorage(changeId: number): Promise<void> {
console.log('flush to storage');
this._maxChangeId = changeId;
await this.storageService.set(DataStoreService.cachePrefix + 'DS', this.jsonStore);
await this.storageService.set(DataStoreService.cachePrefix + 'maxChangeId', changeId);

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { OpenSlidesComponent } from 'app/openslides.component';
import { Projectable } from 'app/site/base/projectable';
import { Projectable, ProjectorElementBuildDeskriptor, isProjectable } from 'app/site/base/projectable';
import { MatDialog } from '@angular/material';
import {
ProjectionDialogComponent,
@ -32,19 +32,27 @@ export class ProjectionDialogService extends OpenSlidesComponent {
*
* @param obj The projectable.
*/
public async openProjectDialogFor(obj: Projectable): Promise<void> {
const dialogRef = this.dialog.open<ProjectionDialogComponent, Projectable, ProjectionDialogReturnType>(
public async openProjectDialogFor(obj: Projectable | ProjectorElementBuildDeskriptor): Promise<void> {
let descriptor: ProjectorElementBuildDeskriptor;
if (isProjectable(obj)) {
descriptor = obj.getSlide();
} else {
descriptor = obj;
}
const dialogRef = this.dialog.open<
ProjectionDialogComponent,
{
minWidth: '500px',
maxHeight: '90vh',
data: obj
}
);
ProjectorElementBuildDeskriptor,
ProjectionDialogReturnType
>(ProjectionDialogComponent, {
minWidth: '500px',
maxHeight: '90vh',
data: descriptor
});
const response = await dialogRef.afterClosed().toPromise();
if (response) {
const [projectors, projectorElement]: ProjectionDialogReturnType = response;
this.projectorService.projectOn(projectors, projectorElement);
this.projectorService.projectOnMultiple(projectors, projectorElement);
}
}
}

View File

@ -1,10 +1,22 @@
import { Injectable } from '@angular/core';
import { OpenSlidesComponent } from 'app/openslides.component';
import { Projectable } from 'app/site/base/projectable';
import {
Projectable,
ProjectorElementBuildDeskriptor,
isProjectable,
isProjectorElementBuildDeskriptor
} from 'app/site/base/projectable';
import { DataStoreService } from './data-store.service';
import { Projector, ProjectorElement } from 'app/shared/models/core/projector';
import { DataSendService } from './data-send.service';
import {
Projector,
ProjectorElement,
ProjectorElements,
IdentifiableProjectorElement
} from 'app/shared/models/core/projector';
import { HttpService } from './http.service';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { BaseModel } from 'app/shared/models/base/base-model';
/**
* This service cares about Projectables being projected and manage all projection-related
@ -22,7 +34,7 @@ export class ProjectorService extends OpenSlidesComponent {
* @param DS
* @param dataSend
*/
public constructor(private DS: DataStoreService, private dataSend: DataSendService) {
public constructor(private DS: DataStoreService, private http: HttpService, private slideManager: SlideManager) {
super();
}
@ -32,10 +44,16 @@ export class ProjectorService extends OpenSlidesComponent {
* @param obj The object in question
* @returns true, if the object is projected on one projector.
*/
public isProjected(obj: Projectable): boolean {
return this.DS.getAll<Projector>('core/projector').some(projector => {
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
});
public isProjected(obj: Projectable | ProjectorElementBuildDeskriptor): boolean {
if (isProjectable(obj)) {
return this.DS.getAll<Projector>('core/projector').some(projector => {
return projector.isElementShown(obj.getSlide().getBasicProjectorElement());
});
} else {
return this.DS.getAll<Projector>('core/projector').some(projector => {
return projector.isElementShown(obj.getBasicProjectorElement());
});
}
}
/**
@ -44,10 +62,28 @@ export class ProjectorService extends OpenSlidesComponent {
* @param obj The object in question
* @return All projectors, where this Object is projected on
*/
public getProjectorsWhichAreProjecting(obj: Projectable): Projector[] {
return this.DS.getAll<Projector>('core/projector').filter(projector => {
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
});
public getProjectorsWhichAreProjecting(obj: Projectable | ProjectorElementBuildDeskriptor): Projector[] {
if (isProjectable(obj)) {
return this.DS.getAll<Projector>('core/projector').filter(projector => {
return projector.isElementShown(obj.getSlide().getBasicProjectorElement());
});
} else {
return this.DS.getAll<Projector>('core/projector').filter(projector => {
return projector.isElementShown(obj.getBasicProjectorElement());
});
}
}
private getProjectorElement(
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
): IdentifiableProjectorElement {
if (isProjectable(obj)) {
return obj.getSlide().getBasicProjectorElement();
} else if (isProjectorElementBuildDeskriptor(obj)) {
return obj.getBasicProjectorElement();
} else {
return obj;
}
}
/**
@ -57,8 +93,11 @@ export class ProjectorService extends OpenSlidesComponent {
* @param projector The projector to test
* @returns true, if the object is projected on the projector.
*/
public isProjectedOn(obj: Projectable, projector: Projector): boolean {
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
public isProjectedOn(
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement,
projector: Projector
): boolean {
return projector.isElementShown(this.getProjectorElement(obj));
}
/**
@ -72,23 +111,86 @@ export class ProjectorService extends OpenSlidesComponent {
* @param projectors All projectors where to add the element.
* @param element The element in question.
*/
public projectOn<T extends ProjectorElement>(projectors: Projector[], element: T): void {
const changedProjectors: Projector[] = [];
public projectOnMultiple(projectors: Projector[], element: IdentifiableProjectorElement): void {
this.DS.getAll<Projector>('core/projector').forEach(projector => {
if (projectors.includes(projector)) {
projector.removeAllNonStableElements();
projector.addElement(element);
changedProjectors.push(projector);
} else if (projector.isElementShown(element.name, element.id)) {
projector.removeElementByNameAndId(element.name, element.id);
changedProjectors.push(projector);
this.projectOn(projector, element);
} else if (projector.isElementShown(element)) {
this.removeFrom(projector, element);
}
});
}
// TODO: Use new 'project' route.
changedProjectors.forEach(projector => {
this.dataSend.updateModel(projector);
});
public async projectOn(
projector: Projector,
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
): Promise<void> {
const element = this.getProjectorElement(obj);
if (element.stable) {
// Just add this stable element
projector.addElement(element);
await this.projectRequest(projector, projector.elements);
} else {
// For non-stable elements remove all other non-stable elements, add them to the history and
// add the one new element to the projector.
const removedElements = projector.removeAllNonStableElements();
let changed = removedElements.length > 0;
if (element) {
projector.addElement(element);
changed = true;
}
if (changed) {
await this.projectRequest(projector, projector.elements, null, removedElements);
}
}
}
public async removeFrom(
projector: Projector,
obj: Projectable | ProjectorElementBuildDeskriptor | IdentifiableProjectorElement
): Promise<void> {
const element = this.getProjectorElement(obj);
if (element.stable) {
// Just remove this stable element
projector.removeElements(element);
await this.projectRequest(projector, projector.elements);
} else {
// For non-stable elements remove all current non-stable elements and add them to the history
const removedElements = projector.removeElements(element);
if (removedElements.length > 0) {
console.log(projector.elements, removedElements);
await this.projectRequest(projector, projector.elements, null, removedElements);
}
}
}
private async projectRequest(
projector: Projector,
elements?: ProjectorElements,
preview?: ProjectorElements,
appendToHistory?: ProjectorElements,
deleteLastHistroyElement?: boolean
): Promise<void> {
const requestData: any = {};
if (elements) {
requestData.elements = elements;
}
if (preview) {
requestData.preview = preview;
}
if (appendToHistory && appendToHistory.length) {
requestData.append_to_history = appendToHistory;
}
if (deleteLastHistroyElement) {
requestData.delete_last_history_element = true;
}
if (appendToHistory && appendToHistory.length && deleteLastHistroyElement) {
throw new Error('You cannot append to the history and delete the last element at the same time');
}
await this.http.post(`/rest/core/projector/${projector.id}/project/`, requestData);
}
/**
@ -103,4 +205,60 @@ export class ProjectorService extends OpenSlidesComponent {
return projector.projectiondefaults.map(pd => pd.name).includes(projectiondefault);
});
}
public getModelFromProjectorElement<T extends BaseModel>(element: IdentifiableProjectorElement): T {
if (!this.slideManager.canSlideBeMappedToModel(element.name)) {
throw new Error('THis projectorelement cannot be mapped to a model');
}
const identifiers = element.getIdentifiers();
if (!identifiers.includes('name') || !identifiers.includes('name')) {
throw new Error('To map this element to a model, a name and id is needed.');
}
return this.DS.get<T>(element.name, element.id);
}
public async projectNextSlide(projector: Projector): Promise<void> {
await this.projectPreviewSlide(projector, 0);
}
public async projectPreviewSlide(projector: Projector, previewIndex: number): Promise<void> {
if (projector.elements_preview.length === 0 || previewIndex >= projector.elements_preview.length) {
return;
}
const removedElements = projector.removeAllNonStableElements();
projector.addElement(projector.elements_preview.splice(previewIndex, 1)[0]);
await this.projectRequest(projector, projector.elements, projector.elements_preview, removedElements);
}
public async projectPreviousSlide(projector: Projector): Promise<void> {
if (projector.elements_history.length === 0) {
return;
}
// Get the last element from the history
const lastElements: ProjectorElements = projector.elements_history[projector.elements_history.length - 1];
let lastElement: ProjectorElement = null;
if (lastElements.length > 0) {
lastElement = lastElements[0];
}
// Add all current elements to the preview.
const removedElements = projector.removeAllNonStableElements();
removedElements.forEach(e => projector.elements_preview.unshift(e));
// Add last element
if (lastElement) {
projector.addElement(lastElement);
}
await this.projectRequest(projector, projector.elements, projector.elements_preview, null, true);
}
public async savePreview(projector: Projector): Promise<void> {
await this.projectRequest(projector, null, projector.elements_preview);
}
public async addElementToPreview(projector: Projector, element: ProjectorElement): Promise<void> {
projector.elements_preview.push(element);
await this.projectRequest(projector, null, projector.elements_preview);
}
}

View File

@ -0,0 +1,17 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from '../../../e2e-imports.module';
import { ServertimeService } from './servertime.service';
describe('ServertimeService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [ServertimeService]
});
});
it('should be created', inject([ServertimeService], (service: ServertimeService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
import { OpenSlidesComponent } from 'app/openslides.component';
import { HttpService } from './http.service';
import { environment } from 'environments/environment.prod';
import { isNumber } from 'util';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ServertimeService extends OpenSlidesComponent {
private static FAILURE_TIMEOUT = 30;
private static NORMAL_TIMEOUT = 60 * 5;
/**
* In milliseconds
*/
private serverOffsetSubject = new BehaviorSubject<number>(0);
public constructor(private http: HttpService) {
super();
}
public startScheduler(): void {
this.scheduleNextRefresh(0);
}
public getServerOffsetObservable(): Observable<number> {
return this.serverOffsetSubject.asObservable();
}
private scheduleNextRefresh(seconds: number): void {
setTimeout(async () => {
let timeout = ServertimeService.NORMAL_TIMEOUT;
try {
await this.refreshServertime();
} catch (e) {
console.log(e);
timeout = ServertimeService.FAILURE_TIMEOUT;
}
this.scheduleNextRefresh(timeout);
}, 1000 * seconds);
}
private async refreshServertime(): Promise<void> {
// servertime is the time in seconds.
const servertime = await this.http.get<number>(environment.urlPrefix + '/core/servertime/');
if (!isNumber(servertime)) {
throw new Error('The returned servertime is not a number');
}
this.serverOffsetSubject.next(Math.floor(Date.now() - servertime * 1000));
}
public getServertime(): number {
return Date.now() - this.serverOffsetSubject.getValue();
}
}

View File

@ -6,7 +6,7 @@
<div>
<ng-container *ngTemplateOutlet="title"></ng-container>
</div>
<div>
<div *ngIf="showActionRow">
<ng-container *ngTemplateOutlet="actionRow"></ng-container>
</div>
</div>

View File

@ -20,6 +20,10 @@
.title-container {
display: flex;
justify-content: space-between;
::ng-deep button {
color: rgba(0, 0, 0, 0.54);
}
}
}
}

View File

@ -9,8 +9,7 @@ describe('MetaTextBlockComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MetaTextBlockComponent]
imports: [E2EImportsModule]
}).compileComponents();
}));

View File

@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core';
import { BaseComponent } from '../../../../base.component';
import { ViewportService } from '../../../../core/services/viewport.service';
import { BaseComponent } from '../../../base.component';
import { ViewportService } from '../../../core/services/viewport.service';
/**
* Component for the motion comments view
@ -15,9 +15,6 @@ export class MetaTextBlockComponent extends BaseComponent {
@Input()
public showActionRow: boolean;
@Input()
public icon: string;
public constructor(public vp: ViewportService) {
super();
}

View File

@ -1,4 +1,4 @@
<h2 mat-dialog-title>{{ projectable.getTitle() }}</h2>
<h2 mat-dialog-title>{{ projectorElementBuildDescriptor.getTitle() }}</h2>
<mat-dialog-content>
<mat-card>
<mat-card-title> <span translate>Projectors</span> </mat-card-title>

View File

@ -4,10 +4,6 @@ mat-dialog-content {
div.projectors {
padding: 15px;
&.projected {
background-color: lightblue;
}
.right {
float: right;
}

View File

@ -1,19 +1,19 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
import { Projectable } from 'app/site/base/projectable';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { DataStoreService } from 'app/core/services/data-store.service';
import { Projector, ProjectorElement } from 'app/shared/models/core/projector';
import { Projector, IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { ProjectorService } from 'app/core/services/projector.service';
import {
ProjectorOption,
isProjectorDecisionOption,
isProjectorChoiceOption,
ProjectorDecisionOption,
ProjectorChoiceOption,
ProjectorOptions
} from 'app/site/base/projector-options';
SlideOption,
isSlideDecisionOption,
isSlideChoiceOption,
SlideDecisionOption,
SlideChoiceOption,
SlideOptions
} from 'app/site/base/slide-options';
export type ProjectionDialogReturnType = [Projector[], ProjectorElement];
export type ProjectionDialogReturnType = [Projector[], IdentifiableProjectorElement];
/**
*/
@ -25,40 +25,40 @@ export type ProjectionDialogReturnType = [Projector[], ProjectorElement];
export class ProjectionDialogComponent {
public projectors: Projector[];
private selectedProjectors: Projector[] = [];
public projectorElement: ProjectorElement;
public options: ProjectorOptions;
public projectorElement: IdentifiableProjectorElement;
public options: SlideOptions;
public constructor(
public dialogRef: MatDialogRef<ProjectionDialogComponent, ProjectionDialogReturnType>,
@Inject(MAT_DIALOG_DATA) public projectable: Projectable,
@Inject(MAT_DIALOG_DATA) public projectorElementBuildDescriptor: ProjectorElementBuildDeskriptor,
private DS: DataStoreService,
private projectorService: ProjectorService
) {
this.projectors = this.DS.getAll<Projector>('core/projector');
// TODO: Maybe watch. But this may not be necessary for the short living time of this dialog.
this.selectedProjectors = this.projectorService.getProjectorsWhichAreProjecting(this.projectable);
this.selectedProjectors = this.projectorService.getProjectorsWhichAreProjecting(
this.projectorElementBuildDescriptor
);
// Add default projector, if the projectable is not projected on it.
const defaultProjector: Projector = this.projectorService.getProjectorForDefault(
this.projectable.getProjectionDefaultName()
);
if (!this.selectedProjectors.includes(defaultProjector)) {
this.selectedProjectors.push(defaultProjector);
if (this.projectorElementBuildDescriptor.projectionDefaultName) {
const defaultProjector: Projector = this.projectorService.getProjectorForDefault(
this.projectorElementBuildDescriptor.projectionDefaultName
);
if (!this.selectedProjectors.includes(defaultProjector)) {
this.selectedProjectors.push(defaultProjector);
}
}
this.projectorElement = {
id: this.projectable.getIdForSlide(),
name: this.projectable.getNameForSlide(),
stable: this.projectable.isStableSlide()
};
this.projectorElement = this.projectorElementBuildDescriptor.getBasicProjectorElement();
// Set option defaults
this.projectable.getProjectorOptions().forEach(option => {
this.projectorElementBuildDescriptor.slideOptions.forEach(option => {
this.projectorElement[option.key] = option.default;
});
this.options = this.projectable.getProjectorOptions();
this.options = this.projectorElementBuildDescriptor.slideOptions;
}
public toggleProjector(projector: Projector): void {
@ -75,15 +75,15 @@ export class ProjectionDialogComponent {
}
public isProjectedOn(projector: Projector): boolean {
return this.projectorService.isProjectedOn(this.projectable, projector);
return this.projectorService.isProjectedOn(this.projectorElementBuildDescriptor, projector);
}
public isDecisionOption(option: ProjectorOption): option is ProjectorDecisionOption {
return isProjectorDecisionOption(option);
public isDecisionOption(option: SlideOption): option is SlideDecisionOption {
return isSlideDecisionOption(option);
}
public isChoiceOption(option: ProjectorOption): option is ProjectorChoiceOption {
return isProjectorChoiceOption(option);
public isChoiceOption(option: SlideOption): option is SlideChoiceOption {
return isSlideChoiceOption(option);
}
public onOk(): void {

View File

@ -1,5 +1,5 @@
import { Component, OnInit, Input } from '@angular/core';
import { Projectable } from 'app/site/base/projectable';
import { Projectable, ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ProjectionDialogService } from 'app/core/services/projection-dialog.service';
/**
@ -11,7 +11,7 @@ import { ProjectionDialogService } from 'app/core/services/projection-dialog.ser
})
export class ProjectorButtonComponent implements OnInit {
@Input()
public object: Projectable;
public object: Projectable | ProjectorElementBuildDeskriptor;
/**
* The consotructor

View File

@ -5,6 +5,8 @@ import { BaseModel } from '../base/base-model';
* @ignore
*/
export class Countdown extends BaseModel<Countdown> {
public static COLLECTIONSTRING = 'core/countdown';
public id: number;
public description: string;
public default_time: number;
@ -12,7 +14,7 @@ export class Countdown extends BaseModel<Countdown> {
public running: boolean;
public constructor(input?: any) {
super('core/countdown', 'Countdown', input);
super(Countdown.COLLECTIONSTRING, 'Countdown', input);
}
public getTitle(): string {

View File

@ -5,11 +5,13 @@ import { BaseModel } from '../base/base-model';
* @ignore
*/
export class ProjectorMessage extends BaseModel<ProjectorMessage> {
public static COLLECTIONSTRING = 'core/projector-message';
public id: number;
public message: string;
public constructor(input?: any) {
super('core/projector-message', 'Message', input);
super(ProjectorMessage.COLLECTIONSTRING, 'Message', input);
}
public getTitle(): string {

View File

@ -23,6 +23,10 @@ export interface ProjectorElement {
[key: string]: any;
}
export interface IdentifiableProjectorElement extends ProjectorElement {
getIdentifiers(): (keyof IdentifiableProjectorElement)[];
}
/**
* Multiple elements.
*/
@ -45,6 +49,8 @@ export interface ProjectionDefault {
export class Projector extends BaseModel<Projector> {
public id: number;
public elements: ProjectorElements;
public elements_preview: ProjectorElements;
public elements_history: ProjectorElements[];
public scale: number;
public scroll: number;
public name: string;
@ -57,22 +63,30 @@ export class Projector extends BaseModel<Projector> {
}
/**
* Returns true, if there is an element with the given name (and optionally
* an id). If the id is given, the element to search MUST have this id.
* Must match all given identifiers. If a projectorelement does not have all keys
* to identify, it will be removed, if all existing keys match
*
* @param name The name of the element
* @param id The optional id to check.
* @returns true, if there is at least one element with the given name (and id).
* @returns true, TODO
*/
public isElementShown(name: string, id?: number): boolean {
return this.elements.some(element => element.name === name && (!id || element.id === id));
public isElementShown(element: IdentifiableProjectorElement): boolean {
return this.elements.some(elementOnProjector => {
return element.getIdentifiers().every(identifier => {
return !elementOnProjector[identifier] || elementOnProjector[identifier] === element[identifier];
});
});
}
/**
* Removes all elements, that do not have `stable=true`.
*
* TODO: use this.partitionArray
*
* @returns all removed unstable elements
*/
public removeAllNonStableElements(): void {
public removeAllNonStableElements(): ProjectorElements {
const unstableElements = this.elements.filter(element => !element.stable);
this.elements = this.elements.filter(element => element.stable);
return unstableElements;
}
/**
@ -85,17 +99,28 @@ export class Projector extends BaseModel<Projector> {
}
/**
* Removes elements given by the name and optional id. If no id is given
* all elements with a matching name are removed.
*
* If an id is given, ut the element dies not specify an id, it will be removed.
*
* @param name The name to search
* @param id The optional id to search.
* Must match everything. If a projectorelement does not have all keys
* to identify, it will be removed, if all existing keys match
*/
public removeElementByNameAndId(name: string, id?: number): void {
this.elements = this.elements.filter(
element => element.name !== name || (!id && !element.id && element.id !== id)
public removeElements(element: IdentifiableProjectorElement): ProjectorElements {
let removedElements: ProjectorElements;
let nonRemovedElements: ProjectorElements;
[removedElements, nonRemovedElements] = this.partitionArray(this.elements, elementOnProjector => {
return element.getIdentifiers().every(identifier => {
return !elementOnProjector[identifier] || elementOnProjector[identifier] === element[identifier];
});
});
this.elements = nonRemovedElements;
return removedElements;
}
private partitionArray<T>(array: T[], callback: (element: T) => boolean): [T[], T[]] {
return array.reduce(
(result, element) => {
result[callback(element) ? 0 : 1].push(element);
return result;
},
[[], []] as [T[], T[]]
);
}

View File

@ -77,6 +77,7 @@ import { C4DialogComponent, CopyrightSignComponent } from './components/copyrigh
import { ProjectorButtonComponent } from './components/projector-button/projector-button.component';
import { ProjectionDialogComponent } from './components/projection-dialog/projection-dialog.component';
import { ResizedDirective } from './directives/resized.directive';
import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component';
/**
* Share Module for all "dumb" components and pipes.
@ -188,7 +189,8 @@ import { ResizedDirective } from './directives/resized.directive';
C4DialogComponent,
ProjectorButtonComponent,
ProjectionDialogComponent,
ResizedDirective
ResizedDirective,
MetaTextBlockComponent
],
declarations: [
PermsDirective,
@ -211,7 +213,8 @@ import { ResizedDirective } from './directives/resized.directive';
C4DialogComponent,
ProjectorButtonComponent,
ProjectionDialogComponent,
ResizedDirective
ResizedDirective,
MetaTextBlockComponent
],
providers: [
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },

View File

@ -1,47 +1,9 @@
import { Projectable } from './projectable';
import { Projectable, ProjectorElementBuildDeskriptor } from './projectable';
import { BaseViewModel } from './base-view-model';
import { ProjectorOptions } from './projector-options';
/**
* Base view class for projectable models.
*/
export abstract class BaseProjectableModel extends BaseViewModel implements Projectable {
/**
* Per default, a slide does not have any options
*
* @override
*/
public getProjectorOptions(): ProjectorOptions {
return [];
}
/**
* @override
*/
public abstract getProjectionDefaultName(): string;
/**
* The id should match the model's id.
*
* @override
*/
public getIdForSlide(): number {
return this.id;
}
/**
* A model s return the collection string
*
* @override
*/
public abstract getNameForSlide(): string;
/**
* Per default a model is a non-stable element.
*
* @override
*/
public isStableSlide(): boolean {
return false;
}
public abstract getSlide(): ProjectorElementBuildDeskriptor;
}

View File

@ -1,32 +1,34 @@
import { ProjectorOptions } from './projector-options';
import { Displayable } from 'app/shared/models/base/displayable';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { SlideOptions } from './slide-options';
export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorElementBuildDeskriptor {
const deskriptor = <ProjectorElementBuildDeskriptor>obj;
return (
deskriptor.slideOptions !== undefined &&
deskriptor.getBasicProjectorElement !== undefined &&
deskriptor.getTitle !== undefined
);
}
export interface ProjectorElementBuildDeskriptor {
slideOptions: SlideOptions;
projectionDefaultName?: string;
getBasicProjectorElement(): IdentifiableProjectorElement;
/**
* The title to show in the projection dialog
*/
getTitle(): string;
}
export function isProjectable(obj: any): obj is Projectable {
return (<Projectable>obj).getSlide !== undefined;
}
/**
* Interface for every model, that should be projectable.
*/
export interface Projectable extends Displayable {
/**
* All options for the slide
*/
getProjectorOptions(): ProjectorOptions;
/**
* The projection default name for the slide
*/
getProjectionDefaultName(): string;
/**
* The (optional) id for the slide
*/
getIdForSlide(): number | null;
/**
* The slide's name
*/
getNameForSlide(): string;
/**
* The stable attribute for the slide.
*/
isStableSlide(): boolean;
getSlide(): ProjectorElementBuildDeskriptor;
}

View File

@ -1,32 +0,0 @@
export interface ProjectorDecisionOption {
key: string;
displayName: string;
default: string;
}
export interface ProjectorChoiceOption extends ProjectorDecisionOption {
choices: { value: string; displayName: string }[];
}
export type ProjectorOption = ProjectorDecisionOption | ProjectorChoiceOption;
export type ProjectorOptions = ProjectorOption[];
export function isProjectorDecisionOption(object: any): object is ProjectorDecisionOption {
const option = <ProjectorDecisionOption>object;
return (
option.key !== undefined &&
option.displayName !== undefined &&
option.default !== undefined &&
(<ProjectorChoiceOption>object).choices === undefined
);
}
export function isProjectorChoiceOption(object: any): object is ProjectorChoiceOption {
const option = <ProjectorChoiceOption>object;
return (
option.key !== undefined &&
option.displayName !== undefined &&
option.default !== undefined &&
option.choices !== undefined
);
}

View File

@ -0,0 +1,32 @@
export interface SlideDecisionOption {
key: string;
displayName: string;
default: string;
}
export interface SlideChoiceOption extends SlideDecisionOption {
choices: { value: string; displayName: string }[];
}
export type SlideOption = SlideDecisionOption | SlideChoiceOption;
export type SlideOptions = SlideOption[];
export function isSlideDecisionOption(object: any): object is SlideDecisionOption {
const option = <SlideDecisionOption>object;
return (
option.key !== undefined &&
option.displayName !== undefined &&
option.default !== undefined &&
(<SlideChoiceOption>object).choices === undefined
);
}
export function isSlideChoiceOption(object: any): object is SlideChoiceOption {
const option = <SlideChoiceOption>object;
return (
option.key !== undefined &&
option.displayName !== undefined &&
option.default !== undefined &&
option.choices !== undefined
);
}

View File

@ -4,6 +4,8 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli
import { StartComponent } from './components/start/start.component';
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
import { SearchComponent } from './components/search/search.component';
import { CountdownListComponent } from './components/countdown-list/countdown-list.component';
import { ProjectorMessageListComponent } from './components/projectormessage-list/projectormessage-list.component';
const routes: Routes = [
{
@ -21,6 +23,14 @@ const routes: Routes = [
{
path: 'search',
component: SearchComponent
},
{
path: 'countdowns',
component: CountdownListComponent
},
{
path: 'messages',
component: ProjectorMessageListComponent
}
];

View File

@ -7,9 +7,20 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli
import { StartComponent } from './components/start/start.component';
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
import { SearchComponent } from './components/search/search.component';
import { CountdownRepositoryService } from './services/countdown-repository.service';
import { CountdownListComponent } from './components/countdown-list/countdown-list.component';
import { ProjectorMessageListComponent } from './components/projectormessage-list/projectormessage-list.component';
@NgModule({
providers: [CountdownRepositoryService],
imports: [AngularCommonModule, CommonRoutingModule, SharedModule],
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, SearchComponent]
declarations: [
PrivacyPolicyComponent,
StartComponent,
LegalNoticeComponent,
SearchComponent,
CountdownListComponent,
ProjectorMessageListComponent
]
})
export class CommonModule {}

View File

@ -0,0 +1,92 @@
<os-head-bar [nav]="false" [goBack]="true" [mainButton]="true" (mainEvent)="onPlusButton()">
<!-- Title -->
<div class="title-slot">
<h2 translate>Countdowns</h2>
</div>
</os-head-bar>
<div class="head-spacer"></div>
<mat-card *ngIf="countdownToCreate">
<mat-card-title translate>New countdown</mat-card-title>
<mat-card-content>
<form [formGroup]="createForm"
(keydown)="onKeyDownCreate($event)">
<p>
<mat-form-field>
<input formControlName="description" matInput placeholder="{{'Description' | translate}}" required>
<mat-hint *ngIf="!createForm.controls.description.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p>
</form>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="create()">
<span translate>Save</span>
</button>
<button mat-button (click)="onCancelCreate()">
<span translate>Cancel</span>
</button>
</mat-card-actions>
</mat-card>
<mat-accordion class="os-card">
<mat-expansion-panel *ngFor="let countdown of countdowns" (opened)="openId = countdown.id"
(closed)="panelClosed(countdown)" [expanded]="openId === countdown.id" multiple="false">
<!-- Projector button and countdown description-->
<mat-expansion-panel-header>
<mat-panel-title>
<div class="header-container">
<div class="header-projector-button">
<os-projector-button [object]="countdown"></os-projector-button>
</div>
<div class="header-name">
{{ countdown.description }}
</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<form [formGroup]="updateForm"
*ngIf="editId === countdown.id"
(keydown)="onKeyDownUpdate($event)">
<h5 translate>Edit countdown</h5>
<p>
<mat-form-field>
<input formControlName="description" matInput placeholder="{{ 'Description' | translate}}" required>
<mat-hint *ngIf="!updateForm.controls.description.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>
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="editId === countdown.id" mat-button class="on-transition-fade" (click)="onCancelUpdate()"
mat-icon-button>
<mat-icon>close</mat-icon>
</button>
<button *ngIf="editId === countdown.id" mat-button class="on-transition-fade" (click)="onSaveButton(countdown)"
mat-icon-button>
<mat-icon>save</mat-icon>
</button>
<button mat-button class='on-transition-fade' (click)=onDeleteButton(countdown) mat-icon-button>
<mat-icon>delete</mat-icon>
</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
<mat-card *ngIf="countdowns.length === 0">
<mat-card-content>
<div class="no-content" translate>No countdowns</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,34 @@
.head-spacer {
width: 100%;
height: 60px;
line-height: 60px;
text-align: right;
background: white; /* TODO: remove this and replace with theme */
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
mat-card {
margin-bottom: 20px;
}
.header-container {
display: grid;
grid-template-rows: auto;
grid-template-columns: 40px 1fr;
width: 100%;
> div {
grid-row-start: 1;
grid-row-end: span 1;
grid-column-end: span 2;
}
.header-projector-button {
grid-column-start: 1;
}
.header-name {
grid-column-start: 2;
padding: 10px;
}
}

View File

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

View File

@ -0,0 +1,188 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { PromptService } from '../../../../core/services/prompt.service';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
import { ViewCountdown } from '../../models/view-countdown';
import { CountdownRepositoryService } from '../../services/countdown-repository.service';
import { Countdown } from 'app/shared/models/core/countdown';
/**
* List view for the statute paragraphs.
*/
@Component({
selector: 'os-countdown-list',
templateUrl: './countdown-list.component.html',
styleUrls: ['./countdown-list.component.scss']
})
export class CountdownListComponent extends BaseViewComponent implements OnInit {
public countdownToCreate: Countdown | null;
/**
* Source of the Data
*/
public countdowns: ViewCountdown[] = [];
/**
* The current focussed formgroup
*/
public updateForm: FormGroup;
public createForm: FormGroup;
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
) {
super(titleService, translate, matSnackBar);
const form = {
description: ['', Validators.required]
};
this.createForm = this.formBuilder.group(form);
this.updateForm = this.formBuilder.group(form);
}
/**
* Init function.
*
* Sets the title and gets/observes countdowns from DataStore
*/
public ngOnInit(): void {
super.setTitle('Countdowns');
this.repo.getViewModelListObservable().subscribe(newCountdowns => {
this.countdowns = newCountdowns;
});
}
/**
* Add a new Section.
*/
public onPlusButton(): void {
if (!this.countdownToCreate) {
this.createForm.reset();
this.createForm.setValue({
description: ''
});
this.countdownToCreate = new Countdown();
}
}
/**
* Handler when clicking on create to create a new statute paragraph
*/
public create(): void {
if (this.createForm.valid) {
this.countdownToCreate.patchValues(this.createForm.value as Countdown);
this.repo.create(this.countdownToCreate).then(() => {
this.countdownToCreate = null;
}, this.raiseError);
}
}
/**
* Executed on edit button
* @param countdown
*/
public onEditButton(countdown: ViewCountdown): void {
this.editId = countdown.id;
this.updateForm.setValue({
description: countdown.description
});
}
/**
* Saves the countdown
* @param countdown The countdown to save
*/
public onSaveButton(countdown: ViewCountdown): void {
if (this.updateForm.valid) {
this.repo.update(this.updateForm.value as Partial<Countdown>, countdown).then(() => {
this.openId = this.editId = null;
}, this.raiseError);
}
}
/**
* Is executed, when the delete button is pressed
*
* @param countdown The countdown to delete
*/
public async onDeleteButton(countdown: ViewCountdown): Promise<void> {
const content = this.translate.instant('Delete') + ` ${countdown.description}?`;
if (await this.promptService.open('Are you sure?', content)) {
this.repo.delete(countdown).then(() => (this.openId = this.editId = null), this.raiseError);
}
}
/**
* Is executed when a mat-extension-panel is closed
*
* @param countdown the statute paragraph in the panel
*/
public panelClosed(countdown: ViewCountdown): void {
this.openId = null;
if (this.editId) {
this.onSaveButton(countdown);
}
}
/**
* clicking Shift and Enter will save automatically
* clicking Escape will cancel the process
*
* @param event has the code
*/
public onKeyDownCreate(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) {
this.create();
}
if (event.key === 'Escape') {
this.onCancelCreate();
}
}
/**
* Cancels the current form action
*/
public onCancelCreate(): void {
this.countdownToCreate = null;
}
/**
* clicking Shift and Enter will save automatically
* clicking Escape will cancel the process
*
* @param event has the code
*/
public onKeyDownUpdate(event: KeyboardEvent): void {
if (event.key === 'Enter' && event.shiftKey) {
const countdown = this.countdowns.find(x => x.id === this.editId);
this.onSaveButton(countdown);
}
if (event.key === 'Escape') {
this.onCancelUpdate();
}
}
/**
* Cancels the current form action
*/
public onCancelUpdate(): void {
this.editId = null;
}
}

View File

@ -0,0 +1,9 @@
<os-head-bar [nav]="false" [goBack]="true" [mainButton]="true" (mainEvent)="onPlusButton()">
<!-- Title -->
<div class="title-slot">
<h2 translate>Messages</h2>
</div>
</os-head-bar>
<div class="head-spacer"></div>
<p>TODO</p>

View File

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

View File

@ -0,0 +1,32 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BaseViewComponent } from '../../../base/base-view';
import { MatSnackBar } from '@angular/material';
/**
* List view for the statute paragraphs.
*/
@Component({
selector: 'os-projectormessage-list',
templateUrl: './projectormessage-list.component.html',
styleUrls: ['./projectormessage-list.component.scss']
})
export class ProjectorMessageListComponent extends BaseViewComponent implements OnInit {
public constructor(titleService: Title, translate: TranslateService, matSnackBar: MatSnackBar) {
super(titleService, translate, matSnackBar);
}
/**
* Init function.
*
* Sets the title and gets/observes countdowns from DataStore
*/
public ngOnInit(): void {
super.setTitle('Messages');
}
public onPlusButton(): void {}
}

View File

@ -0,0 +1,46 @@
import { Countdown } from '../../../shared/models/core/countdown';
import { BaseProjectableModel } from 'app/site/base/base-projectable-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
export class ViewCountdown extends BaseProjectableModel {
private _countdown: Countdown;
public get countdown(): Countdown {
return this._countdown ? this._countdown : null;
}
public get id(): number {
return this.countdown ? this.countdown.id : null;
}
public get description(): string {
return this.countdown ? this.countdown.description : null;
}
public constructor(countdown?: Countdown) {
super();
this._countdown = countdown;
}
public getTitle(): string {
return this.description;
}
public updateValues(countdown: Countdown): void {
console.log('Update countdown TODO with vals:', countdown);
}
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: () => ({
stable: true,
name: Countdown.COLLECTIONSTRING,
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [],
projectionDefaultName: 'countdowns',
getTitle: () => this.getTitle()
};
}
}

View File

@ -0,0 +1,46 @@
import { BaseProjectableModel } from 'app/site/base/base-projectable-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
export class ViewProjectorMessage extends BaseProjectableModel {
private _message: ProjectorMessage;
public get messaage(): ProjectorMessage {
return this._message ? this._message : null;
}
public get id(): number {
return this.messaage ? this.messaage.id : null;
}
public get message(): string {
return this.messaage ? this.messaage.message : null;
}
public constructor(message?: ProjectorMessage) {
super();
this._message = message;
}
public getTitle(): string {
return 'Message 1';
}
public updateValues(message: ProjectorMessage): void {
console.log('Update message TODO with vals:', message);
}
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: () => ({
stable: true,
name: ProjectorMessage.COLLECTIONSTRING,
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [],
projectionDefaultName: 'messages',
getTitle: () => this.getTitle()
};
}
}

View File

@ -0,0 +1,17 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { CountdownRepositoryService } from './countdown-repository.service';
describe('StatuteParagraphRepositoryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [CountdownRepositoryService]
});
});
it('should be created', inject([CountdownRepositoryService], (service: CountdownRepositoryService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { DataSendService } from '../../../core/services/data-send.service';
import { DataStoreService } from '../../../core/services/data-store.service';
import { BaseRepository } from '../../base/base-repository';
import { Identifiable } from '../../../shared/models/base/identifiable';
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
import { ViewCountdown } from '../models/view-countdown';
import { Countdown } from '../../../shared/models/core/countdown';
@Injectable({
providedIn: 'root'
})
export class CountdownRepositoryService extends BaseRepository<ViewCountdown, Countdown> {
public constructor(
DS: DataStoreService,
mapperService: CollectionStringModelMapperService,
private dataSend: DataSendService
) {
super(DS, mapperService, Countdown);
}
protected createViewModel(countdown: Countdown): ViewCountdown {
return new ViewCountdown(countdown);
}
public async create(countdown: Countdown): Promise<Identifiable> {
return await this.dataSend.createModel(countdown);
}
public async update(countdown: Partial<Countdown>, viewCountdown: ViewCountdown): Promise<void> {
const update = viewCountdown.countdown;
update.patchValues(countdown);
await this.dataSend.updateModel(update);
}
public async delete(viewCountdown: ViewCountdown): Promise<void> {
await this.dataSend.deleteModel(viewCountdown.countdown);
}
}

View File

@ -0,0 +1,20 @@
import { TestBed, inject } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { ProjectorMessageRepositoryService } from './projectormessage-repository.service';
describe('ProjectorMessageRepositoryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
providers: [ProjectorMessageRepositoryService]
});
});
it('should be created', inject(
[ProjectorMessageRepositoryService],
(service: ProjectorMessageRepositoryService) => {
expect(service).toBeTruthy();
}
));
});

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { DataStoreService } from '../../../core/services/data-store.service';
import { BaseRepository } from '../../base/base-repository';
import { Identifiable } from '../../../shared/models/base/identifiable';
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
import { ViewProjectorMessage } from '../models/view-projectormessage';
@Injectable({
providedIn: 'root'
})
export class ProjectorMessageRepositoryService extends BaseRepository<ViewProjectorMessage, ProjectorMessage> {
public constructor(DS: DataStoreService, mapperService: CollectionStringModelMapperService) {
super(DS, mapperService, ProjectorMessage);
}
protected createViewModel(message: ProjectorMessage): ViewProjectorMessage {
return new ViewProjectorMessage(message);
}
public async create(message: ProjectorMessage): Promise<Identifiable> {
throw new Error('TODO');
}
public async update(message: Partial<ProjectorMessage>, viewMessage: ViewProjectorMessage): Promise<void> {
throw new Error('TODO');
}
public async delete(viewMessage: ViewProjectorMessage): Promise<void> {
throw new Error('TODO');
}
}

View File

@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MotionCommentsComponent } from './motion-comments.component';
import { E2EImportsModule } from '../../../../../e2e-imports.module';
import { MetaTextBlockComponent } from '../meta-text-block/meta-text-block.component';
describe('MotionCommentsComponent', () => {
let component: MotionCommentsComponent;
@ -11,7 +10,7 @@ describe('MotionCommentsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MetaTextBlockComponent, MotionCommentsComponent]
declarations: [MotionCommentsComponent]
}).compileComponents();
}));

View File

@ -1,6 +1,7 @@
<os-meta-text-block showActionRow="true">
<ng-container class="meta-text-block-title">
<span translate>Voting result</span> <span *ngIf="pollIndex">&nbsp;({{ pollIndex + 1 }})</span>
<span translate>Voting result</span>
<span *ngIf="pollIndex">&nbsp;({{ pollIndex + 1 }})</span>
</ng-container>
<ng-container class="meta-text-block-content">
<div *ngIf="poll.has_votes" class="on-transition-fade poll-result">
@ -51,12 +52,11 @@
</div>
</ng-container>
<ng-container class="meta-text-block-action-row" *osPerms="'motions.can_manage_metadata'">
<button mat-icon-button class="main-nav-color" matTooltip="{{ 'Edit' | translate }}" (click)="editPoll()">
<button mat-icon-button matTooltip="{{ 'Edit' | translate }}" (click)="editPoll()">
<mat-icon inline>edit</mat-icon>
</button>
<button
mat-icon-button
class="main-nav-color"
matTooltip="{{ 'Print ballot papers' | translate }}"
(click)="printBallots()"
>
@ -64,7 +64,6 @@
</button>
<button
mat-icon-button
class="main-nav-color"
matTooltip="{{ 'Delete' | translate }}"
(click)="deletePoll()"
>

View File

@ -47,6 +47,7 @@
}
}
}
.main-nav-color {
color: rgba(0, 0, 0, 0.54);
}

View File

@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PersonalNoteComponent } from './personal-note.component';
import { E2EImportsModule } from 'e2e-imports.module';
import { MetaTextBlockComponent } from '../meta-text-block/meta-text-block.component';
describe('PersonalNoteComponent', () => {
let component: PersonalNoteComponent;
@ -11,7 +10,7 @@ describe('PersonalNoteComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [MetaTextBlockComponent, PersonalNoteComponent]
declarations: [PersonalNoteComponent]
}).compileComponents();
}));

View File

@ -11,7 +11,7 @@ import { User } from '../../../shared/models/users/user';
import { ViewMotionCommentSection } from './view-motion-comment-section';
import { Workflow } from '../../../shared/models/motions/workflow';
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
import { ProjectorOptions } from 'app/site/base/projector-options';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
/**
* The line numbering mode for the motion detail view.
@ -459,28 +459,29 @@ export class ViewMotion extends BaseProjectableModel {
return this.amendment_paragraphs.length > 0;
}
public getProjectorOptions(): ProjectorOptions {
return [
{
key: 'mode',
displayName: 'Mode',
default: 'original',
choices: [
{ value: 'original', displayName: 'Original' },
{ value: 'changed', displayName: 'Changed' },
{ value: 'diff', displayName: 'Diff' },
{ value: 'agreed', displayName: 'Agreed' }
]
}
];
}
public getProjectionDefaultName(): string {
return 'motions';
}
public getNameForSlide(): string {
return Motion.COLLECTIONSTRING;
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: () => ({
name: Motion.COLLECTIONSTRING,
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [
{
key: 'mode',
displayName: 'Mode',
default: 'original',
choices: [
{ value: 'original', displayName: 'Original' },
{ value: 'changed', displayName: 'Changed' },
{ value: 'diff', displayName: 'Diff' },
{ value: 'agreed', displayName: 'Agreed' }
]
}
],
projectionDefaultName: 'motions',
getTitle: () => this.getTitle()
};
}
/**

View File

@ -12,7 +12,6 @@ import { MotionChangeRecommendationComponent } from './components/motion-change-
import { MotionDetailOriginalChangeRecommendationsComponent } from './components/motion-detail-original-change-recommendations/motion-detail-original-change-recommendations.component';
import { MotionDetailDiffComponent } from './components/motion-detail-diff/motion-detail-diff.component';
import { MotionCommentsComponent } from './components/motion-comments/motion-comments.component';
import { MetaTextBlockComponent } from './components/meta-text-block/meta-text-block.component';
import { PersonalNoteComponent } from './components/personal-note/personal-note.component';
import { CallListComponent } from './components/call-list/call-list.component';
import { AmendmentCreateWizardComponent } from './components/amendment-create-wizard/amendment-create-wizard.component';
@ -36,7 +35,6 @@ import { MotionExportDialogComponent } from './components/motion-export-dialog/m
MotionDetailOriginalChangeRecommendationsComponent,
MotionDetailDiffComponent,
MotionCommentsComponent,
MetaTextBlockComponent,
PersonalNoteComponent,
CallListComponent,
AmendmentCreateWizardComponent,
@ -53,7 +51,6 @@ import { MotionExportDialogComponent } from './components/motion-export-dialog/m
StatuteParagraphListComponent,
MotionCommentsComponent,
MotionCommentSectionListComponent,
MetaTextBlockComponent,
PersonalNoteComponent,
ManageSubmittersComponent,
MotionPollDialogComponent,

View File

@ -5,15 +5,17 @@
</div>
</os-head-bar>
<div class="content-container">
<div class="content-container" *ngIf="projector">
<div class="column-left">
<div id="projector">
<os-projector [projector]="projector"></os-projector>
</div>
<a [routerLink]="['/projector', projector.id]">
<div id="projector">
<os-projector [projector]="projector"></os-projector>
</div>
</a>
</div>
<div class="column-right">
<div class="control-group">
{{ projector?.scroll }}
<div class="button-size">{{ projector.scroll }}</div>
<button type="button" mat-icon-button (click)="scroll(scrollScaleDirection.Up)">
<mat-icon>arrow_upward</mat-icon>
</button>
@ -24,9 +26,8 @@
<mat-icon>refresh</mat-icon>
</button>
</div>
<hr>
<div class="control-group">
{{ projector?.scale }}
<div class="button-size">{{ projector.scale }}</div>
<button type="button" mat-icon-button (click)="scale(scrollScaleDirection.Up)">
<mat-icon>zoom_in</mat-icon>
</button>
@ -37,5 +38,121 @@
<mat-icon>refresh</mat-icon>
</button>
</div>
<hr>
<div class="control-group">
<button type="button" mat-button (click)="projectPreviousSlide()" [disabled]="projector?.elements_history.length === 0">
<mat-icon>arrow_back</mat-icon>
<span translate>Previous</span>
</button>
<button type="button" mat-button (click)="projectNextSlide()" [disabled]="projector?.elements_preview.length === 0">
<span translate>Next</span>
<mat-icon>arrow_forward</mat-icon>
</button>
</div>
<div class="queue">
<h5 translate>History</h5>
<p *ngFor="let elements of projector?.elements_history">
{{ getElementDescription(elements[0]) }}
</p>
</div>
<div>
<h5 translate>Current</h5>
<div *ngIf="projector.non_stable_elements.length">
<h4 translate>Slides</h4>
<mat-list>
<mat-list-item *ngFor="let element of projector.non_stable_elements" class="projected">
<button type="button" mat-icon-button (click)="unprojectCurrent(element)">
<mat-icon>videocam</mat-icon>
</button>
{{ getElementDescription(element) }}
</mat-list-item>
</mat-list>
</div>
<div *ngIf="countdowns.length">
<h4>
<span translate>Countdowns</span>
<button type="button" mat-icon-button disableRipple routerLink="/countdowns">
<mat-icon>edit</mat-icon>
</button>
</h4>
<mat-list>
<mat-list-item *ngFor="let countdown of countdowns" [ngClass]="{'projected': isProjected(countdown)}">
<button type="button" mat-icon-button (click)="project(countdown)">
<mat-icon>videocam</mat-icon>
</button>
{{ countdown.description }}
</mat-list-item>
</mat-list>
</div>
<div *ngIf="messages.length">
<h4>
<span translate>Messages</span>
<button type="button" mat-icon-button disableRipple routerLink="/messages">
<mat-icon>edit</mat-icon>
</button>
</h4>
<mat-list>
<mat-list-item *ngFor="let message of messages; let i = index" [ngClass]="{'projected': isProjected(message)}">
<button type="button" mat-icon-button (click)="project(message)">
<mat-icon>videocam</mat-icon>
</button>
<span translate>Message</span> {{ i + 1 }}
</mat-list-item>
</mat-list>
</div>
<div>
<h4>Current list of speakers overlay</h4>
<mat-list>
<mat-list-item [ngClass]="{'projected': isClosProjected(true)}">
<button type="button" mat-icon-button (click)="toggleClos(true)">
<mat-icon>videocam</mat-icon>
</button>
<span translate>Current list of speakers overlay</span>
</mat-list-item>
</mat-list>
</div>
<div *ngIf="!isClosProjected(false)">
<h4>Current list of speakers slide</h4>
<mat-list>
<mat-list-item>
<button type="button" mat-icon-button (click)="toggleClos(false)">
<mat-icon>videocam</mat-icon>
</button>
<span translate>Current list of speakers slide</span>
</mat-list-item>
</mat-list>
</div>
</div>
<div class="queue">
<h5 translate>Queue</h5>
<div cdkDropList class="drop-list" (cdkDropListDropped)="onSortingChange($event)">
<div class="list-entry" *ngFor="let element of projector.elements_preview; let i = index" cdkDrag>
<div class="drag-handle" cdkDragHandle>
<mat-icon>unfold_more</mat-icon>
</div>
<div class="name">
{{ i+1 }}.&nbsp;<span>{{ getElementDescription(element) }}</span>
</div>
<div class="button-right">
<div>
<button type="button" mat-button (click)="projectNow(i)">
<span translate>Project now</span>
</button>
<button type="button" mat-icon-button (click)="removePreviewElement(i)">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -5,16 +5,91 @@
.column-left {
display: inline-block;
padding-top: 20px;
width: 70%;
width: 60%;
min-width: 200px;
padding-right: 25px;
/* Do not let the a tag ruin the projector */
a {
color: inherit;
text-decoration: inherit;
}
}
.column-right {
padding-top: 20px;
min-width: calc(30% - 25px);
width: calc(40% - 30px);
float: right;
}
.control-group {
text-align: center;
color: rgba(0, 0, 0, 0.54);
.button-size {
width: 40px;
display: inline-block;
}
}
h4 {
margin-top: 5px;
button {
width: 20px;
height: 20px;
}
}
h5 {
margin-bottom: 0;
}
.queue {
margin-top: 15px;
.drop-list {
width: 100%;
display: block;
overflow: hidden;
.list-entry {
display: table;
min-height: 50px;
width: 100%;
border-bottom: solid 1px #ccc;
color: rgba(0, 0, 0, 0.87);
.drag-handle {
display: table-cell;
padding: 0 10px;
line-height: 0px;
vertical-align: middle;
width: 25px;
color: slategrey;
cursor: move;
}
.name {
display: table-cell;
vertical-align: middle;
}
.button-right {
display: table-cell;
padding-right: 10px;
vertical-align: middle;
width: auto;
white-space: nowrap;
div {
float: right;
}
}
}
.list-entry:last-child {
border: none;
}
}
}

View File

@ -8,6 +8,16 @@ import { TranslateService } from '@ngx-translate/core';
import { ProjectorRepositoryService, ScrollScaleDirection } from '../../services/projector-repository.service';
import { ViewProjector } from '../../models/view-projector';
import { BaseViewComponent } from 'app/site/base/base-view';
import { ProjectorService } from 'app/core/services/projector.service';
import { moveItemInArray, CdkDragDrop } from '@angular/cdk/drag-drop';
import { ProjectorElement } from 'app/shared/models/core/projector';
import { SlideManager } from 'app/slides/services/slide-manager.service';
import { CountdownRepositoryService } from 'app/site/common/services/countdown-repository.service';
import { ProjectorMessageRepositoryService } from 'app/site/common/services/projectormessage-repository.service';
import { ViewProjectorMessage } from 'app/site/common/models/view-projectormessage';
import { ViewCountdown } from 'app/site/common/models/view-countdown';
import { Projectable } from 'app/site/base/projectable';
import { CurrentListOfSpeakersSlideService } from '../../services/current-list-of-of-speakers-slide.service';
/**
* The projector detail view.
@ -25,6 +35,10 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public scrollScaleDirection = ScrollScaleDirection;
public countdowns: ViewCountdown[] = [];
public messages: ViewProjectorMessage[] = [];
/**
* @param titleService
* @param translate
@ -37,9 +51,17 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
translate: TranslateService,
matSnackBar: MatSnackBar,
private repo: ProjectorRepositoryService,
private route: ActivatedRoute
private route: ActivatedRoute,
private projectorService: ProjectorService,
private slideManager: SlideManager,
private countdownRepo: CountdownRepositoryService,
private messageRepo: ProjectorMessageRepositoryService,
private currentListOfSpeakersSlideService: CurrentListOfSpeakersSlideService
) {
super(titleService, translate, matSnackBar);
this.countdownRepo.getViewModelListObservable().subscribe(countdowns => (this.countdowns = countdowns));
this.messageRepo.getViewModelListObservable().subscribe(messages => (this.messages = messages));
}
/**
@ -68,4 +90,65 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public scale(direction: ScrollScaleDirection): void {
this.repo.scale(this.projector, direction).then(null, this.raiseError);
}
public projectNextSlide(): void {
this.projectorService.projectNextSlide(this.projector.projector).then(null, this.raiseError);
}
public projectPreviousSlide(): void {
this.projectorService.projectPreviousSlide(this.projector.projector).then(null, this.raiseError);
}
public onSortingChange(event: CdkDragDrop<ProjectorElement>): void {
moveItemInArray(this.projector.elements_preview, event.previousIndex, event.currentIndex);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
}
public removePreviewElement(elementIndex: number): void {
this.projector.elements_preview.splice(elementIndex, 1);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
}
public projectNow(elementIndex: number): void {
this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError);
}
public getElementDescription(element: ProjectorElement): string {
if (!this.slideManager.canSlideBeMappedToModel(element.name)) {
return this.slideManager.getSlideVerboseName(element.name);
} else {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
const model = this.projectorService.getModelFromProjectorElement(idElement);
return model.getTitle();
}
}
public isProjected(obj: Projectable): boolean {
return this.projectorService.isProjectedOn(obj, this.projector.projector);
}
public async project(obj: Projectable): Promise<void> {
try {
if (this.isProjected(obj)) {
await this.projectorService.removeFrom(this.projector.projector, obj);
} else {
await this.projectorService.projectOn(this.projector.projector, obj);
}
} catch (e) {
this.raiseError(e);
}
}
public unprojectCurrent(element: ProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
this.projectorService.removeFrom(this.projector.projector, idElement).then(null, this.raiseError);
}
public isClosProjected(stable: boolean): boolean {
return this.currentListOfSpeakersSlideService.isProjectedOn(this.projector, stable);
}
public toggleClos(stable: boolean): void {
this.currentListOfSpeakersSlideService.toggleOn(this.projector, stable);
}
}

View File

@ -39,13 +39,12 @@
</mat-card>
<div id="card-wrapper">
<mat-card class="projector-card" *ngFor="let projector of projectors">
<mat-card-title>
<a [routerLink]="['/projector-site/detail', projector.id]">
{{ projector.name }}
</a>
<div class="card-actions">
<div class="projector-card" *ngFor="let projector of projectors">
<os-meta-text-block showActionRow="false">
<ng-container class="meta-text-block-title">
{{ projector.name | translate }}
</ng-container>
<ng-container class="meta-text-block-action-row">
<button mat-icon-button *ngIf="editId !== projector.id" (click)=onEditButton(projector)>
<mat-icon>edit</mat-icon>
</button>
@ -55,47 +54,53 @@
<button mat-icon-button *ngIf="editId === projector.id" (click)=onSaveButton(projector)>
<mat-icon>save</mat-icon>
</button>
<button mat-icon-button mat-button (click)=onDeleteButton(projector)>
<button mat-icon-button color="warn" (click)=onDeleteButton(projector)>
<mat-icon>delete</mat-icon>
</button>
</div>
</mat-card-title>
<mat-card-content>
<p>TODO: projector</p>
<ng-container *ngIf="editId === projector.id">
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
<p>
</ng-container>
<ng-container class="meta-text-block-content">
<a class="no-markup" [routerLink]="['/projectors/detail', projector.id]">
<div class="projector">
<os-projector [projector]="projector"></os-projector>
</div>
</a>
<ng-container *ngIf="editId === projector.id">
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
<!-- Name field -->
<mat-form-field>
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
<mat-hint *ngIf="!createForm.controls.name.valid">
<mat-hint *ngIf="!updateForm.controls.name.valid">
<span translate>Required</span>
</mat-hint>
</mat-form-field>
</p><p>
<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">
{{ ratio }}
</mat-radio-button>
</mat-radio-group>
</p><p>
<mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider>
{{ updateForm.value.width }}
</p>
</form>
<div>
<mat-checkbox formControlName="clock">
<span translate>Show clock</span>
</mat-checkbox>
</div>
</form>
</ng-container>
</ng-container>
</mat-card-content>
</mat-card>
</os-meta-text-block>
</div>
</div>
<mat-menu #ellipsisMenu="matMenu">
<button mat-menu-item routerLink="/countdowns">
<mat-icon>alarm</mat-icon>
<span translate>Countdowns</span>
</button>
<button mat-menu-item>
<mat-icon>note</mat-icon>
<span translate>Projector messages</span>
</button>
<button mat-menu-item>
<mat-icon>alarm</mat-icon>
<span translate>Countdowns</span>
</button>
</mat-menu>

View File

@ -1,13 +1,28 @@
#card-wrapper {
margin-top: 10px;
margin-left: 10px;
.projector-card {
margin: 20px;
width: 300px;
display: inline-block;
}
width: 350px;
margin: 10px;
float: left;
.card-actions {
float: right;
.projector {
width: 320px;
}
form {
margin-top: 10px;
}
::ng-deep mat-card {
margin: 0;
}
.no-markup {
/* Do not let the a tag ruin the projector */
color: inherit;
text-decoration: inherit;
}
}
}

View File

@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material';
import { TranslateService } from '@ngx-translate/core';
@ -8,8 +9,8 @@ import { ProjectorRepositoryService } from '../../services/projector-repository.
import { ViewProjector } from '../../models/view-projector';
import { Projector } from 'app/shared/models/core/projector';
import { BaseViewComponent } from 'app/site/base/base-view';
import { MatSnackBar } from '@angular/material';
import { PromptService } from 'app/core/services/prompt.service';
import { ClockSlideService } from '../../services/clock-slide.service';
/**
* All supported aspect rations for projectors.
@ -76,7 +77,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
matSnackBar: MatSnackBar,
private repo: ProjectorRepositoryService,
private formBuilder: FormBuilder,
private promptService: PromptService
private promptService: PromptService,
private clockSlideService: ClockSlideService
) {
super(titleService, translate, matSnackBar);
@ -88,7 +90,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
this.updateForm = this.formBuilder.group({
name: ['', Validators.required],
aspectRatio: ['', Validators.required],
width: [0, Validators.required]
width: [0, Validators.required],
clock: [true]
});
}
@ -116,6 +119,12 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
public create(): void {
if (this.createForm.valid && this.projectorToCreate) {
this.projectorToCreate.patchValues(this.createForm.value as Projector);
// TODO: the server shouldn't want to have this data..
this.projectorToCreate.patchValues({
elements: [{ name: 'core/clock', stable: true }],
elements_preview: [],
elements_history: []
});
this.repo.create(this.projectorToCreate).then(() => (this.projectorToCreate = null), this.raiseError);
}
}
@ -178,7 +187,8 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
this.updateForm.patchValue({
name: projector.name,
aspectRatio: this.getAspectRatioKey(projector),
width: projector.width
width: projector.width,
clock: this.clockSlideService.isProjectedOn(projector)
});
}
@ -198,7 +208,7 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
*
* @param projector The projector to save.
*/
public onSaveButton(projector: ViewProjector): void {
public async onSaveButton(projector: ViewProjector): Promise<void> {
if (projector.id !== this.editId || !this.updateForm.valid) {
return;
}
@ -207,8 +217,13 @@ export class ProjectorListComponent extends BaseViewComponent implements OnInit
width: this.updateForm.value.width,
height: Math.round(this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio])
};
this.repo.update(updateProjector, projector);
this.editId = null;
try {
await this.clockSlideService.setProjectedOn(projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, projector);
this.editId = null;
} catch (e) {
this.raiseError(e);
}
}
/**

View File

@ -20,6 +20,7 @@
}
.content {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;

View File

@ -216,8 +216,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
.getProjectorObservable(to)
.subscribe(data => (this.slides = data || []));
this.projectorSubscription = this.projectorRepository.getViewModelObservable(to).subscribe(projector => {
this.scroll = projector.scroll;
this.scale = projector.scale;
if (projector) {
this.scroll = projector.scroll || 0;
this.scale = projector.scale || 0;
}
});
} else if (!to && from > 0) {
// no new projector

View File

@ -1,3 +1,4 @@
::ng-deep #slide {
z-index: 5;
height: 100%;
}

View File

@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component';
import { SlideData } from '../../services/projector-data.service';
import { DynamicSlideLoader } from 'app/slides/services/dynamic-slide-loader.service';
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/services/config.service';
@ -106,7 +106,7 @@ export class SlideContainerComponent extends BaseComponent {
public constructor(
titleService: Title,
translate: TranslateService,
private dynamicSlideLoader: DynamicSlideLoader,
private slideManager: SlideManager,
private configService: ConfigService
) {
super(titleService, translate);
@ -133,13 +133,13 @@ export class SlideContainerComponent extends BaseComponent {
}
/**
* Loads the slides via the dynamicSlideLoader. Creates the slide components and provide the slide data to it.
* Loads the slides via the SlideManager. Creates the slide components and provide the slide data to it.
*
* @param slideName The slide to load.
*/
private slideChanged(slideName: string): void {
this.slideOptions = this.dynamicSlideLoader.getSlideOptions(slideName);
this.dynamicSlideLoader.getSlideFactory(slideName).then(slideFactory => {
this.slideOptions = this.slideManager.getSlideOptions(slideName);
this.slideManager.getSlideFactory(slideName).then(slideFactory => {
this.slide.clear();
this.slideRef = this.slide.createComponent(slideFactory);
this.setDataForComponent();

View File

@ -20,6 +20,18 @@ export class ViewProjector extends BaseViewModel {
return this.projector ? this.projector.elements : null;
}
public get non_stable_elements(): ProjectorElements {
return this.projector ? this.projector.elements.filter(element => !element.stable) : null;
}
public get elements_preview(): ProjectorElements {
return this.projector ? this.projector.elements_preview : null;
}
public get elements_history(): ProjectorElements[] {
return this.projector ? this.projector.elements_history : null;
}
public get height(): number {
return this.projector ? this.projector.height : null;
}

View File

@ -5,7 +5,7 @@ import { ProjectorDetailComponent } from './components/projector-detail/projecto
const routes: Routes = [
{
path: 'list',
path: '',
component: ProjectorListComponent
},
{

View File

@ -12,7 +12,7 @@ export const ProjectorAppConfig: AppConfig = {
],
mainMenuEntries: [
{
route: '/projector-site/list',
route: '/projectors',
displayName: 'Projector',
icon: 'videocam',
weight: 700,

View File

@ -8,8 +8,12 @@ import { ProjectorListComponent } from './components/projector-list/projector-li
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
import { SlideContainerComponent } from './components/slide-container/slide-container.component';
import { FullscreenProjectorComponent } from './components/fullscreen-projector/fullscreen-projector.component';
import { ClockSlideService } from './services/clock-slide.service';
import { ProjectorRepositoryService } from './services/projector-repository.service';
import { ProjectorDataService } from './services/projector-data.service';
@NgModule({
providers: [ClockSlideService, ProjectorDataService, ProjectorRepositoryService],
imports: [CommonModule, ProjectorRoutingModule, SharedModule],
declarations: [
ProjectorComponent,

View File

@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { ProjectorService } from 'app/core/services/projector.service';
import { ViewProjector } from '../models/view-projector';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
/**
*/
@Injectable({
providedIn: 'root'
})
export class ClockSlideService {
public constructor(private projectorService: ProjectorService) {}
private getClockProjectorElement(): IdentifiableProjectorElement {
return {
name: 'core/clock',
stable: true,
getIdentifiers: () => ['name']
};
}
public isProjectedOn(projector: ViewProjector): boolean {
return this.projectorService.isProjectedOn(this.getClockProjectorElement(), projector.projector);
}
public async setProjectedOn(projector: ViewProjector, show: boolean): Promise<void> {
const isClockProjected = this.isProjectedOn(projector);
if (show && !isClockProjected) {
await this.projectorService.projectOn(projector.projector, this.getClockProjectorElement());
} else if (!show && isClockProjected) {
await this.projectorService.removeFrom(projector.projector, this.getClockProjectorElement());
}
}
}

View File

@ -0,0 +1,43 @@
import { Injectable } from '@angular/core';
import { ProjectorService } from 'app/core/services/projector.service';
import { ViewProjector } from '../models/view-projector';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
/**
*/
@Injectable({
providedIn: 'root'
})
export class CurrentListOfSpeakersSlideService {
public constructor(private projectorService: ProjectorService) {}
private getCurrentListOfSpeakersProjectorElement(overlay: boolean): IdentifiableProjectorElement {
return {
name: overlay ? 'agenda/current-list-of-speakers-overlay' : 'agenda/current-list-of-speakers',
stable: overlay,
getIdentifiers: () => ['name', 'stable']
};
}
public isProjectedOn(projector: ViewProjector, overlay: boolean): boolean {
return this.projectorService.isProjectedOn(
this.getCurrentListOfSpeakersProjectorElement(overlay),
projector.projector
);
}
public async toggleOn(projector: ViewProjector, overlay: boolean): Promise<void> {
const isClosProjected = this.isProjectedOn(projector, overlay);
if (isClosProjected) {
await this.projectorService.removeFrom(
projector.projector,
this.getCurrentListOfSpeakersProjectorElement(overlay)
);
} else {
await this.projectorService.projectOn(
projector.projector,
this.getCurrentListOfSpeakersProjectorElement(overlay)
);
}
}
}

View File

@ -12,7 +12,7 @@ export interface SlideData<T = object> {
export type ProjectorData = SlideData[];
interface AllProjectorData {
[id: number]: ProjectorData;
[id: number]: ProjectorData | { error: string };
}
/**
@ -45,8 +45,12 @@ export class ProjectorDataService {
this.websocketService.getOberservable('projector').subscribe((update: AllProjectorData) => {
Object.keys(update).forEach(_id => {
const id = parseInt(_id, 10);
if (this.currentProjectorData[id]) {
this.currentProjectorData[id].next(update[id]);
if ((<{ error: string }>update[id]).error !== undefined) {
console.log('TODO: Why does the server sends errors on autpupdates?');
} else {
if (this.currentProjectorData[id]) {
this.currentProjectorData[id].next(update[id] as ProjectorData);
}
}
});
});

View File

@ -82,7 +82,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
* @param direction The direction.
*/
public async scroll(projector: ViewProjector, direction: ScrollScaleDirection): Promise<void> {
this.controlView(projector, direction, 'scroll');
await this.controlView(projector, direction, 'scroll');
}
/**
@ -92,7 +92,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
* @param direction The direction.
*/
public async scale(projector: ViewProjector, direction: ScrollScaleDirection): Promise<void> {
this.controlView(projector, direction, 'scale');
await this.controlView(projector, direction, 'scale');
}
/**
@ -107,7 +107,7 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
direction: ScrollScaleDirection,
action: 'scale' | 'scroll'
): Promise<void> {
this.http.post(`/rest/core/projector/${projector.id}/control_view`, {
await this.http.post<void>(`/rest/core/projector/${projector.id}/control_view/`, {
action: action,
direction: direction
});

View File

@ -52,7 +52,7 @@ const routes: Routes = [
loadChildren: './history/history.module#HistoryModule'
},
{
path: 'projector-site',
path: 'projectors',
loadChildren: './projector/projector.module#ProjectorModule'
}
],

View File

@ -2,6 +2,7 @@ import { User } from '../../../shared/models/users/user';
import { Group } from '../../../shared/models/users/group';
import { BaseModel } from '../../../shared/models/base/base-model';
import { BaseProjectableModel } from 'app/site/base/base-projectable-model';
import { ProjectorElementBuildDeskriptor } from 'app/site/base/projectable';
export class ViewUser extends BaseProjectableModel {
private _user: User;
@ -109,16 +110,17 @@ export class ViewUser extends BaseProjectableModel {
this._groups = groups;
}
public getProjectionDefaultName(): string {
return 'users';
}
public getNameForSlide(): string {
return User.COLLECTIONSTRING;
}
public isStableSlide(): boolean {
return true;
public getSlide(): ProjectorElementBuildDeskriptor {
return {
getBasicProjectorElement: () => ({
name: User.COLLECTIONSTRING,
id: this.id,
getIdentifiers: () => ['name', 'id']
}),
slideOptions: [],
projectionDefaultName: 'users',
getTitle: () => this.getTitle()
};
}
/**

View File

@ -0,0 +1,3 @@
export interface AgendaCurrentListOfSpeakersSlideData {
error: string;
}

View File

@ -0,0 +1,3 @@
<div id="overlay">
Current list of speakers overlay
</div>

View File

@ -0,0 +1,9 @@
#overlay {
position: absolute;
right: 0;
bottom: 0;
background-color: green;
height: 30px;
margin: 10px;
z-index: 2;
}

View File

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

View File

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
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';
@Component({
selector: 'os-agenda-current-list-of-speakers-overlay-slide',
templateUrl: './agenda-current-list-of-speakers-overlay-slide.component.html',
styleUrls: ['./agenda-current-list-of-speakers-overlay-slide.component.scss']
})
export class AgendaCurrentListOfSpeakersOverlaySlideComponent
extends BaseSlideComponent<AgendaCurrentListOfSpeakersSlideData>
implements OnInit {
public constructor(private http: HttpService) {
super();
console.log(this.http);
}
public ngOnInit(): void {
console.log('Hello from current list of speakers overlay');
}
}

View File

@ -0,0 +1,13 @@
import { AgendaCurrentListOfSpeakersOverlaySlideModule } from './agenda-current-list-of-speakers-overlay-slide.module';
describe('AgendaCurrentListOfSpeakersOverlaySlideModule', () => {
let agendaCurrentListOfSpeakersOverlaySlideModule: AgendaCurrentListOfSpeakersOverlaySlideModule;
beforeEach(() => {
agendaCurrentListOfSpeakersOverlaySlideModule = new AgendaCurrentListOfSpeakersOverlaySlideModule();
});
it('should create an instance', () => {
expect(agendaCurrentListOfSpeakersOverlaySlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { AgendaCurrentListOfSpeakersOverlaySlideComponent } from './agenda-current-list-of-speakers-overlay-slide.component';
@NgModule(makeSlideModule(AgendaCurrentListOfSpeakersOverlaySlideComponent))
export class AgendaCurrentListOfSpeakersOverlaySlideModule {}

View File

@ -0,0 +1,3 @@
<div>
Current list of speakers slide
</div>

View File

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

View File

@ -0,0 +1,37 @@
import { Component, OnInit, Input } from '@angular/core';
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 { SlideData } from 'app/site/projector/services/projector-data.service';
@Component({
selector: 'os-agenda-current-list-of-speakers-slide',
templateUrl: './agenda-current-list-of-speakers-slide.component.html',
styleUrls: ['./agenda-current-list-of-speakers-slide.component.scss']
})
export class AgendaCurrentListOfSpeakersSlideComponent extends BaseSlideComponent<AgendaCurrentListOfSpeakersSlideData>
implements OnInit {
private _data: SlideData<AgendaCurrentListOfSpeakersSlideData>;
public isOverlay: boolean;
public get data(): SlideData<AgendaCurrentListOfSpeakersSlideData> {
return this._data;
}
@Input()
public set data(value: SlideData<AgendaCurrentListOfSpeakersSlideData>) {
this.isOverlay = !value || value.element.stable;
this._data = value;
}
public constructor(private http: HttpService) {
super();
console.log(this.http);
}
public ngOnInit(): void {
console.log('Hello from current list of speakers slide');
}
}

View File

@ -0,0 +1,13 @@
import { AgendaCurrentListOfSpeakersSlideModule } from './agenda-current-list-of-speakers-slide.module';
describe('AgendaCurrentListOfSpeakersModule', () => {
let agendaCurrentListOfSpeakersSlideModule: AgendaCurrentListOfSpeakersSlideModule;
beforeEach(() => {
agendaCurrentListOfSpeakersSlideModule = new AgendaCurrentListOfSpeakersSlideModule();
});
it('should create an instance', () => {
expect(agendaCurrentListOfSpeakersSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { AgendaCurrentListOfSpeakersSlideComponent } from './agenda-current-list-of-speakers-slide.component';
@NgModule(makeSlideModule(AgendaCurrentListOfSpeakersSlideComponent))
export class AgendaCurrentListOfSpeakersSlideModule {}

View File

@ -2,20 +2,72 @@ import { SlideManifest } from './slide-manifest';
/**
* Here, all slides has to be registered.
*
* Note: When adding or removing slides here, you may need to restart yarn/npm, because
* the angular CLI scans this file just at it's start time and creates the modules then. There
* is no such thing as "dynamic update" in this case..
*/
export const allSlides: SlideManifest[] = [
{
slideName: 'motions/motion',
slide: 'motions/motion',
path: 'motions/motion',
loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule',
scaleable: true,
scrollable: true
scrollable: true,
verboseName: 'Motion',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
},
{
slideName: 'users/user',
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
},
{
slide: 'core/clock',
path: 'core/clock',
loadChildren: './slides/core/clock/core-clock-slide.module#CoreClockSlideModule',
scaleable: false,
scrollable: false
scrollable: false,
verboseName: 'Clock',
elementIdentifiers: ['name'],
canBeMappedToModel: false
},
{
slide: 'core/countdown',
path: 'core/countdown',
loadChildren: './slides/core/countdown/core-countdown-slide.module#CoreCountdownSlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Countdown',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
},
{
slide: 'agenda/current-list-of-speakers',
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
},
{
slide: 'agenda/current-list-of-speakers-overlay',
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
}
];

View File

@ -0,0 +1,4 @@
<div id="clock" *ngIf="time">
<mat-icon>schedule</mat-icon>
<span>{{ time }}</span>
</div>

View File

@ -0,0 +1,15 @@
#clock {
position: absolute;
right: 0;
top: 0;
color: white;
height: 30px;
margin: 12px;
z-index: 2;
span {
padding-left: 5px;
font-size: 16px;
float: right;
}
}

View File

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

View File

@ -0,0 +1,44 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { ServertimeService } from 'app/core/services/servertime.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'os-core-clock-slide',
templateUrl: './core-clock-slide.component.html',
styleUrls: ['./core-clock-slide.component.scss']
})
export class CoreClockSlideComponent extends BaseSlideComponent<{}> implements OnInit, OnDestroy {
public time: string;
private servertimeSubscription: Subscription | null = null;
public constructor(private servertimeService: ServertimeService) {
super();
}
public ngOnInit(): void {
// Update clock, when the server offset changes.
this.servertimeSubscription = this.servertimeService
.getServerOffsetObservable()
.subscribe(() => this.updateClock());
// Update clock every 10 seconds.
setInterval(() => this.updateClock(), 10 * 1000);
}
private updateClock(): void {
const time = new Date(this.servertimeService.getServertime());
const hours = '0' + time.getHours();
const minutes = '0' + time.getMinutes();
// Will display time in 10:30:23 format
this.time = hours.slice(-2) + ':' + minutes.slice(-2);
}
public ngOnDestroy(): void {
if (this.servertimeSubscription) {
this.servertimeSubscription.unsubscribe();
}
}
}

View File

@ -0,0 +1,13 @@
import { CoreClockSlideModule } from './core-clock-slide.module';
describe('CoreClockSlideModule', () => {
let coreClockSlideModule: CoreClockSlideModule;
beforeEach(() => {
coreClockSlideModule = new CoreClockSlideModule();
});
it('should create an instance', () => {
expect(coreClockSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { CoreClockSlideComponent } from './core-clock-slide.component';
@NgModule(makeSlideModule(CoreClockSlideComponent))
export class CoreClockSlideModule {}

View File

@ -0,0 +1,3 @@
export interface CoreCountdownSlideData {
error: string;
}

View File

@ -0,0 +1,3 @@
<div id="outer">
COUNTDOWN
</div>

View File

@ -0,0 +1,10 @@
#outer {
position: absolute;
right: 0;
top: 0;
background-color: green;
height: 30px;
margin: 10px;
margin-top: 100px;
z-index: 2;
}

View File

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

View File

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

View File

@ -0,0 +1,13 @@
import { CoreCountdownSlideModule } from './core-countdown-slide.module';
describe('CoreCountdownSlideModule', () => {
let coreCountdownSlideModule: CoreCountdownSlideModule;
beforeEach(() => {
coreCountdownSlideModule = new CoreCountdownSlideModule();
});
it('should create an instance', () => {
expect(CoreCountdownSlideModule).toBeTruthy();
});
});

View File

@ -0,0 +1,7 @@
import { NgModule } from '@angular/core';
import { makeSlideModule } from 'app/slides/base-slide-module';
import { CoreCountdownSlideComponent } from './core-countdown-slide.component';
@NgModule(makeSlideModule(CoreCountdownSlideComponent))
export class CoreCountdownSlideModule {}

View File

@ -1,3 +1,3 @@
export interface MotionsMotionSlideData {
test: string;
title: string;
}

View File

@ -1,4 +1,4 @@
<div>
<div *ngIf="data">
Motion Slide
<h1>TEST</h1>
<h1>{{ data.data.title }}</h1>
</div>

View File

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { BaseSlideComponent } from 'app/slides/base-slide-component';
import { MotionsMotionSlideData } from './motions-motion-slide-model';
import { MotionsMotionSlideData } from './motions-motion-slide-data';
@Component({
selector: 'os-motions-motion-slide',

View File

@ -4,34 +4,36 @@ import { SlideManifest, SlideOptions } 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';
/**
* Cares about loading slides dynamically.
*/
@Injectable()
export class DynamicSlideLoader {
export class SlideManager {
private loadedSlides: { [name: string]: SlideManifest } = {};
public constructor(
@Inject(SLIDE_MANIFESTS) private manifests: SlideManifest[],
private loader: NgModuleFactoryLoader,
private injector: Injector
) {}
) {
this.manifests.forEach(slideManifest => {
this.loadedSlides[slideManifest.slide] = slideManifest;
});
}
/**
* Searches the manifest for the given slide name.
*
* TODO: Improve by loading all manifests in an object with the
* slide name as keys in the constructor. It's just a lookup here, then.
*
* @param slideName The slide to look up.
* @returns the slide's manifest.
*/
private getManifest(slideName: string): SlideManifest {
const manifest = this.manifests.find(m => m.slideName === slideName);
if (!manifest) {
if (!this.loadedSlides[slideName]) {
throw new Error(`Could not find slide for "${slideName}"`);
}
return manifest;
return this.loadedSlides[slideName];
}
/**
@ -44,6 +46,29 @@ export class DynamicSlideLoader {
return this.getManifest(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.
identifiableElement.getIdentifiers = () => identifiers;
return identifiableElement;
}
/**
* Get slide verbose name for a given slide.
*
* @param slideName The slide
* @returns the verbose slide name for the requested slide.
*/
public getSlideVerboseName(slideName: string): string {
return this.getManifest(slideName).verboseName;
}
public canSlideBeMappedToModel(slideName: string): boolean {
return this.getManifest(slideName).canBeMappedToModel;
}
/**
* Asynchronically load the slide's component factory, which is used to create
* the slide component.

View File

@ -1,4 +1,5 @@
import { InjectionToken } from '@angular/core';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
/**
* Slides can have these options.
@ -20,9 +21,12 @@ export interface SlideOptions {
* path in sync.
*/
export interface SlideManifest extends SlideOptions {
slideName: string;
slide: string;
path: string;
loadChildren: string;
verboseName: string;
elementIdentifiers: (keyof IdentifiableProjectorElement)[];
canBeMappedToModel: boolean;
}
export const SLIDE_MANIFESTS = new InjectionToken<SlideManifest[]>('SLIDE_MANIFEST');

View File

@ -2,7 +2,7 @@ import { NgModule, NgModuleFactoryLoader, SystemJsNgModuleLoader } from '@angula
import { ModuleWithProviders } from '@angular/compiler/src/core';
import { ROUTES } from '@angular/router';
import { DynamicSlideLoader } from './services/dynamic-slide-loader.service';
import { SlideManager } from './services/slide-manager.service';
import { SLIDE_MANIFESTS } from './slide-manifest';
import { allSlides } from './all-slides';
@ -15,7 +15,7 @@ import { allSlides } from './all-slides';
* found and put in sepearte chunks.
*/
@NgModule({
providers: [DynamicSlideLoader, { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }]
providers: [SlideManager, { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }]
})
export class SlidesModule {
public static forRoot(): ModuleWithProviders {

Some files were not shown because too many files have changed in this diff Show More