diff --git a/client/src/app/app.component.spec.ts b/client/src/app/app.component.spec.ts index b5d3914cb..329d14d23 100644 --- a/client/src/app/app.component.spec.ts +++ b/client/src/app/app.component.spec.ts @@ -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(); })); }); diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index b5576d7f8..17b4e0c4c 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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(); } /** diff --git a/client/src/app/core/services/autoupdate.service.ts b/client/src/app/core/services/autoupdate.service.ts index 4885c3875..ef19b017b 100644 --- a/client/src/app/core/services/autoupdate.service.ts +++ b/client/src/app/core/services/autoupdate.service.ts @@ -76,8 +76,6 @@ export class AutoupdateService extends OpenSlidesComponent { * Handles the change ids of all autoupdates. */ private async storeResponse(autoupdate: AutoupdateFormat): Promise { - console.log('got autoupdate', autoupdate); - if (autoupdate.all_data) { await this.storeAllData(autoupdate); } else { diff --git a/client/src/app/core/services/data-store.service.ts b/client/src/app/core/services/data-store.service.ts index 43594e702..772622281 100644 --- a/client/src/app/core/services/data-store.service.ts +++ b/client/src/app/core/services/data-store.service.ts @@ -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 { - 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); diff --git a/client/src/app/core/services/projection-dialog.service.ts b/client/src/app/core/services/projection-dialog.service.ts index 7a2453a60..d0e878789 100644 --- a/client/src/app/core/services/projection-dialog.service.ts +++ b/client/src/app/core/services/projection-dialog.service.ts @@ -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 { - const dialogRef = this.dialog.open( + public async openProjectDialogFor(obj: Projectable | ProjectorElementBuildDeskriptor): Promise { + 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); } } } diff --git a/client/src/app/core/services/projector.service.ts b/client/src/app/core/services/projector.service.ts index ac2be0f6c..682c03b1b 100644 --- a/client/src/app/core/services/projector.service.ts +++ b/client/src/app/core/services/projector.service.ts @@ -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('core/projector').some(projector => { - return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide()); - }); + public isProjected(obj: Projectable | ProjectorElementBuildDeskriptor): boolean { + if (isProjectable(obj)) { + return this.DS.getAll('core/projector').some(projector => { + return projector.isElementShown(obj.getSlide().getBasicProjectorElement()); + }); + } else { + return this.DS.getAll('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('core/projector').filter(projector => { - return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide()); - }); + public getProjectorsWhichAreProjecting(obj: Projectable | ProjectorElementBuildDeskriptor): Projector[] { + if (isProjectable(obj)) { + return this.DS.getAll('core/projector').filter(projector => { + return projector.isElementShown(obj.getSlide().getBasicProjectorElement()); + }); + } else { + return this.DS.getAll('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(projectors: Projector[], element: T): void { - const changedProjectors: Projector[] = []; + public projectOnMultiple(projectors: Projector[], element: IdentifiableProjectorElement): void { this.DS.getAll('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 { + 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 { + 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 { + 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(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(element.name, element.id); + } + + public async projectNextSlide(projector: Projector): Promise { + await this.projectPreviewSlide(projector, 0); + } + + public async projectPreviewSlide(projector: Projector, previewIndex: number): Promise { + 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 { + 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 { + await this.projectRequest(projector, null, projector.elements_preview); + } + + public async addElementToPreview(projector: Projector, element: ProjectorElement): Promise { + projector.elements_preview.push(element); + await this.projectRequest(projector, null, projector.elements_preview); + } } diff --git a/client/src/app/core/services/servertime.service.spec.ts b/client/src/app/core/services/servertime.service.spec.ts new file mode 100644 index 000000000..2e94c5a48 --- /dev/null +++ b/client/src/app/core/services/servertime.service.spec.ts @@ -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(); + })); +}); diff --git a/client/src/app/core/services/servertime.service.ts b/client/src/app/core/services/servertime.service.ts new file mode 100644 index 000000000..e4623e290 --- /dev/null +++ b/client/src/app/core/services/servertime.service.ts @@ -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(0); + + public constructor(private http: HttpService) { + super(); + } + + public startScheduler(): void { + this.scheduleNextRefresh(0); + } + + public getServerOffsetObservable(): Observable { + 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 { + // servertime is the time in seconds. + const servertime = await this.http.get(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(); + } +} diff --git a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.html b/client/src/app/shared/components/meta-text-block/meta-text-block.component.html similarity index 95% rename from client/src/app/site/motions/components/meta-text-block/meta-text-block.component.html rename to client/src/app/shared/components/meta-text-block/meta-text-block.component.html index 9c730a763..16bdf5e96 100644 --- a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.html +++ b/client/src/app/shared/components/meta-text-block/meta-text-block.component.html @@ -6,7 +6,7 @@
-
+
diff --git a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.scss b/client/src/app/shared/components/meta-text-block/meta-text-block.component.scss similarity index 85% rename from client/src/app/site/motions/components/meta-text-block/meta-text-block.component.scss rename to client/src/app/shared/components/meta-text-block/meta-text-block.component.scss index 964c42927..ac5cc15a9 100644 --- a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.scss +++ b/client/src/app/shared/components/meta-text-block/meta-text-block.component.scss @@ -20,6 +20,10 @@ .title-container { display: flex; justify-content: space-between; + + ::ng-deep button { + color: rgba(0, 0, 0, 0.54); + } } } } diff --git a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.spec.ts b/client/src/app/shared/components/meta-text-block/meta-text-block.component.spec.ts similarity index 88% rename from client/src/app/site/motions/components/meta-text-block/meta-text-block.component.spec.ts rename to client/src/app/shared/components/meta-text-block/meta-text-block.component.spec.ts index 1d8e68eca..e3dc79935 100644 --- a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.spec.ts +++ b/client/src/app/shared/components/meta-text-block/meta-text-block.component.spec.ts @@ -9,8 +9,7 @@ describe('MetaTextBlockComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [E2EImportsModule], - declarations: [MetaTextBlockComponent] + imports: [E2EImportsModule] }).compileComponents(); })); diff --git a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.ts b/client/src/app/shared/components/meta-text-block/meta-text-block.component.ts similarity index 71% rename from client/src/app/site/motions/components/meta-text-block/meta-text-block.component.ts rename to client/src/app/shared/components/meta-text-block/meta-text-block.component.ts index 21ebd3130..585324a6b 100644 --- a/client/src/app/site/motions/components/meta-text-block/meta-text-block.component.ts +++ b/client/src/app/shared/components/meta-text-block/meta-text-block.component.ts @@ -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(); } diff --git a/client/src/app/shared/components/projection-dialog/projection-dialog.component.html b/client/src/app/shared/components/projection-dialog/projection-dialog.component.html index 72af53280..eb3458e78 100644 --- a/client/src/app/shared/components/projection-dialog/projection-dialog.component.html +++ b/client/src/app/shared/components/projection-dialog/projection-dialog.component.html @@ -1,4 +1,4 @@ -

{{ projectable.getTitle() }}

+

{{ projectorElementBuildDescriptor.getTitle() }}

Projectors diff --git a/client/src/app/shared/components/projection-dialog/projection-dialog.component.scss b/client/src/app/shared/components/projection-dialog/projection-dialog.component.scss index e4a958438..a30f3b39e 100644 --- a/client/src/app/shared/components/projection-dialog/projection-dialog.component.scss +++ b/client/src/app/shared/components/projection-dialog/projection-dialog.component.scss @@ -4,10 +4,6 @@ mat-dialog-content { div.projectors { padding: 15px; - &.projected { - background-color: lightblue; - } - .right { float: right; } diff --git a/client/src/app/shared/components/projection-dialog/projection-dialog.component.ts b/client/src/app/shared/components/projection-dialog/projection-dialog.component.ts index 16812603b..024c3e552 100644 --- a/client/src/app/shared/components/projection-dialog/projection-dialog.component.ts +++ b/client/src/app/shared/components/projection-dialog/projection-dialog.component.ts @@ -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, - @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('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 { diff --git a/client/src/app/shared/components/projector-button/projector-button.component.ts b/client/src/app/shared/components/projector-button/projector-button.component.ts index bdb1804e7..531b0f4e2 100644 --- a/client/src/app/shared/components/projector-button/projector-button.component.ts +++ b/client/src/app/shared/components/projector-button/projector-button.component.ts @@ -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 diff --git a/client/src/app/shared/models/core/countdown.ts b/client/src/app/shared/models/core/countdown.ts index 6a2541f7b..ea6d31ae1 100644 --- a/client/src/app/shared/models/core/countdown.ts +++ b/client/src/app/shared/models/core/countdown.ts @@ -5,6 +5,8 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class Countdown extends BaseModel { + public static COLLECTIONSTRING = 'core/countdown'; + public id: number; public description: string; public default_time: number; @@ -12,7 +14,7 @@ export class Countdown extends BaseModel { public running: boolean; public constructor(input?: any) { - super('core/countdown', 'Countdown', input); + super(Countdown.COLLECTIONSTRING, 'Countdown', input); } public getTitle(): string { diff --git a/client/src/app/shared/models/core/projector-message.ts b/client/src/app/shared/models/core/projector-message.ts index 2e7690c3f..654d03408 100644 --- a/client/src/app/shared/models/core/projector-message.ts +++ b/client/src/app/shared/models/core/projector-message.ts @@ -5,11 +5,13 @@ import { BaseModel } from '../base/base-model'; * @ignore */ export class ProjectorMessage extends BaseModel { + 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 { diff --git a/client/src/app/shared/models/core/projector.ts b/client/src/app/shared/models/core/projector.ts index e0977627d..13a5f4acb 100644 --- a/client/src/app/shared/models/core/projector.ts +++ b/client/src/app/shared/models/core/projector.ts @@ -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 { 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 { } /** - * 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 { } /** - * 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(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[]] ); } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 6c24e1251..95d7c0241 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -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 }, diff --git a/client/src/app/site/base/base-projectable-model.ts b/client/src/app/site/base/base-projectable-model.ts index 4220cd5a0..9ba9367c1 100644 --- a/client/src/app/site/base/base-projectable-model.ts +++ b/client/src/app/site/base/base-projectable-model.ts @@ -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; } diff --git a/client/src/app/site/base/projectable.ts b/client/src/app/site/base/projectable.ts index 724e392e2..ba350bfa6 100644 --- a/client/src/app/site/base/projectable.ts +++ b/client/src/app/site/base/projectable.ts @@ -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 = 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 (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; } diff --git a/client/src/app/site/base/projector-options.ts b/client/src/app/site/base/projector-options.ts deleted file mode 100644 index 0a646f323..000000000 --- a/client/src/app/site/base/projector-options.ts +++ /dev/null @@ -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 = object; - return ( - option.key !== undefined && - option.displayName !== undefined && - option.default !== undefined && - (object).choices === undefined - ); -} - -export function isProjectorChoiceOption(object: any): object is ProjectorChoiceOption { - const option = object; - return ( - option.key !== undefined && - option.displayName !== undefined && - option.default !== undefined && - option.choices !== undefined - ); -} diff --git a/client/src/app/site/base/slide-options.ts b/client/src/app/site/base/slide-options.ts new file mode 100644 index 000000000..3693fb40a --- /dev/null +++ b/client/src/app/site/base/slide-options.ts @@ -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 = object; + return ( + option.key !== undefined && + option.displayName !== undefined && + option.default !== undefined && + (object).choices === undefined + ); +} + +export function isSlideChoiceOption(object: any): object is SlideChoiceOption { + const option = object; + return ( + option.key !== undefined && + option.displayName !== undefined && + option.default !== undefined && + option.choices !== undefined + ); +} diff --git a/client/src/app/site/common/common-routing.module.ts b/client/src/app/site/common/common-routing.module.ts index 91eadfc70..7134e9e48 100644 --- a/client/src/app/site/common/common-routing.module.ts +++ b/client/src/app/site/common/common-routing.module.ts @@ -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 } ]; diff --git a/client/src/app/site/common/common.module.ts b/client/src/app/site/common/common.module.ts index ece0b0067..7f0399b1a 100644 --- a/client/src/app/site/common/common.module.ts +++ b/client/src/app/site/common/common.module.ts @@ -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 {} diff --git a/client/src/app/site/common/components/countdown-list/countdown-list.component.html b/client/src/app/site/common/components/countdown-list/countdown-list.component.html new file mode 100644 index 000000000..127439691 --- /dev/null +++ b/client/src/app/site/common/components/countdown-list/countdown-list.component.html @@ -0,0 +1,92 @@ + + +
+

Countdowns

+
+
+ +
+ + New countdown + +
+

+ + + + Required + + +

+
+
+ + + + +
+ + + + + + + +
+
+ +
+
+ {{ countdown.description }} +
+
+ +
+
+
+
Edit countdown
+

+ + + + Required + + +

+
+ + TODO: Show countdown time etc. + + + + + + + +
+
+ + + +
No countdowns
+
+
diff --git a/client/src/app/site/common/components/countdown-list/countdown-list.component.scss b/client/src/app/site/common/components/countdown-list/countdown-list.component.scss new file mode 100644 index 000000000..5b401ca4c --- /dev/null +++ b/client/src/app/site/common/components/countdown-list/countdown-list.component.scss @@ -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; + } +} diff --git a/client/src/app/site/common/components/countdown-list/countdown-list.component.spec.ts b/client/src/app/site/common/components/countdown-list/countdown-list.component.spec.ts new file mode 100644 index 000000000..279699b0b --- /dev/null +++ b/client/src/app/site/common/components/countdown-list/countdown-list.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/common/components/countdown-list/countdown-list.component.ts b/client/src/app/site/common/components/countdown-list/countdown-list.component.ts new file mode 100644 index 000000000..ed4ee140e --- /dev/null +++ b/client/src/app/site/common/components/countdown-list/countdown-list.component.ts @@ -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).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 { + 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; + } +} diff --git a/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.html b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.html new file mode 100644 index 000000000..4eb85dff5 --- /dev/null +++ b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.html @@ -0,0 +1,9 @@ + + +
+

Messages

+
+
+ +
+

TODO

diff --git a/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.scss b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.spec.ts b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.spec.ts new file mode 100644 index 000000000..d9ed47bcc --- /dev/null +++ b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.ts b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.ts new file mode 100644 index 000000000..6effe7ada --- /dev/null +++ b/client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.ts @@ -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 {} +} diff --git a/client/src/app/site/common/models/view-countdown.ts b/client/src/app/site/common/models/view-countdown.ts new file mode 100644 index 000000000..b6bd8977a --- /dev/null +++ b/client/src/app/site/common/models/view-countdown.ts @@ -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() + }; + } +} diff --git a/client/src/app/site/common/models/view-projectormessage.ts b/client/src/app/site/common/models/view-projectormessage.ts new file mode 100644 index 000000000..e6afd4ea3 --- /dev/null +++ b/client/src/app/site/common/models/view-projectormessage.ts @@ -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() + }; + } +} diff --git a/client/src/app/site/common/services/countdown-repository.service.spec.ts b/client/src/app/site/common/services/countdown-repository.service.spec.ts new file mode 100644 index 000000000..50f05fd4c --- /dev/null +++ b/client/src/app/site/common/services/countdown-repository.service.spec.ts @@ -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(); + })); +}); diff --git a/client/src/app/site/common/services/countdown-repository.service.ts b/client/src/app/site/common/services/countdown-repository.service.ts new file mode 100644 index 000000000..f4e5cd785 --- /dev/null +++ b/client/src/app/site/common/services/countdown-repository.service.ts @@ -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 { + 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 { + return await this.dataSend.createModel(countdown); + } + + public async update(countdown: Partial, viewCountdown: ViewCountdown): Promise { + const update = viewCountdown.countdown; + update.patchValues(countdown); + await this.dataSend.updateModel(update); + } + + public async delete(viewCountdown: ViewCountdown): Promise { + await this.dataSend.deleteModel(viewCountdown.countdown); + } +} diff --git a/client/src/app/site/common/services/projectormessage-repository.service.spec.ts b/client/src/app/site/common/services/projectormessage-repository.service.spec.ts new file mode 100644 index 000000000..cdd7c68c2 --- /dev/null +++ b/client/src/app/site/common/services/projectormessage-repository.service.spec.ts @@ -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(); + } + )); +}); diff --git a/client/src/app/site/common/services/projectormessage-repository.service.ts b/client/src/app/site/common/services/projectormessage-repository.service.ts new file mode 100644 index 000000000..b2da51a97 --- /dev/null +++ b/client/src/app/site/common/services/projectormessage-repository.service.ts @@ -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 { + 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 { + throw new Error('TODO'); + } + + public async update(message: Partial, viewMessage: ViewProjectorMessage): Promise { + throw new Error('TODO'); + } + + public async delete(viewMessage: ViewProjectorMessage): Promise { + throw new Error('TODO'); + } +} diff --git a/client/src/app/site/motions/components/motion-comments/motion-comments.component.spec.ts b/client/src/app/site/motions/components/motion-comments/motion-comments.component.spec.ts index 861dd5610..f635f0e14 100644 --- a/client/src/app/site/motions/components/motion-comments/motion-comments.component.spec.ts +++ b/client/src/app/site/motions/components/motion-comments/motion-comments.component.spec.ts @@ -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(); })); diff --git a/client/src/app/site/motions/components/motion-poll/motion-poll.component.html b/client/src/app/site/motions/components/motion-poll/motion-poll.component.html index 49503253e..afd63e411 100644 --- a/client/src/app/site/motions/components/motion-poll/motion-poll.component.html +++ b/client/src/app/site/motions/components/motion-poll/motion-poll.component.html @@ -1,6 +1,7 @@ - Voting result  ({{ pollIndex + 1 }}) + Voting result +  ({{ pollIndex + 1 }})
@@ -51,12 +52,11 @@
- @@ -24,9 +26,8 @@ refresh -
- {{ projector?.scale }} +
{{ projector.scale }}
@@ -37,5 +38,121 @@ refresh
+
+
+ + +
+ +
+
History
+

+ {{ getElementDescription(elements[0]) }} +

+
+ +
+
Current
+ +
+

Slides

+ + + + {{ getElementDescription(element) }} + + +
+ +
+

+ Countdowns + +

+ + + + {{ countdown.description }} + + +
+ +
+

+ Messages + +

+ + + + Message {{ i + 1 }} + + +
+ +
+

Current list of speakers overlay

+ + + + Current list of speakers overlay + + +
+ +
+

Current list of speakers slide

+ + + + Current list of speakers slide + + +
+
+ +
+
Queue
+
+
+
+ unfold_more +
+
+ {{ i+1 }}. {{ getElementDescription(element) }} +
+
+
+ + +
+
+
+
+
diff --git a/client/src/app/site/projector/components/projector-detail/projector-detail.component.scss b/client/src/app/site/projector/components/projector-detail/projector-detail.component.scss index c4b643c2e..ca968d9ac 100644 --- a/client/src/app/site/projector/components/projector-detail/projector-detail.component.scss +++ b/client/src/app/site/projector/components/projector-detail/projector-detail.component.scss @@ -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; + } + } } diff --git a/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts b/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts index 3ebdd00b2..d67ce7e2e 100644 --- a/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts +++ b/client/src/app/site/projector/components/projector-detail/projector-detail.component.ts @@ -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): 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 { + 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); + } } diff --git a/client/src/app/site/projector/components/projector-list/projector-list.component.html b/client/src/app/site/projector/components/projector-list/projector-list.component.html index 26ee53bfd..af25775c3 100644 --- a/client/src/app/site/projector/components/projector-list/projector-list.component.html +++ b/client/src/app/site/projector/components/projector-list/projector-list.component.html @@ -39,13 +39,12 @@
- - - - {{ projector.name }} - - -
+
+ + + {{ projector.name | translate }} + + @@ -55,47 +54,53 @@ - -
- - -

TODO: projector

- -
-

+ + + +

+ + + - + Required -

+

Resolution and size

{{ ratio }} -

{{ updateForm.value.width }} -

- +
+ + Show clock + +
+ +
-
- + +
+ - diff --git a/client/src/app/site/projector/components/projector-list/projector-list.component.scss b/client/src/app/site/projector/components/projector-list/projector-list.component.scss index d7d6e534d..ab67177fb 100644 --- a/client/src/app/site/projector/components/projector-list/projector-list.component.scss +++ b/client/src/app/site/projector/components/projector-list/projector-list.component.scss @@ -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; + } } } diff --git a/client/src/app/site/projector/components/projector-list/projector-list.component.ts b/client/src/app/site/projector/components/projector-list/projector-list.component.ts index 0f463b5de..9dba89597 100644 --- a/client/src/app/site/projector/components/projector-list/projector-list.component.ts +++ b/client/src/app/site/projector/components/projector-list/projector-list.component.ts @@ -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 { 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); + } } /** diff --git a/client/src/app/site/projector/components/projector/projector.component.scss b/client/src/app/site/projector/components/projector/projector.component.scss index c5c4d1613..8526f1c44 100644 --- a/client/src/app/site/projector/components/projector/projector.component.scss +++ b/client/src/app/site/projector/components/projector/projector.component.scss @@ -20,6 +20,7 @@ } .content { width: 100%; + height: 100%; position: absolute; top: 0; left: 0; diff --git a/client/src/app/site/projector/components/projector/projector.component.ts b/client/src/app/site/projector/components/projector/projector.component.ts index b07609381..5a11e4eb4 100644 --- a/client/src/app/site/projector/components/projector/projector.component.ts +++ b/client/src/app/site/projector/components/projector/projector.component.ts @@ -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 diff --git a/client/src/app/site/projector/components/slide-container/slide-container.component.scss b/client/src/app/site/projector/components/slide-container/slide-container.component.scss index 73841039a..0e8c9aad9 100644 --- a/client/src/app/site/projector/components/slide-container/slide-container.component.scss +++ b/client/src/app/site/projector/components/slide-container/slide-container.component.scss @@ -1,3 +1,4 @@ ::ng-deep #slide { z-index: 5; + height: 100%; } diff --git a/client/src/app/site/projector/components/slide-container/slide-container.component.ts b/client/src/app/site/projector/components/slide-container/slide-container.component.ts index 8c0549230..c73ab9bee 100644 --- a/client/src/app/site/projector/components/slide-container/slide-container.component.ts +++ b/client/src/app/site/projector/components/slide-container/slide-container.component.ts @@ -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(); diff --git a/client/src/app/site/projector/models/view-projector.ts b/client/src/app/site/projector/models/view-projector.ts index 14c93483e..99fa9f310 100644 --- a/client/src/app/site/projector/models/view-projector.ts +++ b/client/src/app/site/projector/models/view-projector.ts @@ -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; } diff --git a/client/src/app/site/projector/projector-routing.module.ts b/client/src/app/site/projector/projector-routing.module.ts index 133f0cd7b..26f7c6f72 100644 --- a/client/src/app/site/projector/projector-routing.module.ts +++ b/client/src/app/site/projector/projector-routing.module.ts @@ -5,7 +5,7 @@ import { ProjectorDetailComponent } from './components/projector-detail/projecto const routes: Routes = [ { - path: 'list', + path: '', component: ProjectorListComponent }, { diff --git a/client/src/app/site/projector/projector.config.ts b/client/src/app/site/projector/projector.config.ts index fb23de7d6..eeb1797b6 100644 --- a/client/src/app/site/projector/projector.config.ts +++ b/client/src/app/site/projector/projector.config.ts @@ -12,7 +12,7 @@ export const ProjectorAppConfig: AppConfig = { ], mainMenuEntries: [ { - route: '/projector-site/list', + route: '/projectors', displayName: 'Projector', icon: 'videocam', weight: 700, diff --git a/client/src/app/site/projector/projector.module.ts b/client/src/app/site/projector/projector.module.ts index 1150e90ef..2b19be505 100644 --- a/client/src/app/site/projector/projector.module.ts +++ b/client/src/app/site/projector/projector.module.ts @@ -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, diff --git a/client/src/app/site/projector/services/clock-slide.service.ts b/client/src/app/site/projector/services/clock-slide.service.ts new file mode 100644 index 000000000..d82e91737 --- /dev/null +++ b/client/src/app/site/projector/services/clock-slide.service.ts @@ -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 { + 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()); + } + } +} diff --git a/client/src/app/site/projector/services/current-list-of-of-speakers-slide.service.ts b/client/src/app/site/projector/services/current-list-of-of-speakers-slide.service.ts new file mode 100644 index 000000000..a86229739 --- /dev/null +++ b/client/src/app/site/projector/services/current-list-of-of-speakers-slide.service.ts @@ -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 { + 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) + ); + } + } +} diff --git a/client/src/app/site/projector/services/projector-data.service.ts b/client/src/app/site/projector/services/projector-data.service.ts index d0474981f..3268cc8bf 100644 --- a/client/src/app/site/projector/services/projector-data.service.ts +++ b/client/src/app/site/projector/services/projector-data.service.ts @@ -12,7 +12,7 @@ export interface SlideData { 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); + } } }); }); diff --git a/client/src/app/site/projector/services/projector-repository.service.ts b/client/src/app/site/projector/services/projector-repository.service.ts index 062ccb9c3..dbb9661e7 100644 --- a/client/src/app/site/projector/services/projector-repository.service.ts +++ b/client/src/app/site/projector/services/projector-repository.service.ts @@ -82,7 +82,7 @@ export class ProjectorRepositoryService extends BaseRepository { - this.controlView(projector, direction, 'scroll'); + await this.controlView(projector, direction, 'scroll'); } /** @@ -92,7 +92,7 @@ export class ProjectorRepositoryService extends BaseRepository { - this.controlView(projector, direction, 'scale'); + await this.controlView(projector, direction, 'scale'); } /** @@ -107,7 +107,7 @@ export class ProjectorRepositoryService extends BaseRepository { - this.http.post(`/rest/core/projector/${projector.id}/control_view`, { + await this.http.post(`/rest/core/projector/${projector.id}/control_view/`, { action: action, direction: direction }); diff --git a/client/src/app/site/site-routing.module.ts b/client/src/app/site/site-routing.module.ts index 47a0d7aab..1e884a909 100644 --- a/client/src/app/site/site-routing.module.ts +++ b/client/src/app/site/site-routing.module.ts @@ -52,7 +52,7 @@ const routes: Routes = [ loadChildren: './history/history.module#HistoryModule' }, { - path: 'projector-site', + path: 'projectors', loadChildren: './projector/projector.module#ProjectorModule' } ], diff --git a/client/src/app/site/users/models/view-user.ts b/client/src/app/site/users/models/view-user.ts index b4c49b6d7..eae8dbe23 100644 --- a/client/src/app/site/users/models/view-user.ts +++ b/client/src/app/site/users/models/view-user.ts @@ -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() + }; } /** diff --git a/client/src/app/slides/agenda/base/agenda-current-list-of-speakers-slide-data.ts b/client/src/app/slides/agenda/base/agenda-current-list-of-speakers-slide-data.ts new file mode 100644 index 000000000..b6caa2101 --- /dev/null +++ b/client/src/app/slides/agenda/base/agenda-current-list-of-speakers-slide-data.ts @@ -0,0 +1,3 @@ +export interface AgendaCurrentListOfSpeakersSlideData { + error: string; +} diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.html b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.html new file mode 100644 index 000000000..a6a56beac --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.html @@ -0,0 +1,3 @@ +
+ Current list of speakers overlay +
diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.scss b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.scss new file mode 100644 index 000000000..91d478884 --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.scss @@ -0,0 +1,9 @@ +#overlay { + position: absolute; + right: 0; + bottom: 0; + background-color: green; + height: 30px; + margin: 10px; + z-index: 2; +} diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.spec.ts b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.spec.ts new file mode 100644 index 000000000..f8b0ddcaa --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.ts b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.ts new file mode 100644 index 000000000..fec60e277 --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.ts @@ -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 + implements OnInit { + public constructor(private http: HttpService) { + super(); + console.log(this.http); + } + + public ngOnInit(): void { + console.log('Hello from current list of speakers overlay'); + } +} diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.spec.ts b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.spec.ts new file mode 100644 index 000000000..6a55a7cdd --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.spec.ts @@ -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(); + }); +}); diff --git a/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.ts b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.ts new file mode 100644 index 000000000..a6111807e --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.ts @@ -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 {} diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.html b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.html new file mode 100644 index 000000000..cef78fcdc --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.html @@ -0,0 +1,3 @@ +
+ Current list of speakers slide +
diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.scss b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.spec.ts b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.spec.ts new file mode 100644 index 000000000..97f34678a --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.ts b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.ts new file mode 100644 index 000000000..df9b2bc0f --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.ts @@ -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 + implements OnInit { + private _data: SlideData; + + public isOverlay: boolean; + + public get data(): SlideData { + return this._data; + } + + @Input() + public set data(value: SlideData) { + 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'); + } +} diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.spec.ts b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.spec.ts new file mode 100644 index 000000000..87e5349c5 --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.spec.ts @@ -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(); + }); +}); diff --git a/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.ts b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.ts new file mode 100644 index 000000000..fbb3075db --- /dev/null +++ b/client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.ts @@ -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 {} diff --git a/client/src/app/slides/all-slides.ts b/client/src/app/slides/all-slides.ts index d24017c39..dddc056df 100644 --- a/client/src/app/slides/all-slides.ts +++ b/client/src/app/slides/all-slides.ts @@ -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 } ]; diff --git a/client/src/app/slides/core/clock/core-clock-slide.component.html b/client/src/app/slides/core/clock/core-clock-slide.component.html new file mode 100644 index 000000000..b4775c55a --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.component.html @@ -0,0 +1,4 @@ +
+ schedule + {{ time }} +
diff --git a/client/src/app/slides/core/clock/core-clock-slide.component.scss b/client/src/app/slides/core/clock/core-clock-slide.component.scss new file mode 100644 index 000000000..fea915978 --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.component.scss @@ -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; + } +} diff --git a/client/src/app/slides/core/clock/core-clock-slide.component.spec.ts b/client/src/app/slides/core/clock/core-clock-slide.component.spec.ts new file mode 100644 index 000000000..5db866409 --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/slides/core/clock/core-clock-slide.component.ts b/client/src/app/slides/core/clock/core-clock-slide.component.ts new file mode 100644 index 000000000..88663d7d3 --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.component.ts @@ -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(); + } + } +} diff --git a/client/src/app/slides/core/clock/core-clock-slide.module.spec.ts b/client/src/app/slides/core/clock/core-clock-slide.module.spec.ts new file mode 100644 index 000000000..6a8c23c3c --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.module.spec.ts @@ -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(); + }); +}); diff --git a/client/src/app/slides/core/clock/core-clock-slide.module.ts b/client/src/app/slides/core/clock/core-clock-slide.module.ts new file mode 100644 index 000000000..8fbd78589 --- /dev/null +++ b/client/src/app/slides/core/clock/core-clock-slide.module.ts @@ -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 {} diff --git a/client/src/app/slides/core/countdown/core-countdown-slide-data.ts b/client/src/app/slides/core/countdown/core-countdown-slide-data.ts new file mode 100644 index 000000000..b5e45a00f --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide-data.ts @@ -0,0 +1,3 @@ +export interface CoreCountdownSlideData { + error: string; +} diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.component.html b/client/src/app/slides/core/countdown/core-countdown-slide.component.html new file mode 100644 index 000000000..3d7ef2c31 --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.component.html @@ -0,0 +1,3 @@ +
+ COUNTDOWN +
diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.component.scss b/client/src/app/slides/core/countdown/core-countdown-slide.component.scss new file mode 100644 index 000000000..8144d80b6 --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.component.scss @@ -0,0 +1,10 @@ +#outer { + position: absolute; + right: 0; + top: 0; + background-color: green; + height: 30px; + margin: 10px; + margin-top: 100px; + z-index: 2; +} diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.component.spec.ts b/client/src/app/slides/core/countdown/core-countdown-slide.component.spec.ts new file mode 100644 index 000000000..387a6cdea --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.component.ts b/client/src/app/slides/core/countdown/core-countdown-slide.component.ts new file mode 100644 index 000000000..5f1eb3250 --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.component.ts @@ -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 implements OnInit { + public constructor(private http: HttpService) { + super(); + console.log(this.http); + } + + public ngOnInit(): void { + console.log('Hello from countdown slide'); + } +} diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.module.spec.ts b/client/src/app/slides/core/countdown/core-countdown-slide.module.spec.ts new file mode 100644 index 000000000..7e56b05b0 --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.module.spec.ts @@ -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(); + }); +}); diff --git a/client/src/app/slides/core/countdown/core-countdown-slide.module.ts b/client/src/app/slides/core/countdown/core-countdown-slide.module.ts new file mode 100644 index 000000000..c2c870f5e --- /dev/null +++ b/client/src/app/slides/core/countdown/core-countdown-slide.module.ts @@ -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 {} diff --git a/client/src/app/slides/motions/motion/motions-motion-slide-model.ts b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts similarity index 69% rename from client/src/app/slides/motions/motion/motions-motion-slide-model.ts rename to client/src/app/slides/motions/motion/motions-motion-slide-data.ts index 9de28ec8a..9de7b2f40 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide-model.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts @@ -1,3 +1,3 @@ export interface MotionsMotionSlideData { - test: string; + title: string; } diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.html b/client/src/app/slides/motions/motion/motions-motion-slide.component.html index dd99a6121..73d736376 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.html +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.html @@ -1,4 +1,4 @@ -
+
Motion Slide -

TEST

+

{{ data.data.title }}

diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts index a283b38f5..c3b472a8b 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.ts @@ -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', diff --git a/client/src/app/slides/services/dynamic-slide-loader.service.ts b/client/src/app/slides/services/slide-manager.service.ts similarity index 65% rename from client/src/app/slides/services/dynamic-slide-loader.service.ts rename to client/src/app/slides/services/slide-manager.service.ts index 141b16037..82ef522ab 100644 --- a/client/src/app/slides/services/dynamic-slide-loader.service.ts +++ b/client/src/app/slides/services/slide-manager.service.ts @@ -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. diff --git a/client/src/app/slides/slide-manifest.ts b/client/src/app/slides/slide-manifest.ts index cf0f78fd4..15564144d 100644 --- a/client/src/app/slides/slide-manifest.ts +++ b/client/src/app/slides/slide-manifest.ts @@ -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('SLIDE_MANIFEST'); diff --git a/client/src/app/slides/slides.module.ts b/client/src/app/slides/slides.module.ts index 7c1ef361a..23933c44f 100644 --- a/client/src/app/slides/slides.module.ts +++ b/client/src/app/slides/slides.module.ts @@ -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 { diff --git a/client/src/app/slides/users/user/users-user-slide-model.ts b/client/src/app/slides/users/user/users-user-slide-data.ts similarity index 100% rename from client/src/app/slides/users/user/users-user-slide-model.ts rename to client/src/app/slides/users/user/users-user-slide-data.ts diff --git a/client/src/app/slides/users/user/users-user-slide.component.ts b/client/src/app/slides/users/user/users-user-slide.component.ts index 846827ee4..33a6d43a2 100644 --- a/client/src/app/slides/users/user/users-user-slide.component.ts +++ b/client/src/app/slides/users/user/users-user-slide.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { BaseSlideComponent } from 'app/slides/base-slide-component'; -import { UsersUserSlideData } from './users-user-slide-model'; +import { UsersUserSlideData } from './users-user-slide-data'; @Component({ selector: 'os-users-user-slide', diff --git a/client/src/styles.scss b/client/src/styles.scss index 9b2da8059..09625db31 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -65,6 +65,10 @@ h4 { font-size: 12px; color: rgba(0, 0, 0, 0.54); margin-bottom: 5px; + + .mat-icon-button mat-icon { + font-size: 16px; + } } .small { @@ -277,9 +281,10 @@ mat-expansion-panel { border-radius: 0 !important; } -mat-panel-title mat-icon { +/* This destroys the projector button in list views using extension panels. */ +/*mat-panel-title mat-icon { padding-right: 30px; -} +}*/ .hidden-cell { flex: 0; @@ -471,6 +476,10 @@ button.mat-menu-item.selected { color: rgba(0, 0, 0, 0.87) !important; } +.projected { + background-color: lightblue; +} + /* TODO: move to site.component.scss-theme.scss (does not work currently) */ /* make the .user-menu expansion panel look like the nav-toolbar above */ diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index d27b91f56..3b0c72321 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -90,6 +90,22 @@ def list_of_speakers_slide( return {"user_ids": user_ids} +def current_list_of_speakers_slide( + all_data: AllData, element: Dict[str, Any] +) -> Dict[str, Any]: + """ + TODO + + Note: This data is for all projectors showing this slide, so we cannot give projector- + specific data. The work-around is to make a dict with projector-ids as keys and the + data-per-projector as values. This is not a security concern, because if a person can + see one projector, he is able to see all others, too. Maybe a bit more data.. + """ + return {"error": "TODO"} + + def register_projector_slides() -> None: register_projector_slide("agenda/item-list", items_slide) register_projector_slide("agenda/list-of-speakers", list_of_speakers_slide) + register_projector_slide("agenda/current-list-of-speakers", current_list_of_speakers_slide) + register_projector_element("agenda/current-list-of-speakers-overlay", current_list_of_speakers_slide) diff --git a/openslides/core/projector.py b/openslides/core/projector.py index bd4f07cba..ac814b363 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -47,7 +47,11 @@ def message_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: return {"error": f"Message {message_id} does not exist"} +def clock_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: + return {} + + def register_projector_slides() -> None: register_projector_slide("core/countdown", countdown_slide) register_projector_slide("core/projector-message", message_slide) - # TODO: Add clock slide + register_projector_element("core/clock", clock_slide) diff --git a/openslides/core/serializers.py b/openslides/core/serializers.py index 69e13827c..5cf60a74a 100644 --- a/openslides/core/serializers.py +++ b/openslides/core/serializers.py @@ -78,7 +78,7 @@ class ProjectorSerializer(ModelSerializer): """ elements = JSONSerializerField(validators=[elements_validator]) - elements_preview = JSONSerializerField(validators=[elements_array_validator]) + elements_preview = JSONSerializerField(validators=[elements_validator]) elements_history = JSONSerializerField(validators=[elements_array_validator]) projectiondefaults = ProjectionDefaultSerializer(many=True, read_only=True) diff --git a/openslides/core/views.py b/openslides/core/views.py index aa5b723cd..278fe053d 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -164,20 +164,28 @@ class ProjectorViewSet(ModelViewSet): `append_to_history` adds one element to the end of the history_elements. `elements` and `preview` preplaces the coresponding fields in the database. + + If `delete_last_history_element` is True, the last element is deleted. + Note: You cannot give `append_to_history` and `delete_last_history_element` + at the same time. """ projector = self.get_object() elements = request.data.get("elements") preview = request.data.get("preview") history_element = request.data.get("append_to_history") + delete_last_history_element = request.data.get("delete_last_history_element", False) changed_data = {} if elements is not None: changed_data["elements"] = elements if preview is not None: changed_data["elements_preview"] = preview - if history_element is not None: + if history_element is not None and delete_last_history_element is False: history = projector.elements_history + [history_element] changed_data["elements_history"] = history + if history_element is None and delete_last_history_element is True: + history = projector.elements_history[:-1] + changed_data["elements_history"] = history serializer = self.get_serializer(projector, data=changed_data, partial=True) serializer.is_valid(raise_exception=True) diff --git a/tests/integration/core/test_views.py b/tests/integration/core/test_views.py index ff1ea817d..27a996ce7 100644 --- a/tests/integration/core/test_views.py +++ b/tests/integration/core/test_views.py @@ -90,7 +90,7 @@ def test_project_view(client): { "append_to_history": [{"name": "topics/topic", "id": 1}], "elements": [{"name": "topics/topic", "id": 2}], - "preview": [[{"name": "topics/topic", "id": 3}]], + "preview": [{"name": "topics/topic", "id": 3}], }, content_type="application/json", ) @@ -102,7 +102,7 @@ def test_project_view(client): [{"name": "topics/topic", "id": 3}], [{"name": "topics/topic", "id": 1}], ] - assert projector.elements_preview == [[{"name": "topics/topic", "id": 3}]] + assert projector.elements_preview == [{"name": "topics/topic", "id": 3}] @pytest.mark.django_db(transaction=False)