From 965d23be50caccd7302e29ff9f17f11cda6bf6b2 Mon Sep 17 00:00:00 2001 From: FinnStutzenstein Date: Thu, 24 Jan 2019 16:25:50 +0100 Subject: [PATCH 1/2] more work on projector, countdowns, clos - splitted clos-slide and clos-overlay. - Synchronize to server, more little changes --- client/src/app/app.component.spec.ts | 8 + client/src/app/app.component.ts | 6 +- .../app/core/services/autoupdate.service.ts | 2 - .../app/core/services/data-store.service.ts | 1 - .../services/projection-dialog.service.ts | 28 ++- .../app/core/services/projector.service.ts | 210 +++++++++++++++--- .../core/services/servertime.service.spec.ts | 17 ++ .../app/core/services/servertime.service.ts | 58 +++++ .../meta-text-block.component.html | 2 +- .../meta-text-block.component.scss | 4 + .../meta-text-block.component.spec.ts | 3 +- .../meta-text-block.component.ts | 7 +- .../projection-dialog.component.html | 2 +- .../projection-dialog.component.scss | 4 - .../projection-dialog.component.ts | 62 +++--- .../projector-button.component.ts | 4 +- .../src/app/shared/models/core/countdown.ts | 4 +- .../shared/models/core/projector-message.ts | 4 +- .../src/app/shared/models/core/projector.ts | 61 +++-- client/src/app/shared/shared.module.ts | 7 +- .../app/site/base/base-projectable-model.ts | 42 +--- client/src/app/site/base/projectable.ts | 52 ++--- client/src/app/site/base/projector-options.ts | 32 --- client/src/app/site/base/slide-options.ts | 32 +++ .../app/site/common/common-routing.module.ts | 10 + client/src/app/site/common/common.module.ts | 13 +- .../countdown-list.component.html | 92 ++++++++ .../countdown-list.component.scss | 34 +++ .../countdown-list.component.spec.ts | 26 +++ .../countdown-list.component.ts | 188 ++++++++++++++++ .../projectormessage-list.component.html | 9 + .../projectormessage-list.component.scss | 0 .../projectormessage-list.component.spec.ts | 26 +++ .../projectormessage-list.component.ts | 32 +++ .../app/site/common/models/view-countdown.ts | 46 ++++ .../common/models/view-projectormessage.ts | 46 ++++ .../countdown-repository.service.spec.ts | 17 ++ .../services/countdown-repository.service.ts | 39 ++++ ...rojectormessage-repository.service.spec.ts | 20 ++ .../projectormessage-repository.service.ts | 32 +++ .../motion-comments.component.spec.ts | 3 +- .../motion-poll/motion-poll.component.html | 7 +- .../motion-poll/motion-poll.component.scss | 1 + .../personal-note.component.spec.ts | 3 +- .../app/site/motions/models/view-motion.ts | 47 ++-- client/src/app/site/motions/motions.module.ts | 3 - .../projector-detail.component.html | 131 ++++++++++- .../projector-detail.component.scss | 79 ++++++- .../projector-detail.component.ts | 85 ++++++- .../projector-list.component.html | 57 ++--- .../projector-list.component.scss | 27 ++- .../projector-list.component.ts | 29 ++- .../projector/projector.component.scss | 1 + .../projector/projector.component.ts | 6 +- .../slide-container.component.scss | 1 + .../slide-container.component.ts | 10 +- .../site/projector/models/view-projector.ts | 12 + .../projector/projector-routing.module.ts | 2 +- .../app/site/projector/projector.config.ts | 2 +- .../app/site/projector/projector.module.ts | 4 + .../projector/services/clock-slide.service.ts | 35 +++ ...rrent-list-of-of-speakers-slide.service.ts | 43 ++++ .../services/projector-data.service.ts | 10 +- .../services/projector-repository.service.ts | 6 +- client/src/app/site/site-routing.module.ts | 2 +- client/src/app/site/users/models/view-user.ts | 22 +- ...nda-current-list-of-speakers-slide-data.ts | 3 + ...t-of-speakers-overlay-slide.component.html | 3 + ...t-of-speakers-overlay-slide.component.scss | 9 + ...f-speakers-overlay-slide.component.spec.ts | 26 +++ ...ist-of-speakers-overlay-slide.component.ts | 23 ++ ...t-of-speakers-overlay-slide.module.spec.ts | 13 ++ ...t-list-of-speakers-overlay-slide.module.ts | 7 + ...rent-list-of-speakers-slide.component.html | 3 + ...rent-list-of-speakers-slide.component.scss | 0 ...t-list-of-speakers-slide.component.spec.ts | 26 +++ ...urrent-list-of-speakers-slide.component.ts | 37 +++ ...rent-list-of-speakers-slide.module.spec.ts | 13 ++ ...a-current-list-of-speakers-slide.module.ts | 7 + client/src/app/slides/all-slides.ts | 60 ++++- .../clock/core-clock-slide.component.html | 4 + .../clock/core-clock-slide.component.scss | 15 ++ .../clock/core-clock-slide.component.spec.ts | 26 +++ .../core/clock/core-clock-slide.component.ts | 44 ++++ .../clock/core-clock-slide.module.spec.ts | 13 ++ .../core/clock/core-clock-slide.module.ts | 7 + .../countdown/core-countdown-slide-data.ts | 3 + .../core-countdown-slide.component.html | 3 + .../core-countdown-slide.component.scss | 10 + .../core-countdown-slide.component.spec.ts | 26 +++ .../core-countdown-slide.component.ts | 20 ++ .../core-countdown-slide.module.spec.ts | 13 ++ .../countdown/core-countdown-slide.module.ts | 7 + ...-model.ts => motions-motion-slide-data.ts} | 2 +- .../motions-motion-slide.component.html | 4 +- .../motion/motions-motion-slide.component.ts | 2 +- ...er.service.ts => slide-manager.service.ts} | 43 +++- client/src/app/slides/slide-manifest.ts | 6 +- client/src/app/slides/slides.module.ts | 4 +- ...lide-model.ts => users-user-slide-data.ts} | 0 .../users/user/users-user-slide.component.ts | 2 +- client/src/styles.scss | 13 +- openslides/agenda/projector.py | 16 ++ openslides/core/projector.py | 6 +- openslides/core/serializers.py | 2 +- openslides/core/views.py | 10 +- tests/integration/core/test_views.py | 4 +- 107 files changed, 2102 insertions(+), 343 deletions(-) create mode 100644 client/src/app/core/services/servertime.service.spec.ts create mode 100644 client/src/app/core/services/servertime.service.ts rename client/src/app/{site/motions => shared}/components/meta-text-block/meta-text-block.component.html (95%) rename client/src/app/{site/motions => shared}/components/meta-text-block/meta-text-block.component.scss (85%) rename client/src/app/{site/motions => shared}/components/meta-text-block/meta-text-block.component.spec.ts (88%) rename client/src/app/{site/motions => shared}/components/meta-text-block/meta-text-block.component.ts (71%) delete mode 100644 client/src/app/site/base/projector-options.ts create mode 100644 client/src/app/site/base/slide-options.ts create mode 100644 client/src/app/site/common/components/countdown-list/countdown-list.component.html create mode 100644 client/src/app/site/common/components/countdown-list/countdown-list.component.scss create mode 100644 client/src/app/site/common/components/countdown-list/countdown-list.component.spec.ts create mode 100644 client/src/app/site/common/components/countdown-list/countdown-list.component.ts create mode 100644 client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.html create mode 100644 client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.scss create mode 100644 client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.spec.ts create mode 100644 client/src/app/site/common/components/projectormessage-list/projectormessage-list.component.ts create mode 100644 client/src/app/site/common/models/view-countdown.ts create mode 100644 client/src/app/site/common/models/view-projectormessage.ts create mode 100644 client/src/app/site/common/services/countdown-repository.service.spec.ts create mode 100644 client/src/app/site/common/services/countdown-repository.service.ts create mode 100644 client/src/app/site/common/services/projectormessage-repository.service.spec.ts create mode 100644 client/src/app/site/common/services/projectormessage-repository.service.ts create mode 100644 client/src/app/site/projector/services/clock-slide.service.ts create mode 100644 client/src/app/site/projector/services/current-list-of-of-speakers-slide.service.ts create mode 100644 client/src/app/slides/agenda/base/agenda-current-list-of-speakers-slide-data.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.html create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.scss create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.spec.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.component.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.spec.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.html create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.scss create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.spec.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.component.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.spec.ts create mode 100644 client/src/app/slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module.ts create mode 100644 client/src/app/slides/core/clock/core-clock-slide.component.html create mode 100644 client/src/app/slides/core/clock/core-clock-slide.component.scss create mode 100644 client/src/app/slides/core/clock/core-clock-slide.component.spec.ts create mode 100644 client/src/app/slides/core/clock/core-clock-slide.component.ts create mode 100644 client/src/app/slides/core/clock/core-clock-slide.module.spec.ts create mode 100644 client/src/app/slides/core/clock/core-clock-slide.module.ts create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide-data.ts create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.component.html create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.component.scss create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.component.spec.ts create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.component.ts create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.module.spec.ts create mode 100644 client/src/app/slides/core/countdown/core-countdown-slide.module.ts rename client/src/app/slides/motions/motion/{motions-motion-slide-model.ts => motions-motion-slide-data.ts} (69%) rename client/src/app/slides/services/{dynamic-slide-loader.service.ts => slide-manager.service.ts} (65%) rename client/src/app/slides/users/user/{users-user-slide-model.ts => users-user-slide-data.ts} (100%) 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) From 42f8b74d8e4ba4383abc2b91d2c9467447dfc60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Sch=C3=BCtze?= Date: Sat, 26 Jan 2019 20:37:49 +0100 Subject: [PATCH 2/2] Projector templates - Projector base template - Projection button - projection dialog - motion slide (template and motions/projector.py) - user slide (template and users/projector.py) - motion and user list view with projector column permission check. --- .../services/projection-dialog.service.ts | 1 - .../projection-dialog.component.html | 37 +++++------ .../projection-dialog.component.scss | 7 +-- .../projector-button.component.html | 3 +- .../projector-button.component.scss | 8 +++ .../projector-button.component.ts | 19 +++++- .../mediafile-list.component.ts | 4 +- .../motion-list/motion-list.component.html | 13 ++-- .../motion-list/motion-list.component.ts | 27 ++++---- .../app/site/motions/models/view-motion.ts | 4 +- .../projector/projector.component.html | 21 +++++-- .../projector/projector.component.scss | 63 ++++++++++++++++--- .../projector/projector.component.ts | 8 +++ .../slide-container.component.scss | 3 + .../slide-container.component.ts | 4 +- .../user-list/user-list.component.html | 10 +-- .../user-list/user-list.component.scss | 4 ++ .../user-list/user-list.component.ts | 27 +++++++- .../clock/core-clock-slide.component.scss | 8 ++- .../motion/motions-motion-slide-data.ts | 12 ++++ .../motions-motion-slide.component.html | 35 ++++++++++- .../motions-motion-slide.component.scss | 16 ++++- .../users/user/users-user-slide-data.ts | 2 +- .../user/users-user-slide.component.html | 4 +- client/src/styles.scss | 58 +++++++++++++++-- openslides/agenda/projector.py | 8 ++- openslides/core/projector.py | 2 +- openslides/core/views.py | 4 +- openslides/motions/projector.py | 22 ++----- openslides/users/projector.py | 22 ++++++- tests/unit/motions/test_projector.py | 2 - 31 files changed, 347 insertions(+), 111 deletions(-) diff --git a/client/src/app/core/services/projection-dialog.service.ts b/client/src/app/core/services/projection-dialog.service.ts index d0e878789..33a6448e2 100644 --- a/client/src/app/core/services/projection-dialog.service.ts +++ b/client/src/app/core/services/projection-dialog.service.ts @@ -45,7 +45,6 @@ export class ProjectionDialogService extends OpenSlidesComponent { ProjectorElementBuildDeskriptor, ProjectionDialogReturnType >(ProjectionDialogComponent, { - minWidth: '500px', maxHeight: '90vh', data: descriptor }); 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 eb3458e78..2ef53e9ad 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,23 +1,19 @@ -

{{ projectorElementBuildDescriptor.getTitle() }}

+

Project + Motion + {{ projectorElementBuildDescriptor.getTitle() }}?

- - Projectors - -
- - {{ projector.name | translate }} - - - videocam - -
-
-
- - - Slide options - - +
+ + {{ projector.name | translate }} + + + videocam + +
+ + + +
@@ -33,8 +29,7 @@
- - +
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 a30f3b39e..4f28b3d89 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 @@ -1,15 +1,12 @@ mat-dialog-content { overflow: inherit; + min-width: auto; div.projectors { - padding: 15px; + padding: 15px 0; .right { float: right; } } - - mat-card { - margin-bottom: 10px; - } } diff --git a/client/src/app/shared/components/projector-button/projector-button.component.html b/client/src/app/shared/components/projector-button/projector-button.component.html index ac70297d1..2ae95b6d4 100644 --- a/client/src/app/shared/components/projector-button/projector-button.component.html +++ b/client/src/app/shared/components/projector-button/projector-button.component.html @@ -1,3 +1,4 @@ - diff --git a/client/src/app/shared/components/projector-button/projector-button.component.scss b/client/src/app/shared/components/projector-button/projector-button.component.scss index e69de29bb..afe39ac65 100644 --- a/client/src/app/shared/components/projector-button/projector-button.component.scss +++ b/client/src/app/shared/components/projector-button/projector-button.component.scss @@ -0,0 +1,8 @@ +.projectorbutton-active { + color: white !important; +} + +.projectorbutton-inactive { + background-color: white; + color: grey; +} 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 531b0f4e2..4696d9d04 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,6 +1,7 @@ import { Component, OnInit, Input } from '@angular/core'; import { Projectable, ProjectorElementBuildDeskriptor } from 'app/site/base/projectable'; import { ProjectionDialogService } from 'app/core/services/projection-dialog.service'; +import { ProjectorService } from '../../../core/services/projector.service'; /** */ @@ -16,7 +17,10 @@ export class ProjectorButtonComponent implements OnInit { /** * The consotructor */ - public constructor(private projectionDialogService: ProjectionDialogService) {} + public constructor( + private projectionDialogService: ProjectionDialogService, + private projectorService: ProjectorService + ) {} /** * Initialization function @@ -27,4 +31,17 @@ export class ProjectorButtonComponent implements OnInit { event.stopPropagation(); this.projectionDialogService.openProjectDialogFor(this.object); } + + /** + * + * + * @returns true, if the object is projected on one projector. + */ + public isProjected(): boolean { + if (this.object) { + return this.projectorService.isProjected(this.object); + } else { + return false; + } + } } diff --git a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts index 455a27c84..81cbea14a 100644 --- a/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts +++ b/client/src/app/site/mediafiles/components/mediafile-list/mediafile-list.component.ts @@ -36,12 +36,12 @@ export class MediafileListComponent extends ListViewBaseComponent public fontActions: string[]; /** - * Columns to display in Mediafile table when fill width is available + * Columns to display in Mediafile table when desktop view is available */ public displayedColumnsDesktop: string[] = ['title', 'info', 'indicator', 'menu']; /** - * Columns to display in Mediafile table when fill width is available + * Columns to display in Mediafile table when mobile view is available */ public displayedColumnsMobile: string[] = ['title', 'menu']; diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.html b/client/src/app/site/motions/components/motion-list/motion-list.component.html index 329c1526e..95fdf776a 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.html +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.html @@ -27,16 +27,16 @@ - - + + {{ isSelected(motion) ? 'check_circle' : '' }} - Projector - + Projector + @@ -84,13 +84,14 @@ grey: motion.state.css_class === 'default', lightblue: motion.state.css_class === 'primary' }" + [disabled]="true" > {{ getStateLabel(motion) }} - - {{ getRecommendationLabel(motion) }} + + {{ getRecommendationLabel(motion) }}
diff --git a/client/src/app/site/motions/components/motion-list/motion-list.component.ts b/client/src/app/site/motions/components/motion-list/motion-list.component.ts index ce15e2cd4..1f1f65208 100644 --- a/client/src/app/site/motions/components/motion-list/motion-list.component.ts +++ b/client/src/app/site/motions/components/motion-list/motion-list.component.ts @@ -25,6 +25,8 @@ import { WorkflowState } from '../../../../shared/models/motions/workflow-state' import { WorkflowRepositoryService } from '../../services/workflow-repository.service'; import { MotionPdfExportService } from '../../services/motion-pdf-export.service'; import { MotionExportDialogComponent } from '../motion-export-dialog/motion-export-dialog.component'; +import { OperatorService } from '../../../../core/services/operator.service'; +import { ViewportService } from '../../../../core/services/viewport.service'; /** * Component that displays all the motions in a Table using DataSource. @@ -36,18 +38,14 @@ import { MotionExportDialogComponent } from '../motion-export-dialog/motion-expo }) export class MotionListComponent extends ListViewBaseComponent implements OnInit { /** - * Use for minimal width. Please note the 'selector' row for multiSelect mode, - * to be able to display an indicator for the state of selection - * TODO: Remove projector, if columnsToDisplayFullWidth is used.. + * Columns to display in table when desktop view is available */ - public columnsToDisplayMinWidth = ['projector', 'identifier', 'title', 'state', 'speakers']; + public displayedColumnsDesktop: string[] = ['identifier', 'title', 'state', 'speakers']; /** - * Use for maximal width. Please note the 'selector' row for multiSelect mode, - * to be able to display an indicator for the state of selection - * TODO: Needs vp.desktop check + * Columns to display in table when mobile view is available */ - public columnsToDisplayFullWidth = ['projector', 'identifier', 'title', 'state', 'speakers']; + public displayedColumnsMobile = ['identifier', 'title']; /** * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? @@ -82,6 +80,7 @@ export class MotionListComponent extends ListViewBaseComponent imple * @param userRepo * @param sortService * @param filterService + * @param vp * @param perms LocalPermissionService */ public constructor( @@ -97,8 +96,10 @@ export class MotionListComponent extends ListViewBaseComponent imple private workflowRepo: WorkflowRepositoryService, private motionRepo: MotionRepositoryService, private motionCsvExport: MotionCsvExportService, + private operator: OperatorService, private pdfExport: MotionPdfExportService, private dialog: MatDialog, + private vp: ViewportService, public multiselectService: MotionMultiselectService, public sortService: MotionSortListService, public filterService: MotionFilterListService, @@ -221,10 +222,14 @@ export class MotionListComponent extends ListViewBaseComponent imple * Returns current definitions for the listView table */ public getColumnDefinition(): string[] { - if (this.isMultiSelect) { - return ['selector'].concat(this.columnsToDisplayMinWidth); + let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop; + if (this.operator.hasPerms('core.can_manage_projector')) { + columns = ['projector'].concat(columns); } - return this.columnsToDisplayMinWidth; + if (this.isMultiSelect) { + columns = ['selector'].concat(columns); + } + return columns; } /** diff --git a/client/src/app/site/motions/models/view-motion.ts b/client/src/app/site/motions/models/view-motion.ts index 86a56e38e..33582ee08 100644 --- a/client/src/app/site/motions/models/view-motion.ts +++ b/client/src/app/site/motions/models/view-motion.ts @@ -318,7 +318,7 @@ export class ViewMotion extends BaseProjectableModel { public getTitle(): string { if (this.identifier) { - return this.identifier + ' - ' + this.title; + return 'Motion ' + this.identifier; } return this.title; } @@ -480,7 +480,7 @@ export class ViewMotion extends BaseProjectableModel { } ], projectionDefaultName: 'motions', - getTitle: () => this.getTitle() + getTitle: () => this.identifier }; } diff --git a/client/src/app/site/projector/components/projector/projector.component.html b/client/src/app/site/projector/components/projector/projector.component.html index a7ed7087d..f899c60ca 100644 --- a/client/src/app/site/projector/components/projector/projector.component.html +++ b/client/src/app/site/projector/components/projector/projector.component.html @@ -1,8 +1,15 @@
-
-
- Header Title + @@ -10,8 +17,12 @@
-
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 8526f1c44..5f8936b64 100644 --- a/client/src/app/site/projector/components/projector/projector.component.scss +++ b/client/src/app/site/projector/components/projector/projector.component.scss @@ -9,29 +9,76 @@ transform-origin: left top; overflow: hidden; - .header { + #header { position: absolute; top: 0; left: 0; - color: white; width: 100%; - height: 50px; + height: 70px; + box-shadow: 0 0 7px rgba(0, 0, 0, 0.6); + background-repeat: no-repeat; + background-size: 100% 100%; + margin-bottom: 20px; z-index: 1; + + #logo { + padding-left: 50px; + padding-top: 10px; + height: 50px; + margin-right: 25px; + float: left; + } + + #eventdata { + padding-left: 50px; + padding-top: 12px; + height: 50px; + overflow: hidden; + line-height: 1.1; + + .event-name { + font-size: 26px; + font-weight: 400; + + &.titleonly { + padding-top: 12px; + } + } + + .event-description { + font-size: 18px; + opacity: 0.8; + } + } } .content { width: 100%; height: 100%; position: absolute; top: 0; - left: 0; + left: 50px; + right: 50px; } - .footer { - position: absolute; - color: white; + + #footer { + position: fixed; width: 100%; - height: 50px; + height: 35px; bottom: 0; z-index: 1; + + .footertext { + font-size: 16px; + padding-left: 50px; + padding-right: 50px; + padding-top: 5px; + overflow: hidden; + text-align: right; + } + + span { + opacity: 0.8; + } } } } 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 5a11e4eb4..1993b3beb 100644 --- a/client/src/app/site/projector/components/projector/projector.component.ts +++ b/client/src/app/site/projector/components/projector/projector.component.ts @@ -132,6 +132,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy { public enableHeaderAndFooter = true; public enableTitle = true; public enableLogo = true; + public eventName; + public eventDescription; + public eventDate; + public eventLocation; /** * Listen to all related config variables. Register the resizeSubject. @@ -166,6 +170,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy { this.configService .get('projector_background_color') .subscribe(val => (this.projectorStyle['background-color'] = val)); + this.configService.get('general_event_name').subscribe(val => (this.eventName = val)); + this.configService.get('general_event_description').subscribe(val => (this.eventDescription = val)); + this.configService.get('general_event_date').subscribe(val => (this.eventDate = val)); + this.configService.get('general_event_location').subscribe(val => (this.eventLocation = val)); // Watches for resizing of the container. this.resizeSubject.subscribe(() => { 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 0e8c9aad9..2f5906446 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,6 @@ +#slide { + width: calc(100% - 100px); +} ::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 c73ab9bee..c8653e53f 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 @@ -95,7 +95,7 @@ export class SlideContainerComponent extends BaseComponent { */ public slideStyle: { 'font-size': string; 'margin-top': string } = { 'font-size': '100%', - 'margin-top': '50px' + 'margin-top': '100px' }; /** @@ -124,7 +124,7 @@ export class SlideContainerComponent extends BaseComponent { let value = this._scroll; value *= -50; if (this.headerEnabled) { - value += 50; // Default offset for the header + value += 100; // Default offset for the header } this.slideStyle['margin-top'] = `${value}px`; } else { diff --git a/client/src/app/site/users/components/user-list/user-list.component.html b/client/src/app/site/users/components/user-list/user-list.component.html index d8bd8bb9f..59b0034e1 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.html +++ b/client/src/app/site/users/components/user-list/user-list.component.html @@ -25,16 +25,16 @@ - - + + {{ isSelected(user) ? 'check_circle' : '' }} - Projector - + Projector + @@ -49,7 +49,7 @@ Group -
+
people {{ user.groups }} diff --git a/client/src/app/site/users/components/user-list/user-list.component.scss b/client/src/app/site/users/components/user-list/user-list.component.scss index 7897b07cf..d30857b94 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.scss +++ b/client/src/app/site/users/components/user-list/user-list.component.scss @@ -9,6 +9,10 @@ } .os-listview-table { + .mat-column-projector { + padding-right: 15px; + } + .mat-column-name { flex: 1 0 200px; } diff --git a/client/src/app/site/users/components/user-list/user-list.component.ts b/client/src/app/site/users/components/user-list/user-list.component.ts index 7d6e39b6f..18a8b9160 100644 --- a/client/src/app/site/users/components/user-list/user-list.component.ts +++ b/client/src/app/site/users/components/user-list/user-list.component.ts @@ -14,6 +14,8 @@ import { UserRepositoryService } from '../../services/user-repository.service'; import { ViewUser } from '../../models/view-user'; import { UserFilterListService } from '../../services/user-filter-list.service'; import { UserSortListService } from '../../services/user-sort-list.service'; +import { ViewportService } from '../../../../core/services/viewport.service'; +import { OperatorService } from '../../../../core/services/operator.service'; /** * Component for the user list view. @@ -25,6 +27,16 @@ import { UserSortListService } from '../../services/user-sort-list.service'; styleUrls: ['./user-list.component.scss'] }) export class UserListComponent extends ListViewBaseComponent implements OnInit { + /** + * Columns to display in table when desktop view is available + */ + public displayedColumnsDesktop: string[] = ['name', 'group']; + + /** + * Columns to display in table when mobile view is available + */ + public displayedColumnsMobile = ['name']; + /** * Stores the observed configuration if the presence view is available to administrators */ @@ -48,6 +60,8 @@ export class UserListComponent extends ListViewBaseComponent implement * @param groupRepo: The user group repository * @param router the router service * @param route the local route + * @param operator + * @param vp * @param csvExport CSV export Service, * @param promptService * @param groupRepo @@ -64,6 +78,8 @@ export class UserListComponent extends ListViewBaseComponent implement private choiceService: ChoiceService, private router: Router, private route: ActivatedRoute, + private operator: OperatorService, + private vp: ViewportService, protected csvExport: CsvExportService, private promptService: PromptService, public filterService: UserFilterListService, @@ -243,10 +259,15 @@ export class UserListComponent extends ListViewBaseComponent implement * @returns column definition */ public getColumnDefinition(): string[] { - // TODO: no projector in mobile view. - const columns = ['projector', 'name', 'group', 'presence']; + let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop; + if (this.operator.hasPerms('core.can_manage_projector')) { + columns = ['projector'].concat(columns); + } + if (this.operator.hasPerms('users.can_manage')) { + columns = columns.concat(['presence']); + } if (this.isMultiSelect) { - return ['selector'].concat(columns); + columns = ['selector'].concat(columns); } return columns; } 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 index fea915978..67fdfa6fc 100644 --- a/client/src/app/slides/core/clock/core-clock-slide.component.scss +++ b/client/src/app/slides/core/clock/core-clock-slide.component.scss @@ -6,10 +6,16 @@ height: 30px; margin: 12px; z-index: 2; + padding-right: 50px; + padding-top: 5px; + + mat-icon { + padding-top: 5px; + } span { padding-left: 5px; - font-size: 16px; + font-size: 24px; float: right; } } diff --git a/client/src/app/slides/motions/motion/motions-motion-slide-data.ts b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts index 9de7b2f40..75b2f650e 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide-data.ts +++ b/client/src/app/slides/motions/motion/motions-motion-slide-data.ts @@ -1,3 +1,15 @@ export interface MotionsMotionSlideData { + identifier: string; title: string; + text: string; + reason?: string; + is_child: boolean; + show_meta_box: boolean; + submitter?: string[]; + recommender?: string; + recommendation?: string; + recommendation_extension?: string; + amendment_paragraphs: { paragraph: string }[]; + change_recommendations: object[]; + modified_final_version?: 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 73d736376..73f494852 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,35 @@
- Motion Slide -

{{ data.data.title }}

+ + +
+ +
+

{{ data.data.title }}

+

Motion {{ data.data.identifier }}

+
+ + +
+ + +
+ + +
+

Reason

+
+
+
diff --git a/client/src/app/slides/motions/motion/motions-motion-slide.component.scss b/client/src/app/slides/motions/motion/motions-motion-slide.component.scss index 05920c613..d7ca3f8c2 100644 --- a/client/src/app/slides/motions/motion/motions-motion-slide.component.scss +++ b/client/src/app/slides/motions/motion/motions-motion-slide.component.scss @@ -1,3 +1,15 @@ -div { - background-color: red; +#sidebox { + width: 260px; + right: 0; + margin-top: 50px; + background: #d3d3d3; + border-radius: 7px 0 0 7px; + padding: 3px 7px 10px 10px; + position: fixed; + z-index: 5; + + h3 { + margin-top: 10px; + margin-bottom: 0px; + } } diff --git a/client/src/app/slides/users/user/users-user-slide-data.ts b/client/src/app/slides/users/user/users-user-slide-data.ts index 2d7b8a59c..34df2beb5 100644 --- a/client/src/app/slides/users/user/users-user-slide-data.ts +++ b/client/src/app/slides/users/user/users-user-slide-data.ts @@ -1,3 +1,3 @@ export interface UsersUserSlideData { - test: string; + user: string; } diff --git a/client/src/app/slides/users/user/users-user-slide.component.html b/client/src/app/slides/users/user/users-user-slide.component.html index 6b555d8ce..63ee3cc1d 100644 --- a/client/src/app/slides/users/user/users-user-slide.component.html +++ b/client/src/app/slides/users/user/users-user-slide.component.html @@ -1,3 +1,3 @@ -
- User Slide +
+

{{ data.data.user }}

diff --git a/client/src/styles.scss b/client/src/styles.scss index 09625db31..2ddc0baf0 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -41,7 +41,9 @@ body { h1, h2, h3, -.title-font { +.title-font, +.slidetitle h1, +.slidetitle h2 { font-family: OSFont Condensed, Fira Sans Condensed, Roboto-condensed, Arial, Helvetica, sans-serif; } @@ -232,6 +234,15 @@ mat-card { .os-listview-table { @extend %os-table; + /* multi select column */ + .mat-column-selector { + flex: 0 0 60px; + } + /* projector button column */ + .mat-column-projector { + flex: 0 0 40px; + overflow: visible; + } /** hide mat header row */ .mat-header-row { display: none; @@ -242,6 +253,7 @@ mat-card { @extend %os-table; } + /* TODO: find a better way to get more vertical space in (empty/small) tables for maximize filter dialog */ mat-paginator { min-height: 800px; @@ -292,10 +304,6 @@ mat-expansion-panel { display: none; } -.icon-cell { - flex: 0 0 40px; -} - // ngx-file-drop requires the custom style in the global css file .file-drop-style { margin: auto; @@ -565,6 +573,7 @@ button.mat-menu-item.selected { flex: 1; min-width: 0px; } + .filter-imports { max-width: 50%; } @@ -574,3 +583,42 @@ button.mat-menu-item.selected { font-weight: 500; font-size: 16px; } + + +/*** Projector slides ***/ + +#slide { + + h3 { + color: #222; + margin-bottom: 10px + } + + .slidetitle { + border-bottom: 5px solid #d3d3d3; + margin-bottom: 40px; + + h1 { + font-size: 2.25em; + line-height: 1.1em; + margin-bottom: 0; + padding-bottom: 0; + } + h2 { + color: #9a9898; + margin-top: 10px; + margin-bottom: 0px; + font-size: 28px; + font-weight: normal; + display: block; + } + } + + ul, ol { + margin: 0 0 10px 0; + } + + hr { + margin: 10px 0; + } +} diff --git a/openslides/agenda/projector.py b/openslides/agenda/projector.py index 3b0c72321..731d76f0a 100644 --- a/openslides/agenda/projector.py +++ b/openslides/agenda/projector.py @@ -107,5 +107,9 @@ def current_list_of_speakers_slide( 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) + register_projector_slide( + "agenda/current-list-of-speakers", current_list_of_speakers_slide + ) + register_projector_slide( + "agenda/current-list-of-speakers-overlay", current_list_of_speakers_slide + ) diff --git a/openslides/core/projector.py b/openslides/core/projector.py index ac814b363..a2564f535 100644 --- a/openslides/core/projector.py +++ b/openslides/core/projector.py @@ -54,4 +54,4 @@ def clock_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: def register_projector_slides() -> None: register_projector_slide("core/countdown", countdown_slide) register_projector_slide("core/projector-message", message_slide) - register_projector_element("core/clock", clock_slide) + register_projector_slide("core/clock", clock_slide) diff --git a/openslides/core/views.py b/openslides/core/views.py index 278fe053d..4ce5720e6 100644 --- a/openslides/core/views.py +++ b/openslides/core/views.py @@ -173,7 +173,9 @@ class ProjectorViewSet(ModelViewSet): 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) + delete_last_history_element = request.data.get( + "delete_last_history_element", False + ) changed_data = {} if elements is not None: diff --git a/openslides/motions/projector.py b/openslides/motions/projector.py index d58980bdd..1ae69fb88 100644 --- a/openslides/motions/projector.py +++ b/openslides/motions/projector.py @@ -45,12 +45,11 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: * show_meta_box * reason * modified_final_version - * state - * state_extension * recommendation * recommendation_extension + * recommender + * change_recommendations * submitter - * poll """ mode = element.get("mode") motion_id = element.get("id") @@ -81,11 +80,6 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: return_value["modified_final_version"] = motion["modified_final_version"] if show_meta_box: - state = get_state(all_data, motion, motion["state_id"]) - return_value["state"] = state["name"] - if state["show_state_extension_field"]: - return_value["state_extension"] = motion["state_extension"] - if ( not get_config(all_data, "motions_disable_recommendation_on_projector") and motion["recommendation_id"] @@ -101,6 +95,9 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: "recommendation_extension" ] + return_value["recommender"] = get_config( + all_data, "motions_recommendations_by" + ) return_value["change_recommendations"] = motion["change_recommendations"] return_value["submitter"] = [ @@ -110,15 +107,6 @@ def motion_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: ) ] - for poll in motion["polls"][::-1]: - if poll["has_votes"]: - return_value["poll"] = { - "yes": poll["yes"], - "no": poll["no"], - "abstain": poll["abstain"], - } - break - return return_value diff --git a/openslides/users/projector.py b/openslides/users/projector.py index c65d40547..a383d1bfb 100644 --- a/openslides/users/projector.py +++ b/openslides/users/projector.py @@ -1,6 +1,10 @@ from typing import Any, Dict, List -from ..utils.projector import AllData, register_projector_slide +from ..utils.projector import ( + AllData, + ProjectorElementException, + register_projector_slide, +) # Important: All functions have to be prune. This means, that thay can only @@ -12,8 +16,22 @@ from ..utils.projector import AllData, register_projector_slide def user_slide(all_data: AllData, element: Dict[str, Any]) -> Dict[str, Any]: """ User slide. + + The returned dict can contain the following fields: + * user """ - return {"error": "TODO"} + user_id = element.get("id") + + if user_id is None: + return {"error": "id is required for user slide"} + + try: + user = all_data["users/user"][user_id] + except KeyError: + raise ProjectorElementException(f"user with id {user_id} does not exist") + + return_value = {"user": get_user_name(all_data, user["id"])} + return return_value def get_user_name(all_data: AllData, user_id: int) -> str: diff --git a/tests/unit/motions/test_projector.py b/tests/unit/motions/test_projector.py index eb91a2df5..e372748b5 100644 --- a/tests/unit/motions/test_projector.py +++ b/tests/unit/motions/test_projector.py @@ -166,7 +166,5 @@ def test_motion_slide(all_data): "is_child": False, "show_meta_box": True, "reason": "", - "state": "submitted", "submitter": ["Administrator"], - "poll": {"yes": "10.000000", "no": "-1.000000", "abstain": "20.000000"}, }