Merge pull request #4187 from FinnStutzenstein/client-projector

more work on projector, countdowns, clos
This commit is contained in:
Emanuel Schütze 2019-01-29 16:43:24 +01:00 committed by GitHub
commit a1dc92bf0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 2442 additions and 447 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
<h2 mat-dialog-title>{{ projectable.getTitle() }}</h2> <h2 mat-dialog-title translate>Project
<span *ngIf="projectorElementBuildDescriptor.projectionDefaultName === 'motions'" translate>Motion</span>
{{ projectorElementBuildDescriptor.getTitle() }}?</h2>
<mat-dialog-content> <mat-dialog-content>
<mat-card>
<mat-card-title> <span translate>Projectors</span> </mat-card-title>
<mat-card-content>
<div class="projectors" *ngFor="let projector of projectors" [ngClass]="isProjectedOn(projector) ? 'projected' : ''"> <div class="projectors" *ngFor="let projector of projectors" [ngClass]="isProjectedOn(projector) ? 'projected' : ''">
<mat-checkbox [checked]="isProjectorSelected(projector)" (change)="toggleProjector(projector)"> <mat-checkbox [checked]="isProjectorSelected(projector)" (change)="toggleProjector(projector)">
{{ projector.name | translate }} {{ projector.name | translate }}
@ -11,13 +10,10 @@
<mat-icon>videocam</mat-icon> <mat-icon>videocam</mat-icon>
</span> </span>
</div> </div>
</mat-card-content>
</mat-card> <mat-divider></mat-divider>
<mat-card *ngIf="options.length > 0">
<mat-card-title> <div *ngIf="options.length > 0">
<span translate>Slide options</span>
</mat-card-title>
<mat-card-content>
<div *ngFor="let option of options"> <div *ngFor="let option of options">
<div *ngIf="isDecisionOption(option)"> <div *ngIf="isDecisionOption(option)">
<mat-checkbox [checked]="projectorElement[option.key]" (change)="projectorElement[option.key] = !projectorElement[option.key]"> <mat-checkbox [checked]="projectorElement[option.key]" (change)="projectorElement[option.key] = !projectorElement[option.key]">
@ -33,8 +29,7 @@
</mat-radio-group> </mat-radio-group>
</div> </div>
</div> </div>
</mat-card-content> </div>
</mat-card>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions> <mat-dialog-actions>
<button mat-button (click)="onOk()" color="primary" translate>OK</button> <button mat-button (click)="onOk()" color="primary" translate>OK</button>

View File

@ -1,19 +1,12 @@
mat-dialog-content { mat-dialog-content {
overflow: inherit; overflow: inherit;
min-width: auto;
div.projectors { div.projectors {
padding: 15px; padding: 15px 0;
&.projected {
background-color: lightblue;
}
.right { .right {
float: right; float: right;
} }
} }
mat-card {
margin-bottom: 10px;
}
} }

View File

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

View File

@ -1,3 +1,4 @@
<button type="button" mat-icon-button (click)="onClick($event)"> <button type="button" mat-mini-fab (click)="onClick($event)"
[ngClass]="isProjected() ? 'projectorbutton-active' : 'projectorbutton-inactive'">
<mat-icon>videocam</mat-icon> <mat-icon>videocam</mat-icon>
</button> </button>

View File

@ -0,0 +1,8 @@
.projectorbutton-active {
color: white !important;
}
.projectorbutton-inactive {
background-color: white;
color: grey;
}

View File

@ -1,6 +1,7 @@
import { Component, OnInit, Input } from '@angular/core'; 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'; import { ProjectionDialogService } from 'app/core/services/projection-dialog.service';
import { ProjectorService } from '../../../core/services/projector.service';
/** /**
*/ */
@ -11,12 +12,15 @@ import { ProjectionDialogService } from 'app/core/services/projection-dialog.ser
}) })
export class ProjectorButtonComponent implements OnInit { export class ProjectorButtonComponent implements OnInit {
@Input() @Input()
public object: Projectable; public object: Projectable | ProjectorElementBuildDeskriptor;
/** /**
* The consotructor * The consotructor
*/ */
public constructor(private projectionDialogService: ProjectionDialogService) {} public constructor(
private projectionDialogService: ProjectionDialogService,
private projectorService: ProjectorService
) {}
/** /**
* Initialization function * Initialization function
@ -27,4 +31,17 @@ export class ProjectorButtonComponent implements OnInit {
event.stopPropagation(); event.stopPropagation();
this.projectionDialogService.openProjectDialogFor(this.object); 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;
}
}
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +1,9 @@
import { Projectable } from './projectable'; import { Projectable, ProjectorElementBuildDeskriptor } from './projectable';
import { BaseViewModel } from './base-view-model'; import { BaseViewModel } from './base-view-model';
import { ProjectorOptions } from './projector-options';
/** /**
* Base view class for projectable models. * Base view class for projectable models.
*/ */
export abstract class BaseProjectableModel extends BaseViewModel implements Projectable { export abstract class BaseProjectableModel extends BaseViewModel implements Projectable {
/** public abstract getSlide(): ProjectorElementBuildDeskriptor;
* 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;
}
} }

View File

@ -1,32 +1,34 @@
import { ProjectorOptions } from './projector-options';
import { Displayable } from 'app/shared/models/base/displayable'; import { Displayable } from 'app/shared/models/base/displayable';
import { IdentifiableProjectorElement } from 'app/shared/models/core/projector';
import { SlideOptions } from './slide-options';
export function isProjectorElementBuildDeskriptor(obj: any): obj is ProjectorElementBuildDeskriptor {
const deskriptor = <ProjectorElementBuildDeskriptor>obj;
return (
deskriptor.slideOptions !== undefined &&
deskriptor.getBasicProjectorElement !== undefined &&
deskriptor.getTitle !== undefined
);
}
export interface ProjectorElementBuildDeskriptor {
slideOptions: SlideOptions;
projectionDefaultName?: string;
getBasicProjectorElement(): IdentifiableProjectorElement;
/**
* The title to show in the projection dialog
*/
getTitle(): string;
}
export function isProjectable(obj: any): obj is Projectable {
return (<Projectable>obj).getSlide !== undefined;
}
/** /**
* Interface for every model, that should be projectable. * Interface for every model, that should be projectable.
*/ */
export interface Projectable extends Displayable { export interface Projectable extends Displayable {
/** getSlide(): ProjectorElementBuildDeskriptor;
* 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;
} }

View File

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

View File

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

View File

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

View File

@ -7,9 +7,20 @@ import { PrivacyPolicyComponent } from './components/privacy-policy/privacy-poli
import { StartComponent } from './components/start/start.component'; import { StartComponent } from './components/start/start.component';
import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component'; import { LegalNoticeComponent } from './components/legal-notice/legal-notice.component';
import { SearchComponent } from './components/search/search.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({ @NgModule({
providers: [CountdownRepositoryService],
imports: [AngularCommonModule, CommonRoutingModule, SharedModule], imports: [AngularCommonModule, CommonRoutingModule, SharedModule],
declarations: [PrivacyPolicyComponent, StartComponent, LegalNoticeComponent, SearchComponent] declarations: [
PrivacyPolicyComponent,
StartComponent,
LegalNoticeComponent,
SearchComponent,
CountdownListComponent,
ProjectorMessageListComponent
]
}) })
export class CommonModule {} export class CommonModule {}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,12 +36,12 @@ export class MediafileListComponent extends ListViewBaseComponent<ViewMediafile>
public fontActions: string[]; 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']; 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']; public displayedColumnsMobile: string[] = ['title', 'menu'];

View File

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

View File

@ -27,16 +27,16 @@
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column --> <!-- Selector column -->
<ng-container matColumnDef="selector"> <ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let motion" class="checkbox-cell"> <mat-cell *matCellDef="let motion">
<mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon> <mat-icon>{{ isSelected(motion) ? 'check_circle' : '' }}</mat-icon>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<!-- Projector column --> <!-- Projector column -->
<ng-container matColumnDef="projector"> <ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let motion" class="icon-cell"> <mat-cell *matCellDef="let motion">
<os-projector-button [object]="motion"></os-projector-button> <os-projector-button [object]="motion"></os-projector-button>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -84,13 +84,14 @@
grey: motion.state.css_class === 'default', grey: motion.state.css_class === 'default',
lightblue: motion.state.css_class === 'primary' lightblue: motion.state.css_class === 'primary'
}" }"
[disabled]="true"
> >
{{ getStateLabel(motion) }} {{ getStateLabel(motion) }}
</mat-basic-chip> </mat-basic-chip>
<!-- recommendation --> <!-- recommendation -->
<span *ngIf="motion.recommendation"> <span *ngIf="motion.recommendation && motion.state.next_states_id.length > 0">
<mat-basic-chip class="bluegrey"> {{ getRecommendationLabel(motion) }} </mat-basic-chip> <mat-basic-chip class="bluegrey" [disabled]="true">{{ getRecommendationLabel(motion) }} </mat-basic-chip>
</span> </span>
</div> </div>
</mat-cell> </mat-cell>

View File

@ -25,6 +25,8 @@ import { WorkflowState } from '../../../../shared/models/motions/workflow-state'
import { WorkflowRepositoryService } from '../../services/workflow-repository.service'; import { WorkflowRepositoryService } from '../../services/workflow-repository.service';
import { MotionPdfExportService } from '../../services/motion-pdf-export.service'; import { MotionPdfExportService } from '../../services/motion-pdf-export.service';
import { MotionExportDialogComponent } from '../motion-export-dialog/motion-export-dialog.component'; 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. * 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<ViewMotion> implements OnInit { export class MotionListComponent extends ListViewBaseComponent<ViewMotion> implements OnInit {
/** /**
* Use for minimal width. Please note the 'selector' row for multiSelect mode, * Columns to display in table when desktop view is available
* to be able to display an indicator for the state of selection
* TODO: Remove projector, if columnsToDisplayFullWidth is used..
*/ */
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, * Columns to display in table when mobile view is available
* to be able to display an indicator for the state of selection
* TODO: Needs vp.desktop check
*/ */
public columnsToDisplayFullWidth = ['projector', 'identifier', 'title', 'state', 'speakers']; public displayedColumnsMobile = ['identifier', 'title'];
/** /**
* Value of the configuration variable `motions_statutes_enabled` - are statutes enabled? * Value of the configuration variable `motions_statutes_enabled` - are statutes enabled?
@ -82,6 +80,7 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* @param userRepo * @param userRepo
* @param sortService * @param sortService
* @param filterService * @param filterService
* @param vp
* @param perms LocalPermissionService * @param perms LocalPermissionService
*/ */
public constructor( public constructor(
@ -97,8 +96,10 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
private workflowRepo: WorkflowRepositoryService, private workflowRepo: WorkflowRepositoryService,
private motionRepo: MotionRepositoryService, private motionRepo: MotionRepositoryService,
private motionCsvExport: MotionCsvExportService, private motionCsvExport: MotionCsvExportService,
private operator: OperatorService,
private pdfExport: MotionPdfExportService, private pdfExport: MotionPdfExportService,
private dialog: MatDialog, private dialog: MatDialog,
private vp: ViewportService,
public multiselectService: MotionMultiselectService, public multiselectService: MotionMultiselectService,
public sortService: MotionSortListService, public sortService: MotionSortListService,
public filterService: MotionFilterListService, public filterService: MotionFilterListService,
@ -221,10 +222,14 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
* Returns current definitions for the listView table * Returns current definitions for the listView table
*/ */
public getColumnDefinition(): string[] { public getColumnDefinition(): string[] {
if (this.isMultiSelect) { let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
return ['selector'].concat(this.columnsToDisplayMinWidth); if (this.operator.hasPerms('core.can_manage_projector')) {
columns = ['projector'].concat(columns);
} }
return this.columnsToDisplayMinWidth; if (this.isMultiSelect) {
columns = ['selector'].concat(columns);
}
return columns;
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,16 @@ import { TranslateService } from '@ngx-translate/core';
import { ProjectorRepositoryService, ScrollScaleDirection } from '../../services/projector-repository.service'; import { ProjectorRepositoryService, ScrollScaleDirection } from '../../services/projector-repository.service';
import { ViewProjector } from '../../models/view-projector'; import { ViewProjector } from '../../models/view-projector';
import { BaseViewComponent } from 'app/site/base/base-view'; 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. * The projector detail view.
@ -25,6 +35,10 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
public scrollScaleDirection = ScrollScaleDirection; public scrollScaleDirection = ScrollScaleDirection;
public countdowns: ViewCountdown[] = [];
public messages: ViewProjectorMessage[] = [];
/** /**
* @param titleService * @param titleService
* @param translate * @param translate
@ -37,9 +51,17 @@ export class ProjectorDetailComponent extends BaseViewComponent implements OnIni
translate: TranslateService, translate: TranslateService,
matSnackBar: MatSnackBar, matSnackBar: MatSnackBar,
private repo: ProjectorRepositoryService, 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); 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 { public scale(direction: ScrollScaleDirection): void {
this.repo.scale(this.projector, direction).then(null, this.raiseError); this.repo.scale(this.projector, direction).then(null, this.raiseError);
} }
public projectNextSlide(): void {
this.projectorService.projectNextSlide(this.projector.projector).then(null, this.raiseError);
}
public projectPreviousSlide(): void {
this.projectorService.projectPreviousSlide(this.projector.projector).then(null, this.raiseError);
}
public onSortingChange(event: CdkDragDrop<ProjectorElement>): void {
moveItemInArray(this.projector.elements_preview, event.previousIndex, event.currentIndex);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
}
public removePreviewElement(elementIndex: number): void {
this.projector.elements_preview.splice(elementIndex, 1);
this.projectorService.savePreview(this.projector.projector).then(null, this.raiseError);
}
public projectNow(elementIndex: number): void {
this.projectorService.projectPreviewSlide(this.projector.projector, elementIndex).then(null, this.raiseError);
}
public getElementDescription(element: ProjectorElement): string {
if (!this.slideManager.canSlideBeMappedToModel(element.name)) {
return this.slideManager.getSlideVerboseName(element.name);
} else {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
const model = this.projectorService.getModelFromProjectorElement(idElement);
return model.getTitle();
}
}
public isProjected(obj: Projectable): boolean {
return this.projectorService.isProjectedOn(obj, this.projector.projector);
}
public async project(obj: Projectable): Promise<void> {
try {
if (this.isProjected(obj)) {
await this.projectorService.removeFrom(this.projector.projector, obj);
} else {
await this.projectorService.projectOn(this.projector.projector, obj);
}
} catch (e) {
this.raiseError(e);
}
}
public unprojectCurrent(element: ProjectorElement): void {
const idElement = this.slideManager.getIdentifialbeProjectorElement(element);
this.projectorService.removeFrom(this.projector.projector, idElement).then(null, this.raiseError);
}
public isClosProjected(stable: boolean): boolean {
return this.currentListOfSpeakersSlideService.isProjectedOn(this.projector, stable);
}
public toggleClos(stable: boolean): void {
this.currentListOfSpeakersSlideService.toggleOn(this.projector, stable);
}
} }

View File

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

View File

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

View File

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

View File

@ -1,8 +1,15 @@
<div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container> <div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container>
<div id="projector" [ngStyle]="projectorStyle"> <div id="projector" [ngStyle]="projectorStyle">
<div class="header" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter"> <div id="header" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter">
<div *ngIf="enableTitle"> <!-- TODO: Logo <img *ngIf="enableLogo" id="logo"> -->
Header Title <div *ngIf="enableTitle" id="eventdata">
<div
*ngIf="eventName"
class="event-name"
[ngClass]="!eventDescription ? 'titleonly' : ''"
[innerHTML]="eventName"
></div>
<div *ngIf="eventDescription" class="event-description" [innerHTML]="eventDescription"></div>
</div> </div>
</div> </div>
@ -10,8 +17,12 @@
<os-slide-container [slideData]="slide" [scroll]="scroll" [scale]="scale"></os-slide-container> <os-slide-container [slideData]="slide" [scroll]="scroll" [scale]="scale"></os-slide-container>
</div> </div>
<div class="footer" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter"> <div id="footer" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter">
Footer <div class="footertext">
<span *ngIf="eventDate"> {{ eventDate }} </span>
<span *ngIf="eventDate && eventLocation"> | </span>
<span *ngIf="eventLocation"> {{ eventLocation }} </span>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -9,28 +9,76 @@
transform-origin: left top; transform-origin: left top;
overflow: hidden; overflow: hidden;
.header { #header {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
color: white;
width: 100%; 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; 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 { .content {
width: 100%; width: 100%;
height: 100%;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 50px;
right: 50px;
} }
.footer {
position: absolute; #footer {
color: white; position: fixed;
width: 100%; width: 100%;
height: 50px; height: 35px;
bottom: 0; bottom: 0;
z-index: 1; 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;
}
} }
} }
} }

View File

@ -132,6 +132,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
public enableHeaderAndFooter = true; public enableHeaderAndFooter = true;
public enableTitle = true; public enableTitle = true;
public enableLogo = true; public enableLogo = true;
public eventName;
public eventDescription;
public eventDate;
public eventLocation;
/** /**
* Listen to all related config variables. Register the resizeSubject. * Listen to all related config variables. Register the resizeSubject.
@ -166,6 +170,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
this.configService this.configService
.get<string>('projector_background_color') .get<string>('projector_background_color')
.subscribe(val => (this.projectorStyle['background-color'] = val)); .subscribe(val => (this.projectorStyle['background-color'] = val));
this.configService.get<string>('general_event_name').subscribe(val => (this.eventName = val));
this.configService.get<string>('general_event_description').subscribe(val => (this.eventDescription = val));
this.configService.get<string>('general_event_date').subscribe(val => (this.eventDate = val));
this.configService.get<string>('general_event_location').subscribe(val => (this.eventLocation = val));
// Watches for resizing of the container. // Watches for resizing of the container.
this.resizeSubject.subscribe(() => { this.resizeSubject.subscribe(() => {
@ -216,8 +224,10 @@ export class ProjectorComponent extends BaseComponent implements OnDestroy {
.getProjectorObservable(to) .getProjectorObservable(to)
.subscribe(data => (this.slides = data || [])); .subscribe(data => (this.slides = data || []));
this.projectorSubscription = this.projectorRepository.getViewModelObservable(to).subscribe(projector => { this.projectorSubscription = this.projectorRepository.getViewModelObservable(to).subscribe(projector => {
this.scroll = projector.scroll; if (projector) {
this.scale = projector.scale; this.scroll = projector.scroll || 0;
this.scale = projector.scale || 0;
}
}); });
} else if (!to && from > 0) { } else if (!to && from > 0) {
// no new projector // no new projector

View File

@ -1,3 +1,7 @@
#slide {
width: calc(100% - 100px);
}
::ng-deep #slide { ::ng-deep #slide {
z-index: 5; z-index: 5;
height: 100%;
} }

View File

@ -4,7 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
import { BaseComponent } from 'app/base.component'; import { BaseComponent } from 'app/base.component';
import { SlideData } from '../../services/projector-data.service'; 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 { BaseSlideComponent } from 'app/slides/base-slide-component';
import { SlideOptions } from 'app/slides/slide-manifest'; import { SlideOptions } from 'app/slides/slide-manifest';
import { ConfigService } from 'app/core/services/config.service'; import { ConfigService } from 'app/core/services/config.service';
@ -95,7 +95,7 @@ export class SlideContainerComponent extends BaseComponent {
*/ */
public slideStyle: { 'font-size': string; 'margin-top': string } = { public slideStyle: { 'font-size': string; 'margin-top': string } = {
'font-size': '100%', 'font-size': '100%',
'margin-top': '50px' 'margin-top': '100px'
}; };
/** /**
@ -106,7 +106,7 @@ export class SlideContainerComponent extends BaseComponent {
public constructor( public constructor(
titleService: Title, titleService: Title,
translate: TranslateService, translate: TranslateService,
private dynamicSlideLoader: DynamicSlideLoader, private slideManager: SlideManager,
private configService: ConfigService private configService: ConfigService
) { ) {
super(titleService, translate); super(titleService, translate);
@ -124,7 +124,7 @@ export class SlideContainerComponent extends BaseComponent {
let value = this._scroll; let value = this._scroll;
value *= -50; value *= -50;
if (this.headerEnabled) { if (this.headerEnabled) {
value += 50; // Default offset for the header value += 100; // Default offset for the header
} }
this.slideStyle['margin-top'] = `${value}px`; this.slideStyle['margin-top'] = `${value}px`;
} else { } else {
@ -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. * @param slideName The slide to load.
*/ */
private slideChanged(slideName: string): void { private slideChanged(slideName: string): void {
this.slideOptions = this.dynamicSlideLoader.getSlideOptions(slideName); this.slideOptions = this.slideManager.getSlideOptions(slideName);
this.dynamicSlideLoader.getSlideFactory(slideName).then(slideFactory => { this.slideManager.getSlideFactory(slideName).then(slideFactory => {
this.slide.clear(); this.slide.clear();
this.slideRef = this.slide.createComponent(slideFactory); this.slideRef = this.slide.createComponent(slideFactory);
this.setDataForComponent(); this.setDataForComponent();

View File

@ -20,6 +20,18 @@ export class ViewProjector extends BaseViewModel {
return this.projector ? this.projector.elements : null; 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 { public get height(): number {
return this.projector ? this.projector.height : null; return this.projector ? this.projector.height : null;
} }

View File

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

View File

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

View File

@ -8,8 +8,12 @@ import { ProjectorListComponent } from './components/projector-list/projector-li
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component'; import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
import { SlideContainerComponent } from './components/slide-container/slide-container.component'; import { SlideContainerComponent } from './components/slide-container/slide-container.component';
import { FullscreenProjectorComponent } from './components/fullscreen-projector/fullscreen-projector.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({ @NgModule({
providers: [ClockSlideService, ProjectorDataService, ProjectorRepositoryService],
imports: [CommonModule, ProjectorRoutingModule, SharedModule], imports: [CommonModule, ProjectorRoutingModule, SharedModule],
declarations: [ declarations: [
ProjectorComponent, ProjectorComponent,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,16 +25,16 @@
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort> <mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
<!-- Selector column --> <!-- Selector column -->
<ng-container matColumnDef="selector"> <ng-container matColumnDef="selector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header></mat-header-cell>
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)" class="icon-cell"> <mat-cell *matCellDef="let user" (click)="selectItem(user, $event)">
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon> <mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<!-- Projector column --> <!-- Projector column -->
<ng-container matColumnDef="projector"> <ng-container matColumnDef="projector">
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Projector</mat-header-cell>
<mat-cell *matCellDef="let user" class="icon-cell"> <mat-cell *matCellDef="let user">
<os-projector-button [object]="user"></os-projector-button> <os-projector-button [object]="user"></os-projector-button>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@ -49,7 +49,7 @@
<ng-container matColumnDef="group"> <ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let user"> <mat-cell *matCellDef="let user">
<div class="groupsCell"> <div class='groupsCell'>
<span *ngIf="user.groups && user.groups.length"> <span *ngIf="user.groups && user.groups.length">
<mat-icon>people</mat-icon> <mat-icon>people</mat-icon>
{{ user.groups }} {{ user.groups }}

View File

@ -9,6 +9,10 @@
} }
.os-listview-table { .os-listview-table {
.mat-column-projector {
padding-right: 15px;
}
.mat-column-name { .mat-column-name {
flex: 1 0 200px; flex: 1 0 200px;
} }

View File

@ -14,6 +14,8 @@ import { UserRepositoryService } from '../../services/user-repository.service';
import { ViewUser } from '../../models/view-user'; import { ViewUser } from '../../models/view-user';
import { UserFilterListService } from '../../services/user-filter-list.service'; import { UserFilterListService } from '../../services/user-filter-list.service';
import { UserSortListService } from '../../services/user-sort-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. * Component for the user list view.
@ -25,6 +27,16 @@ import { UserSortListService } from '../../services/user-sort-list.service';
styleUrls: ['./user-list.component.scss'] styleUrls: ['./user-list.component.scss']
}) })
export class UserListComponent extends ListViewBaseComponent<ViewUser> implements OnInit { export class UserListComponent extends ListViewBaseComponent<ViewUser> 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 * Stores the observed configuration if the presence view is available to administrators
*/ */
@ -48,6 +60,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @param groupRepo: The user group repository * @param groupRepo: The user group repository
* @param router the router service * @param router the router service
* @param route the local route * @param route the local route
* @param operator
* @param vp
* @param csvExport CSV export Service, * @param csvExport CSV export Service,
* @param promptService * @param promptService
* @param groupRepo * @param groupRepo
@ -64,6 +78,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
private choiceService: ChoiceService, private choiceService: ChoiceService,
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private operator: OperatorService,
private vp: ViewportService,
protected csvExport: CsvExportService, protected csvExport: CsvExportService,
private promptService: PromptService, private promptService: PromptService,
public filterService: UserFilterListService, public filterService: UserFilterListService,
@ -243,10 +259,15 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
* @returns column definition * @returns column definition
*/ */
public getColumnDefinition(): string[] { public getColumnDefinition(): string[] {
// TODO: no projector in mobile view. let columns = this.vp.isMobile ? this.displayedColumnsMobile : this.displayedColumnsDesktop;
const columns = ['projector', 'name', 'group', 'presence']; 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) { if (this.isMultiSelect) {
return ['selector'].concat(columns); columns = ['selector'].concat(columns);
} }
return columns; return columns;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,72 @@ import { SlideManifest } from './slide-manifest';
/** /**
* Here, all slides has to be registered. * 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[] = [ export const allSlides: SlideManifest[] = [
{ {
slideName: 'motions/motion', slide: 'motions/motion',
path: 'motions/motion', path: 'motions/motion',
loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule', loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule',
scaleable: true, scaleable: true,
scrollable: true scrollable: true,
verboseName: 'Motion',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
}, },
{ {
slideName: 'users/user', slide: 'users/user',
path: 'users/user', path: 'users/user',
loadChildren: './slides/users/user/users-user-slide.module#UsersUserSlideModule', 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, scaleable: false,
scrollable: false scrollable: false,
verboseName: 'Clock',
elementIdentifiers: ['name'],
canBeMappedToModel: false
},
{
slide: 'core/countdown',
path: 'core/countdown',
loadChildren: './slides/core/countdown/core-countdown-slide.module#CoreCountdownSlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Countdown',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: true
},
{
slide: 'agenda/current-list-of-speakers',
path: 'agenda/current-list-of-speakers',
loadChildren:
'./slides/agenda/current-list-of-speakers/agenda-current-list-of-speakers-slide.module#AgendaCurrentListOfSpeakersSlideModule',
scaleable: true,
scrollable: true,
verboseName: 'Current list of speakers',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false
},
{
slide: 'agenda/current-list-of-speakers-overlay',
path: 'agenda/current-list-of-speakers-overlay',
loadChildren:
'./slides/agenda/current-list-of-speakers-overlay/agenda-current-list-of-speakers-overlay-slide.module#AgendaCurrentListOfSpeakersOverlaySlideModule',
scaleable: false,
scrollable: false,
verboseName: 'Current list of speakers overlay',
elementIdentifiers: ['name', 'id'],
canBeMappedToModel: false
} }
]; ];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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