Merge pull request #4103 from FinnStutzenstein/client-projector-ui
Client projector ui (WIP)
This commit is contained in:
commit
209105efc3
@ -41,6 +41,7 @@
|
||||
"@tinymce/tinymce-angular": "^2.3.1",
|
||||
"angular-tree-component": "^8.0.1",
|
||||
"core-js": "^2.5.4",
|
||||
"css-element-queries": "^1.1.1",
|
||||
"file-saver": "^2.0.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ngx-file-drop": "^5.0.0",
|
||||
|
@ -7,6 +7,7 @@ import { LoginLegalNoticeComponent } from './site/login/components/login-legal-n
|
||||
import { LoginPrivacyPolicyComponent } from './site/login/components/login-privacy-policy/login-privacy-policy.component';
|
||||
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component';
|
||||
import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
|
||||
import { FullscreenProjectorComponent } from './site/projector/components/fullscreen-projector/fullscreen-projector.component';
|
||||
|
||||
/**
|
||||
* Global app routing
|
||||
@ -23,8 +24,8 @@ const routes: Routes = [
|
||||
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent }
|
||||
]
|
||||
},
|
||||
|
||||
{ path: 'projector', loadChildren: './projector-container/projector-container.module#ProjectorContainerModule' },
|
||||
{ path: 'projector', component: FullscreenProjectorComponent },
|
||||
{ path: 'projector/:id', component: FullscreenProjectorComponent },
|
||||
{ path: '', loadChildren: './site/site.module#SiteModule' },
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
||||
|
@ -15,6 +15,8 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { PruningTranslationLoader } from './core/pruning-loader';
|
||||
import { LoginModule } from './site/login/login.module';
|
||||
import { AppLoadService } from './core/services/app-load.service';
|
||||
import { ProjectorModule } from './site/projector/projector.module';
|
||||
import { SlidesModule } from './slides/slides.module';
|
||||
|
||||
// PWA
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
@ -60,7 +62,9 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
||||
CoreModule,
|
||||
LoginModule,
|
||||
PapaParseModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })
|
||||
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
|
||||
ProjectorModule,
|
||||
SlidesModule.forRoot()
|
||||
],
|
||||
providers: [{ provide: APP_INITIALIZER, useFactory: AppLoaderFactory, deps: [AppLoadService], multi: true }],
|
||||
bootstrap: [AppComponent]
|
||||
|
@ -14,6 +14,7 @@ import { ViewportService } from './services/viewport.service';
|
||||
import { PromptDialogComponent } from '../shared/components/prompt-dialog/prompt-dialog.component';
|
||||
import { HttpService } from './services/http.service';
|
||||
import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice-dialog.component';
|
||||
import { ProjectionDialogComponent } from 'app/shared/components/projection-dialog/projection-dialog.component';
|
||||
|
||||
/** Global Core Module. Contains all global (singleton) services
|
||||
*
|
||||
@ -32,7 +33,7 @@ import { ChoiceDialogComponent } from '../shared/components/choice-dialog/choice
|
||||
ViewportService,
|
||||
WebsocketService
|
||||
],
|
||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent]
|
||||
entryComponents: [PromptDialogComponent, ChoiceDialogComponent, ProjectionDialogComponent]
|
||||
})
|
||||
export class CoreModule {
|
||||
/** make sure CoreModule is imported only by one NgModule, the AppModule */
|
||||
|
@ -14,6 +14,7 @@ import { MainMenuService } from './main-menu.service';
|
||||
import { HistoryAppConfig } from 'app/site/history/history.config';
|
||||
import { SearchService } from './search.service';
|
||||
import { isSearchable } from '../../shared/models/base/searchable';
|
||||
import { ProjectorAppConfig } from 'app/site/projector/projector.config';
|
||||
|
||||
/**
|
||||
* A list of all app configurations of all delivered apps.
|
||||
@ -27,7 +28,8 @@ const appConfigs: AppConfig[] = [
|
||||
MediafileAppConfig,
|
||||
TagAppConfig,
|
||||
UsersAppConfig,
|
||||
HistoryAppConfig
|
||||
HistoryAppConfig,
|
||||
ProjectorAppConfig
|
||||
];
|
||||
|
||||
/**
|
||||
|
50
client/src/app/core/services/projection-dialog.service.ts
Normal file
50
client/src/app/core/services/projection-dialog.service.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { Projectable } from 'app/site/base/projectable';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import {
|
||||
ProjectionDialogComponent,
|
||||
ProjectionDialogReturnType
|
||||
} from 'app/shared/components/projection-dialog/projection-dialog.component';
|
||||
import { ProjectorService } from './projector.service';
|
||||
|
||||
/**
|
||||
* Manages the projection dialog. Projects the result of the user's choice.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectionDialogService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param dialog
|
||||
* @param projectorService
|
||||
*/
|
||||
public constructor(private dialog: MatDialog, private projectorService: ProjectorService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the projection dialog for the given projectable. After the user's choice,
|
||||
* the projectors will be updated.
|
||||
*
|
||||
* @param obj The projectable.
|
||||
*/
|
||||
public async openProjectDialogFor(obj: Projectable): Promise<void> {
|
||||
const dialogRef = this.dialog.open<ProjectionDialogComponent, Projectable, ProjectionDialogReturnType>(
|
||||
ProjectionDialogComponent,
|
||||
{
|
||||
minWidth: '500px',
|
||||
maxHeight: '90vh',
|
||||
data: obj
|
||||
}
|
||||
);
|
||||
const response = await dialogRef.afterClosed().toPromise();
|
||||
if (response) {
|
||||
const [projectors, projectorElement]: ProjectionDialogReturnType = response;
|
||||
this.projectorService.projectOn(projectors, projectorElement);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { ProjectionDialogService } from './projection-dialog.service';
|
||||
|
||||
describe('ProjectionDialogService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectionDialogService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ProjectionDialogService], (service: ProjectionDialogService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
17
client/src/app/core/services/projector.service.spec.ts
Normal file
17
client/src/app/core/services/projector.service.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { ProjectorService } from './projector.service';
|
||||
|
||||
describe('ProjectorService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectorService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ProjectorService], (service: ProjectorService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
106
client/src/app/core/services/projector.service.ts
Normal file
106
client/src/app/core/services/projector.service.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { OpenSlidesComponent } from 'app/openslides.component';
|
||||
import { Projectable } from 'app/site/base/projectable';
|
||||
import { DataStoreService } from './data-store.service';
|
||||
import { Projector, ProjectorElement } from 'app/shared/models/core/projector';
|
||||
import { DataSendService } from './data-send.service';
|
||||
|
||||
/**
|
||||
* This service cares about Projectables being projected and manage all projection-related
|
||||
* actions.
|
||||
*
|
||||
* We cannot access the ProjectorRepository here, so we will deal with plain projector objects.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectorService extends OpenSlidesComponent {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DS
|
||||
* @param dataSend
|
||||
*/
|
||||
public constructor(private DS: DataStoreService, private dataSend: DataSendService) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if a given object is projected.
|
||||
*
|
||||
* @param obj The object in question
|
||||
* @returns true, if the object is projected on one projector.
|
||||
*/
|
||||
public isProjected(obj: Projectable): boolean {
|
||||
return this.DS.getAll<Projector>('core/projector').some(projector => {
|
||||
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projectors where the object is prejected on.
|
||||
*
|
||||
* @param obj The object in question
|
||||
* @return All projectors, where this Object is projected on
|
||||
*/
|
||||
public getProjectorsWhichAreProjecting(obj: Projectable): Projector[] {
|
||||
return this.DS.getAll<Projector>('core/projector').filter(projector => {
|
||||
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, if the object is projected on the given projector.
|
||||
*
|
||||
* @param obj The object
|
||||
* @param projector The projector to test
|
||||
* @returns true, if the object is projected on the projector.
|
||||
*/
|
||||
public isProjectedOn(obj: Projectable, projector: Projector): boolean {
|
||||
return projector.isElementShown(obj.getNameForSlide(), obj.getIdForSlide());
|
||||
}
|
||||
|
||||
/**
|
||||
* Projects the given ProjectorElement on the given projectors.
|
||||
*
|
||||
* TODO: this does not care about the element being stable. Some more logic must be added later.
|
||||
*
|
||||
* On the given projectors: Delete all non-stable elements and add the given element.
|
||||
* On all other projectors: If the element (compared with name and id) is there, it will be deleted.
|
||||
*
|
||||
* @param projectors All projectors where to add the element.
|
||||
* @param element The element in question.
|
||||
*/
|
||||
public projectOn<T extends ProjectorElement>(projectors: Projector[], element: T): void {
|
||||
const changedProjectors: Projector[] = [];
|
||||
this.DS.getAll<Projector>('core/projector').forEach(projector => {
|
||||
if (projectors.includes(projector)) {
|
||||
projector.removeAllNonStableElements();
|
||||
projector.addElement(element);
|
||||
changedProjectors.push(projector);
|
||||
} else if (projector.isElementShown(element.name, element.id)) {
|
||||
projector.removeElementByNameAndId(element.name, element.id);
|
||||
changedProjectors.push(projector);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Use new 'project' route.
|
||||
changedProjectors.forEach(projector => {
|
||||
this.dataSend.updateModel(projector);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a projectiondefault, we want to retrieve the projector, that is assigned
|
||||
* to this default.
|
||||
*
|
||||
* @param projectiondefault The projection default
|
||||
* @return the projector associated to the given projectiondefault.
|
||||
*/
|
||||
public getProjectorForDefault(projectiondefault: string): Projector {
|
||||
return this.DS.getAll<Projector>('core/projector').find(projector => {
|
||||
return projector.projectiondefaults.map(pd => pd.name).includes(projectiondefault);
|
||||
});
|
||||
}
|
||||
}
|
@ -28,12 +28,18 @@ export class ViewportService {
|
||||
* Simple boolean to determine whether the client is in mobile view or not
|
||||
* Use in HTML with automatic change detection
|
||||
*/
|
||||
public isMobile: boolean;
|
||||
public get isMobile(): boolean {
|
||||
return this._isMobileSubject.getValue();
|
||||
}
|
||||
|
||||
private _isMobileSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* Returns a subject that contains whether the viewport os mobile or not
|
||||
*/
|
||||
public isMobileSubject = new BehaviorSubject<boolean>(false);
|
||||
public get isMobileSubject(): BehaviorSubject<boolean> {
|
||||
return this._isMobileSubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the BreakpointObserver
|
||||
@ -49,14 +55,6 @@ export class ViewportService {
|
||||
public checkForChange(): void {
|
||||
this.breakpointObserver
|
||||
.observe([Breakpoints.Handset, '(min-width: 600px) and (max-width: 899.99px)'])
|
||||
.subscribe((state: BreakpointState) => {
|
||||
if (state.matches) {
|
||||
this.isMobile = true;
|
||||
this.isMobileSubject.next(true);
|
||||
} else {
|
||||
this.isMobile = false;
|
||||
this.isMobileSubject.next(false);
|
||||
}
|
||||
});
|
||||
.subscribe((state: BreakpointState) => this._isMobileSubject.next(state.matches));
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,10 @@ export class WebsocketService {
|
||||
*/
|
||||
private _connectEvent: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
private connectionOpen = false;
|
||||
|
||||
private sendQueueWhileNotConnected: string[] = [];
|
||||
|
||||
/**
|
||||
* Getter for the connect event.
|
||||
*/
|
||||
@ -122,6 +126,11 @@ export class WebsocketService {
|
||||
this._reconnectEvent.emit();
|
||||
}
|
||||
this._connectEvent.emit();
|
||||
this.connectionOpen = true;
|
||||
this.sendQueueWhileNotConnected.forEach(entry => {
|
||||
this.websocket.send(entry);
|
||||
});
|
||||
this.sendQueueWhileNotConnected = [];
|
||||
});
|
||||
};
|
||||
|
||||
@ -143,6 +152,7 @@ export class WebsocketService {
|
||||
this.websocket.onclose = (event: CloseEvent) => {
|
||||
this.zone.run(() => {
|
||||
this.websocket = null;
|
||||
this.connectionOpen = false;
|
||||
if (event.code !== 1000) {
|
||||
// 1000 is a normal close, like the close on logout
|
||||
if (!this.connectionErrorNotice) {
|
||||
@ -211,6 +221,13 @@ export class WebsocketService {
|
||||
message.id += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
}
|
||||
this.websocket.send(JSON.stringify(message));
|
||||
|
||||
// Either send directly or add to queue, if not connected.
|
||||
const jsonMessage = JSON.stringify(message);
|
||||
if (this.connectionOpen) {
|
||||
this.websocket.send(jsonMessage);
|
||||
} else {
|
||||
this.sendQueueWhileNotConnected.push(jsonMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<p>
|
||||
projector-container works!
|
||||
Here an iframe with the real-projector is needed
|
||||
</p>
|
@ -1,24 +0,0 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectorContainerComponent } from './projector-container.component';
|
||||
|
||||
describe('ProjectorContainerComponent', () => {
|
||||
let component: ProjectorContainerComponent;
|
||||
let fixture: ComponentFixture<ProjectorContainerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProjectorContainerComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectorContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-projector-container',
|
||||
templateUrl: './projector-container.component.html',
|
||||
styleUrls: ['./projector-container.component.css']
|
||||
})
|
||||
export class ProjectorContainerComponent implements OnInit {
|
||||
public constructor() {}
|
||||
|
||||
public ngOnInit(): void {}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { ProjectorContainerModule } from './projector-container.module';
|
||||
|
||||
describe('ProjectorContainerModule', () => {
|
||||
let projectorContainerModule: ProjectorContainerModule;
|
||||
|
||||
beforeEach(() => {
|
||||
projectorContainerModule = new ProjectorContainerModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(projectorContainerModule).toBeTruthy();
|
||||
});
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ProjectorContainerComponent } from './projector-container.component';
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { ProjectorComponent } from './projector/projector.component';
|
||||
import { ProjectorContainerRoutingModule } from './projector/projector-container.routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ProjectorContainerRoutingModule, SharedModule],
|
||||
declarations: [ProjectorContainerComponent, ProjectorComponent]
|
||||
})
|
||||
export class ProjectorContainerModule {}
|
@ -1,15 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { ProjectorContainerComponent } from '../projector-container.component';
|
||||
import { ProjectorComponent } from './projector.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ProjectorContainerComponent },
|
||||
{ path: 'real', component: ProjectorComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ProjectorContainerRoutingModule {}
|
@ -1,3 +0,0 @@
|
||||
<p>
|
||||
projector works!
|
||||
</p>
|
@ -1,19 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'os-projector',
|
||||
templateUrl: './projector.component.html',
|
||||
styleUrls: ['./projector.component.css']
|
||||
})
|
||||
export class ProjectorComponent extends BaseComponent implements OnInit {
|
||||
public constructor(titleService: Title, translate: TranslateService) {
|
||||
super(titleService, translate);
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Projector');
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<h2 mat-dialog-title>{{ projectable.getTitle() }}</h2>
|
||||
<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' : ''">
|
||||
<mat-checkbox [checked]="isProjectorSelected(projector)" (change)="toggleProjector(projector)">
|
||||
{{ projector.name | translate }}
|
||||
</mat-checkbox>
|
||||
<span *ngIf="isProjectedOn(projector)" class="right">
|
||||
<mat-icon>videocam</mat-icon>
|
||||
</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<mat-card *ngIf="options.length > 0">
|
||||
<mat-card-title>
|
||||
<span translate>Slide options</span>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<div *ngFor="let option of options">
|
||||
<div *ngIf="isDecisionOption(option)">
|
||||
<mat-checkbox [checked]="projectorElement[option.key]" (change)="projectorElement[option.key] = !projectorElement[option.key]">
|
||||
{{ option.displayName | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div *ngIf="isChoiceOption(option)">
|
||||
<h3>{{ option.displayName | translate }}</h3>
|
||||
<mat-radio-group [name]="option.key" [(ngModel)]="projectorElement[option.key]">
|
||||
<mat-radio-button *ngFor="let choice of option.choices" [value]="choice.value">
|
||||
{{ choice.displayName | translate }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onOk()" color="primary" translate>OK</button>
|
||||
<button mat-button (click)="onCancel()" translate>Cancel</button>
|
||||
</mat-dialog-actions>
|
@ -0,0 +1,19 @@
|
||||
mat-dialog-content {
|
||||
overflow: inherit;
|
||||
|
||||
div.projectors {
|
||||
padding: 15px;
|
||||
|
||||
&.projected {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.right {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, TestBed } from '@angular/core/testing';
|
||||
|
||||
// import { ProjectionDialogComponent } from './prjection-dialog.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
|
||||
describe('ProjectionDialogComponent', () => {
|
||||
// let component: ProjectionDialogComponent;
|
||||
// let fixture: ComponentFixture<ProjectionDialogComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
// TODO: You cannot create this component in the standard way. Needs different testing.
|
||||
beforeEach(() => {
|
||||
/*fixture = TestBed.createComponent(ProjectionDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();*/
|
||||
});
|
||||
|
||||
/*it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});*/
|
||||
});
|
@ -0,0 +1,96 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { Projectable } from 'app/site/base/projectable';
|
||||
import { DataStoreService } from 'app/core/services/data-store.service';
|
||||
import { Projector, ProjectorElement } from 'app/shared/models/core/projector';
|
||||
import { ProjectorService } from 'app/core/services/projector.service';
|
||||
import {
|
||||
ProjectorOption,
|
||||
isProjectorDecisionOption,
|
||||
isProjectorChoiceOption,
|
||||
ProjectorDecisionOption,
|
||||
ProjectorChoiceOption,
|
||||
ProjectorOptions
|
||||
} from 'app/site/base/projector-options';
|
||||
|
||||
export type ProjectionDialogReturnType = [Projector[], ProjectorElement];
|
||||
|
||||
/**
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-projection-dialog',
|
||||
templateUrl: './projection-dialog.component.html',
|
||||
styleUrls: ['./projection-dialog.component.scss']
|
||||
})
|
||||
export class ProjectionDialogComponent {
|
||||
public projectors: Projector[];
|
||||
private selectedProjectors: Projector[] = [];
|
||||
public projectorElement: ProjectorElement;
|
||||
public options: ProjectorOptions;
|
||||
|
||||
public constructor(
|
||||
public dialogRef: MatDialogRef<ProjectionDialogComponent, ProjectionDialogReturnType>,
|
||||
@Inject(MAT_DIALOG_DATA) public projectable: Projectable,
|
||||
private DS: DataStoreService,
|
||||
private projectorService: ProjectorService
|
||||
) {
|
||||
this.projectors = this.DS.getAll<Projector>('core/projector');
|
||||
// TODO: Maybe watch. But this may not be necessary for the short living time of this dialog.
|
||||
|
||||
this.selectedProjectors = this.projectorService.getProjectorsWhichAreProjecting(this.projectable);
|
||||
|
||||
// Add default projector, if the projectable is not projected on it.
|
||||
const defaultProjector: Projector = this.projectorService.getProjectorForDefault(
|
||||
this.projectable.getProjectionDefaultName()
|
||||
);
|
||||
if (!this.selectedProjectors.includes(defaultProjector)) {
|
||||
this.selectedProjectors.push(defaultProjector);
|
||||
}
|
||||
|
||||
this.projectorElement = {
|
||||
id: this.projectable.getIdForSlide(),
|
||||
name: this.projectable.getNameForSlide(),
|
||||
stable: this.projectable.isStableSlide()
|
||||
};
|
||||
|
||||
// Set option defaults
|
||||
this.projectable.getProjectorOptions().forEach(option => {
|
||||
this.projectorElement[option.key] = option.default;
|
||||
});
|
||||
|
||||
this.options = this.projectable.getProjectorOptions();
|
||||
}
|
||||
|
||||
public toggleProjector(projector: Projector): void {
|
||||
const index = this.selectedProjectors.indexOf(projector);
|
||||
if (index < 0) {
|
||||
this.selectedProjectors.push(projector);
|
||||
} else {
|
||||
this.selectedProjectors.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public isProjectorSelected(projector: Projector): boolean {
|
||||
return this.selectedProjectors.includes(projector);
|
||||
}
|
||||
|
||||
public isProjectedOn(projector: Projector): boolean {
|
||||
return this.projectorService.isProjectedOn(this.projectable, projector);
|
||||
}
|
||||
|
||||
public isDecisionOption(option: ProjectorOption): option is ProjectorDecisionOption {
|
||||
return isProjectorDecisionOption(option);
|
||||
}
|
||||
|
||||
public isChoiceOption(option: ProjectorOption): option is ProjectorChoiceOption {
|
||||
return isProjectorChoiceOption(option);
|
||||
}
|
||||
|
||||
public onOk(): void {
|
||||
this.dialogRef.close([this.selectedProjectors, this.projectorElement]);
|
||||
}
|
||||
|
||||
public onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<button type="button" mat-icon-button (click)="onClick($event)">
|
||||
<mat-icon>videocam</mat-icon>
|
||||
</button>
|
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { ProjectorButtonComponent } from './projector-button.component';
|
||||
|
||||
describe('ProjectorButtonComponent', () => {
|
||||
let component: ProjectorButtonComponent;
|
||||
let fixture: ComponentFixture<ProjectorButtonComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectorButtonComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,30 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Projectable } from 'app/site/base/projectable';
|
||||
import { ProjectionDialogService } from 'app/core/services/projection-dialog.service';
|
||||
|
||||
/**
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-projector-button',
|
||||
templateUrl: './projector-button.component.html',
|
||||
styleUrls: ['./projector-button.component.scss']
|
||||
})
|
||||
export class ProjectorButtonComponent implements OnInit {
|
||||
@Input()
|
||||
public object: Projectable;
|
||||
|
||||
/**
|
||||
* The consotructor
|
||||
*/
|
||||
public constructor(private projectionDialogService: ProjectionDialogService) {}
|
||||
|
||||
/**
|
||||
* Initialization function
|
||||
*/
|
||||
public ngOnInit(): void {}
|
||||
|
||||
public onClick(event: Event): void {
|
||||
event.stopPropagation();
|
||||
this.projectionDialogService.openProjectDialogFor(this.object);
|
||||
}
|
||||
}
|
59
client/src/app/shared/directives/resized.directive.ts
Normal file
59
client/src/app/shared/directives/resized.directive.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { OnInit, ElementRef, Directive, Input } from '@angular/core';
|
||||
import { ResizeSensor } from 'css-element-queries';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* This directive takes a Subject<void> as input and everytime the surrounding element
|
||||
* was resized, the subject is fired.
|
||||
*
|
||||
* Usage:
|
||||
* `<div [osRezised]="mySubject">...content...</div>`
|
||||
*/
|
||||
@Directive({
|
||||
selector: '[osResized]'
|
||||
})
|
||||
export class ResizedDirective implements OnInit {
|
||||
@Input()
|
||||
public osResized: Subject<void>;
|
||||
|
||||
/**
|
||||
* Old width, to check, if the width has actually changed.
|
||||
*/
|
||||
private oldWidth: number;
|
||||
|
||||
/**
|
||||
* Old height, to check, if the height has actually changed.
|
||||
*/
|
||||
private oldHeight: number;
|
||||
|
||||
public constructor(private element: ElementRef) {}
|
||||
|
||||
/**
|
||||
* Inits the ResizeSensor. triggers initial size change.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
new ResizeSensor(this.element.nativeElement, x => this.onSizeChanged());
|
||||
this.onSizeChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* The size has changed. Check, if the size actually hs changed. If so,
|
||||
* trigger the given subject.
|
||||
*/
|
||||
private onSizeChanged(): void {
|
||||
const newWidth = this.element.nativeElement.clientWidth;
|
||||
const newHeight = this.element.nativeElement.clientHeight;
|
||||
|
||||
if (newWidth === this.oldWidth && newHeight === this.oldHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.oldWidth = newWidth;
|
||||
this.oldHeight = newHeight;
|
||||
|
||||
if (this.osResized) {
|
||||
this.osResized.next();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { ProjectableBaseModel } from '../base/projectable-base-model';
|
||||
import { Speaker, SpeakerState } from './speaker';
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* The representation of the content object for agenda items. The unique combination
|
||||
@ -24,7 +24,7 @@ export const itemVisibilityChoices = [
|
||||
* Representations of agenda Item
|
||||
* @ignore
|
||||
*/
|
||||
export class Item extends ProjectableBaseModel {
|
||||
export class Item extends BaseModel<Item> {
|
||||
public id: number;
|
||||
public item_number: string;
|
||||
public title: string;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { AgendaInformation } from './agenda-information';
|
||||
import { ProjectableBaseModel } from './projectable-base-model';
|
||||
import { Searchable } from './searchable';
|
||||
import { SearchRepresentation } from '../../../core/services/search.service';
|
||||
import { BaseModel } from './base-model';
|
||||
|
||||
/**
|
||||
* A base model for models, that can be content objects in the agenda. Provides title and navigation
|
||||
* information for the agenda.
|
||||
*/
|
||||
export abstract class AgendaBaseModel extends ProjectableBaseModel implements AgendaInformation, Searchable {
|
||||
export abstract class AgendaBaseModel extends BaseModel<AgendaBaseModel> implements AgendaInformation, Searchable {
|
||||
/**
|
||||
* A model that can be a content object of an agenda item.
|
||||
* @param collectionString
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { BaseModel } from './base-model';
|
||||
import { Projectable } from './projectable';
|
||||
|
||||
export abstract class ProjectableBaseModel extends BaseModel<ProjectableBaseModel> implements Projectable {
|
||||
/**
|
||||
* A model which can be projected. This class give basic implementation for the projector.
|
||||
*
|
||||
* @param collectionString
|
||||
* @param verboseName
|
||||
* @param input
|
||||
*/
|
||||
protected constructor(collectionString: string, verboseName: string, input?: any) {
|
||||
super(collectionString, verboseName, input);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a Dummy, which should be changed if the projector gets implemented.
|
||||
*/
|
||||
public project(): void {}
|
||||
|
||||
/**
|
||||
* @returns the projector title.
|
||||
*/
|
||||
public getProjectorTitle(): string {
|
||||
return this.getTitle();
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Interface for every model, that should be projectable.
|
||||
*/
|
||||
export interface Projectable {
|
||||
/**
|
||||
* Should return the title for the projector.
|
||||
*/
|
||||
getProjectorTitle(): string;
|
||||
|
||||
/**
|
||||
* Dummy. I don't know how the projctor system will be, so this function may change
|
||||
*/
|
||||
project(): void;
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { ProjectableBaseModel } from '../base/projectable-base-model';
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a countdown
|
||||
* @ignore
|
||||
*/
|
||||
export class Countdown extends ProjectableBaseModel {
|
||||
export class Countdown extends BaseModel<Countdown> {
|
||||
public id: number;
|
||||
public description: string;
|
||||
public default_time: number;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ProjectableBaseModel } from '../base/projectable-base-model';
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a projector message.
|
||||
* @ignore
|
||||
*/
|
||||
export class ProjectorMessage extends ProjectableBaseModel {
|
||||
export class ProjectorMessage extends BaseModel<ProjectorMessage> {
|
||||
public id: number;
|
||||
public message: string;
|
||||
|
||||
|
@ -1,24 +1,104 @@
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* A projectorelement must have a name and optional attributes.
|
||||
* error is listed here, because this might be set by the server, if
|
||||
* something is wrong and I want you to be sensible about this.
|
||||
*/
|
||||
export interface ProjectorElement {
|
||||
/**
|
||||
* The name of the element.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* An optional error. If this is set, this element is invalid, so
|
||||
* DO NOT read additional data (name is save).
|
||||
*/
|
||||
error?: string;
|
||||
|
||||
/**
|
||||
* Additional data.
|
||||
*/
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple elements.
|
||||
*/
|
||||
export type ProjectorElements = ProjectorElement[];
|
||||
|
||||
/**
|
||||
* A projectiondefault
|
||||
*/
|
||||
export interface ProjectionDefault {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name: string;
|
||||
projector_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Representation of a projector. Has the nested property "projectiondefaults"
|
||||
* @ignore
|
||||
*/
|
||||
export class Projector extends BaseModel<Projector> {
|
||||
public id: number;
|
||||
public elements: Object;
|
||||
public elements: ProjectorElements;
|
||||
public scale: number;
|
||||
public scroll: number;
|
||||
public name: string;
|
||||
public blank: boolean;
|
||||
public width: number;
|
||||
public height: number;
|
||||
public projectiondefaults: Object[];
|
||||
public projectiondefaults: ProjectionDefault[];
|
||||
|
||||
public constructor(input?: any) {
|
||||
super('core/projector', 'Projector', input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if there is an element with the given name (and optionally
|
||||
* an id). If the id is given, the element to search MUST have this id.
|
||||
*
|
||||
* @param name The name of the element
|
||||
* @param id The optional id to check.
|
||||
* @returns true, if there is at least one element with the given name (and id).
|
||||
*/
|
||||
public isElementShown(name: string, id?: number): boolean {
|
||||
return this.elements.some(element => element.name === name && (!id || element.id === id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements, that do not have `stable=true`.
|
||||
*/
|
||||
public removeAllNonStableElements(): void {
|
||||
this.elements = this.elements.filter(element => element.stable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given element to the projectorelements
|
||||
*
|
||||
* @param element The element to add.
|
||||
*/
|
||||
public addElement<T extends ProjectorElement>(element: T): void {
|
||||
this.elements.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes elements given by the name and optional id. If no id is given
|
||||
* all elements with a matching name are removed.
|
||||
*
|
||||
* If an id is given, ut the element dies not specify an id, it will be removed.
|
||||
*
|
||||
* @param name The name to search
|
||||
* @param id The optional id to search.
|
||||
*/
|
||||
public removeElementByNameAndId(name: string, id?: number): void {
|
||||
this.elements = this.elements.filter(
|
||||
element => element.name !== name || (!id && !element.id && element.id !== id)
|
||||
);
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { File } from './file';
|
||||
import { ProjectableBaseModel } from '../base/projectable-base-model';
|
||||
import { Searchable } from '../base/searchable';
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* Representation of MediaFile. Has the nested property "File"
|
||||
* @ignore
|
||||
*/
|
||||
export class Mediafile extends ProjectableBaseModel implements Searchable {
|
||||
export class Mediafile extends BaseModel<Mediafile> implements Searchable {
|
||||
public id: number;
|
||||
public title: string;
|
||||
public mediafile: File;
|
||||
|
@ -13,6 +13,8 @@ import { MotionPoll } from './motion-poll';
|
||||
* @ignore
|
||||
*/
|
||||
export class Motion extends AgendaBaseModel {
|
||||
public static COLLECTIONSTRING = 'motions/motion';
|
||||
|
||||
public id: number;
|
||||
public identifier: string;
|
||||
public title: string;
|
||||
@ -45,7 +47,7 @@ export class Motion extends AgendaBaseModel {
|
||||
public last_modified: string;
|
||||
|
||||
public constructor(input?: any) {
|
||||
super('motions/motion', 'Motion', input);
|
||||
super(Motion.COLLECTIONSTRING, 'Motion', input);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ProjectableBaseModel } from '../base/projectable-base-model';
|
||||
import { Searchable } from '../base/searchable';
|
||||
import { SearchRepresentation } from '../../../core/services/search.service';
|
||||
import { BaseModel } from '../base/base-model';
|
||||
|
||||
/**
|
||||
* Representation of a user in contrast to the operator.
|
||||
* @ignore
|
||||
*/
|
||||
export class User extends ProjectableBaseModel implements Searchable {
|
||||
export class User extends BaseModel<User> implements Searchable {
|
||||
public static COLLECTIONSTRING = 'users/user';
|
||||
|
||||
public id: number;
|
||||
public username: string;
|
||||
public title: string;
|
||||
@ -25,7 +27,7 @@ export class User extends ProjectableBaseModel implements Searchable {
|
||||
public default_password: string;
|
||||
|
||||
public constructor(input?: any) {
|
||||
super('users/user', 'Participant', input);
|
||||
super(User.COLLECTIONSTRING, 'Participant', input);
|
||||
}
|
||||
|
||||
public get full_name(): string {
|
||||
|
@ -26,7 +26,8 @@ import {
|
||||
MatBadgeModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatBottomSheetModule
|
||||
MatBottomSheetModule,
|
||||
MatSliderModule
|
||||
} from '@angular/material';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatChipsModule } from '@angular/material';
|
||||
@ -73,6 +74,9 @@ import { OsSortBottomSheetComponent } from './components/os-sort-filter-bar/os-s
|
||||
import { FilterMenuComponent } from './components/os-sort-filter-bar/filter-menu/filter-menu.component';
|
||||
import { LogoComponent } from './components/logo/logo.component';
|
||||
import { C4DialogComponent, CopyrightSignComponent } from './components/copyright-sign/copyright-sign.component';
|
||||
import { ProjectorButtonComponent } from './components/projector-button/projector-button.component';
|
||||
import { ProjectionDialogComponent } from './components/projection-dialog/projection-dialog.component';
|
||||
import { ResizedDirective } from './directives/resized.directive';
|
||||
|
||||
/**
|
||||
* Share Module for all "dumb" components and pipes.
|
||||
@ -121,6 +125,7 @@ import { C4DialogComponent, CopyrightSignComponent } from './components/copyrigh
|
||||
MatButtonToggleModule,
|
||||
MatStepperModule,
|
||||
MatTabsModule,
|
||||
MatSliderModule,
|
||||
DragDropModule,
|
||||
TranslateModule.forChild(),
|
||||
RouterModule,
|
||||
@ -160,6 +165,7 @@ import { C4DialogComponent, CopyrightSignComponent } from './components/copyrigh
|
||||
MatRadioModule,
|
||||
MatButtonToggleModule,
|
||||
MatStepperModule,
|
||||
MatSliderModule,
|
||||
DragDropModule,
|
||||
NgxMatSelectSearchModule,
|
||||
FileDropModule,
|
||||
@ -179,7 +185,10 @@ import { C4DialogComponent, CopyrightSignComponent } from './components/copyrigh
|
||||
OsSortFilterBarComponent,
|
||||
LogoComponent,
|
||||
CopyrightSignComponent,
|
||||
C4DialogComponent
|
||||
C4DialogComponent,
|
||||
ProjectorButtonComponent,
|
||||
ProjectionDialogComponent,
|
||||
ResizedDirective
|
||||
],
|
||||
declarations: [
|
||||
PermsDirective,
|
||||
@ -199,7 +208,10 @@ import { C4DialogComponent, CopyrightSignComponent } from './components/copyrigh
|
||||
FilterMenuComponent,
|
||||
LogoComponent,
|
||||
CopyrightSignComponent,
|
||||
C4DialogComponent
|
||||
C4DialogComponent,
|
||||
ProjectorButtonComponent,
|
||||
ProjectionDialogComponent,
|
||||
ResizedDirective
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: OpenSlidesDateAdapter },
|
||||
|
@ -66,7 +66,7 @@ export class AgendaRepositoryService extends BaseRepository<ViewItem, Item> {
|
||||
throw new Error(
|
||||
`The content object (${agendaItem.content_object.collection}, ${
|
||||
agendaItem.content_object.id
|
||||
}) of item ${agendaItem.id} is not a BaseProjectableModel.`
|
||||
}) of item ${agendaItem.id} is not a AgendaBaseModel.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,8 @@
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- slector column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment" class="checkbox-cell">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let assignment" class="icon-cell">
|
||||
<mat-icon>{{ isSelected(assignment) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
47
client/src/app/site/base/base-projectable-model.ts
Normal file
47
client/src/app/site/base/base-projectable-model.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Projectable } from './projectable';
|
||||
import { BaseViewModel } from './base-view-model';
|
||||
import { ProjectorOptions } from './projector-options';
|
||||
|
||||
/**
|
||||
* Base view class for projectable models.
|
||||
*/
|
||||
export abstract class BaseProjectableModel extends BaseViewModel implements Projectable {
|
||||
/**
|
||||
* Per default, a slide does not have any options
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public getProjectorOptions(): ProjectorOptions {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public abstract getProjectionDefaultName(): string;
|
||||
|
||||
/**
|
||||
* The id should match the model's id.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public getIdForSlide(): number {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* A model s return the collection string
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public abstract getNameForSlide(): string;
|
||||
|
||||
/**
|
||||
* Per default a model is a non-stable element.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
public isStableSlide(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
32
client/src/app/site/base/projectable.ts
Normal file
32
client/src/app/site/base/projectable.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ProjectorOptions } from './projector-options';
|
||||
import { Displayable } from 'app/shared/models/base/displayable';
|
||||
|
||||
/**
|
||||
* Interface for every model, that should be projectable.
|
||||
*/
|
||||
export interface Projectable extends Displayable {
|
||||
/**
|
||||
* All options for the slide
|
||||
*/
|
||||
getProjectorOptions(): ProjectorOptions;
|
||||
|
||||
/**
|
||||
* The projection default name for the slide
|
||||
*/
|
||||
getProjectionDefaultName(): string;
|
||||
|
||||
/**
|
||||
* The (optional) id for the slide
|
||||
*/
|
||||
getIdForSlide(): number | null;
|
||||
|
||||
/**
|
||||
* The slide's name
|
||||
*/
|
||||
getNameForSlide(): string;
|
||||
|
||||
/**
|
||||
* The stable attribute for the slide.
|
||||
*/
|
||||
isStableSlide(): boolean;
|
||||
}
|
32
client/src/app/site/base/projector-options.ts
Normal file
32
client/src/app/site/base/projector-options.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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
|
||||
);
|
||||
}
|
@ -1,17 +1,11 @@
|
||||
import { AppConfig } from '../base/app-config';
|
||||
import { Projector } from '../../shared/models/core/projector';
|
||||
import { Countdown } from '../../shared/models/core/countdown';
|
||||
import { ChatMessage } from '../../shared/models/core/chat-message';
|
||||
import { ProjectorMessage } from '../../shared/models/core/projector-message';
|
||||
import { Tag } from '../../shared/models/core/tag';
|
||||
|
||||
export const CommonAppConfig: AppConfig = {
|
||||
name: 'common',
|
||||
models: [
|
||||
{ collectionString: 'core/projector', model: Projector },
|
||||
{ collectionString: 'core/chat-message', model: ChatMessage },
|
||||
{ collectionString: 'core/countdown', model: Countdown },
|
||||
{ collectionString: 'core/projector-message', model: ProjectorMessage },
|
||||
{ collectionString: 'core/tag', model: Tag }
|
||||
],
|
||||
mainMenuEntries: [
|
||||
|
@ -9,7 +9,7 @@ export const ConfigAppConfig: AppConfig = {
|
||||
route: '/settings',
|
||||
displayName: 'Settings',
|
||||
icon: 'settings',
|
||||
weight: 700,
|
||||
weight: 1300,
|
||||
permission: 'core.can_manage_config'
|
||||
}
|
||||
]
|
||||
|
@ -55,8 +55,8 @@
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector Column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let item" class="checkbox-cell" (click)="selectItem(item, $event)">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let item" class="icon-cell" (click)="selectItem(item, $event)">
|
||||
<mat-icon>{{ isSelected(item) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
@ -15,7 +15,7 @@
|
||||
<!-- Prefix field -->
|
||||
<mat-form-field>
|
||||
<input formControlName="prefix" matInput placeholder="{{'Prefix' | translate}}" required>
|
||||
<mat-hint *ngIf="!createForm.controls.name.valid">
|
||||
<mat-hint *ngIf="!createForm.controls.prefix.valid">
|
||||
<span translate>Required</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
|
@ -30,7 +30,7 @@
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-button (click)="create()">
|
||||
<span translate>Save</span>
|
||||
<span translate>Create</span>
|
||||
</button>
|
||||
<button mat-button (click)="commentSectionToCreate = null">
|
||||
<span translate>Cancel</span>
|
||||
|
@ -160,7 +160,7 @@
|
||||
<os-personal-note *ngIf="!editMotion" [motion]="motion"></os-personal-note>
|
||||
<ng-container *ngTemplateOutlet="motionLogTemplate"></ng-container>
|
||||
</div>
|
||||
<div class="desktop-right ">
|
||||
<div class="desktop-right">
|
||||
<!-- Content -->
|
||||
<mat-card [ngClass]="editMotion ? 'os-form-card' : 'os-card'">
|
||||
<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>
|
||||
|
@ -30,6 +30,14 @@
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Projector column -->
|
||||
<ng-container matColumnDef="projector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell>
|
||||
<mat-cell *matCellDef="let motion" class="icon-cell">
|
||||
<os-projector-button [object]="motion"></os-projector-button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- identifier column -->
|
||||
<ng-container matColumnDef="identifier">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Identifier</mat-header-cell>
|
||||
|
@ -34,15 +34,16 @@ export class MotionListComponent extends ListViewBaseComponent<ViewMotion> imple
|
||||
/**
|
||||
* Use for minimal width. Please note the 'selector' row for multiSelect mode,
|
||||
* to be able to display an indicator for the state of selection
|
||||
* TODO: Remove projector, if columnsToDisplayFullWidth is used..
|
||||
*/
|
||||
public columnsToDisplayMinWidth = ['identifier', 'title', 'state', 'speakers'];
|
||||
public columnsToDisplayMinWidth = ['projector', 'identifier', 'title', 'state', 'speakers'];
|
||||
|
||||
/**
|
||||
* Use for maximal width. Please note the 'selector' row for multiSelect mode,
|
||||
* to be able to display an indicator for the state of selection
|
||||
* TODO: Needs vp.desktop check
|
||||
*/
|
||||
public columnsToDisplayFullWidth = ['identifier', 'title', 'state', 'speakers'];
|
||||
public columnsToDisplayFullWidth = ['projector', 'identifier', 'title', 'state', 'speakers'];
|
||||
|
||||
/**
|
||||
* Value of the configuration variable `motions_statutes_enabled` - are statutes enabled?
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { BaseProjectableModel } from 'app/site/base/base-projectable-model';
|
||||
import { Category } from '../../../shared/models/motions/category';
|
||||
import { MotionComment } from '../../../shared/models/motions/motion-comment';
|
||||
import { Item } from 'app/shared/models/agenda/item';
|
||||
import { Mediafile } from 'app/shared/models/mediafiles/mediafile';
|
||||
import { Motion } from '../../../shared/models/motions/motion';
|
||||
import { MotionBlock } from 'app/shared/models/motions/motion-block';
|
||||
import { MotionComment } from '../../../shared/models/motions/motion-comment';
|
||||
import { PersonalNoteContent } from 'app/shared/models/users/personal-note';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
import { ViewMotionCommentSection } from './view-motion-comment-section';
|
||||
import { Workflow } from '../../../shared/models/motions/workflow';
|
||||
import { WorkflowState } from '../../../shared/models/motions/workflow-state';
|
||||
import { ProjectorOptions } from 'app/site/base/projector-options';
|
||||
|
||||
/**
|
||||
* The line numbering mode for the motion detail view.
|
||||
@ -39,7 +40,7 @@ export enum ChangeRecoMode {
|
||||
* Provides "safe" access to variables and functions in {@link Motion}
|
||||
* @ignore
|
||||
*/
|
||||
export class ViewMotion extends BaseViewModel {
|
||||
export class ViewMotion extends BaseProjectableModel {
|
||||
protected _motion: Motion;
|
||||
protected _category: Category;
|
||||
protected _submitters: User[];
|
||||
@ -436,6 +437,30 @@ export class ViewMotion extends BaseViewModel {
|
||||
return this.amendment_paragraphs.length > 0;
|
||||
}
|
||||
|
||||
public getProjectorOptions(): ProjectorOptions {
|
||||
return [
|
||||
{
|
||||
key: 'mode',
|
||||
displayName: 'Mode',
|
||||
default: 'original',
|
||||
choices: [
|
||||
{ value: 'original', displayName: 'Original' },
|
||||
{ value: 'changed', displayName: 'Changed' },
|
||||
{ value: 'diff', displayName: 'Diff' },
|
||||
{ value: 'agreed', displayName: 'Agreed' }
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
public getProjectionDefaultName(): string {
|
||||
return 'motions';
|
||||
}
|
||||
|
||||
public getNameForSlide(): string {
|
||||
return Motion.COLLECTIONSTRING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate this motion into a copy of itself
|
||||
*/
|
||||
|
@ -0,0 +1,5 @@
|
||||
<div id="container" #container [osResized]="resizeSubject">
|
||||
<div id="projector" [ngStyle]="projectorStyle">
|
||||
<os-projector [projector]="projector"></os-projector>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,14 @@
|
||||
#container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #222;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#projector {
|
||||
width: 500px;
|
||||
margin: auto auto;
|
||||
min-width: 1px;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FullscreenProjectorComponent } from './fullscreen-projector.component';
|
||||
import { E2EImportsModule } from 'e2e-imports.module';
|
||||
import { ProjectorModule } from '../../projector.module';
|
||||
|
||||
describe('FullscreenProjectorComponent', () => {
|
||||
let component: FullscreenProjectorComponent;
|
||||
let fixture: ComponentFixture<FullscreenProjectorComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule, ProjectorModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FullscreenProjectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
fit('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,139 @@
|
||||
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
|
||||
import { AuthService } from 'app/core/services/auth.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { OperatorService } from 'app/core/services/operator.service';
|
||||
import { ViewProjector } from 'app/site/projector/models/view-projector';
|
||||
import { Size } from 'app/site/projector/size';
|
||||
import { ProjectorRepositoryService } from 'app/site/projector/services/projector-repository.service';
|
||||
|
||||
/**
|
||||
* The fullscreen projector. Bootstraps OpenSlides, gets the requested projector,
|
||||
* holds the projector component and fits it to the screen.
|
||||
*
|
||||
* DO NOT use this component in the site!
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-fullscreen-projector',
|
||||
templateUrl: './fullscreen-projector.component.html',
|
||||
styleUrls: ['./fullscreen-projector.component.scss']
|
||||
})
|
||||
export class FullscreenProjectorComponent implements OnInit {
|
||||
// TODO: isLoading, canSeeProjector and other issues must be displayed!
|
||||
public isLoading = true;
|
||||
public canSeeProjector = false;
|
||||
|
||||
/**
|
||||
* The id of the projector given by the url.
|
||||
*/
|
||||
public projectorId: number;
|
||||
|
||||
/**
|
||||
* The projector from the datastore.
|
||||
*/
|
||||
public projector: ViewProjector | null;
|
||||
|
||||
/**
|
||||
* Saves the projectorsize. It's used to check, if the size has changed
|
||||
* on a projector update.
|
||||
*/
|
||||
private oldProjectorSize: Size = { width: 0, height: 0 };
|
||||
|
||||
/**
|
||||
* This subject fires, if the container changes it's size.
|
||||
*/
|
||||
public resizeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* Used to give the projector the right size.
|
||||
*/
|
||||
public projectorStyle = {
|
||||
width: '100px' // Some default. Will be overwritten soon.
|
||||
};
|
||||
|
||||
/**
|
||||
* The container to get the window size.
|
||||
*/
|
||||
@ViewChild('container')
|
||||
private containerElement: ElementRef;
|
||||
|
||||
/**
|
||||
* Constructor. Updates the projector dimensions on a resize.
|
||||
*
|
||||
* @param auth
|
||||
* @param route
|
||||
* @param operator
|
||||
* @param repo
|
||||
*/
|
||||
public constructor(
|
||||
auth: AuthService, // Needed tro trigger loading of OpenSlides. Starts the Bootup process.
|
||||
private route: ActivatedRoute,
|
||||
private operator: OperatorService,
|
||||
private repo: ProjectorRepositoryService
|
||||
) {
|
||||
this.resizeSubject.subscribe(() => {
|
||||
this.updateProjectorDimensions();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the projector id from the URL. Loads the projector.
|
||||
* Subscribes to the operator to get his/her permissions.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.route.params.subscribe(params => {
|
||||
this.loadProjector(parseInt(params.id, 10) || 1);
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
this.operator.getObservable().subscribe(() => {
|
||||
this.canSeeProjector = this.operator.hasPerms('projector.can_see');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the projector.
|
||||
*
|
||||
* @param projectorId: The projector id for the projector to load.
|
||||
*/
|
||||
private loadProjector(projectorId: number): void {
|
||||
this.projectorId = projectorId;
|
||||
// TODO: what happens on delete?
|
||||
|
||||
// Watches the projector. Update the container size, if the projector size changes.
|
||||
this.repo.getViewModelObservable(this.projectorId).subscribe(projector => {
|
||||
this.projector = projector;
|
||||
if (
|
||||
projector &&
|
||||
(projector.width !== this.oldProjectorSize.width || projector.height !== this.oldProjectorSize.height)
|
||||
) {
|
||||
this.oldProjectorSize.width = projector.height;
|
||||
this.oldProjectorSize.height = projector.height;
|
||||
this.updateProjectorDimensions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fits the projector into the container.
|
||||
*/
|
||||
private updateProjectorDimensions(): void {
|
||||
if (!this.containerElement || !this.projector) {
|
||||
return;
|
||||
}
|
||||
const projectorAspectRatio = this.projector.width / this.projector.height;
|
||||
const windowAspectRatio =
|
||||
this.containerElement.nativeElement.offsetWidth / this.containerElement.nativeElement.offsetHeight;
|
||||
|
||||
if (projectorAspectRatio >= windowAspectRatio) {
|
||||
// full width
|
||||
this.projectorStyle.width = `${this.containerElement.nativeElement.offsetWidth}px`;
|
||||
} else {
|
||||
// full height
|
||||
const width = Math.floor(this.containerElement.nativeElement.offsetHeight * projectorAspectRatio);
|
||||
this.projectorStyle.width = `${width}px`;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<os-head-bar [nav]="false" [goBack]="true">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>{{ projector?.name | translate }}</h2>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="column-left">
|
||||
<div id="projector">
|
||||
<os-projector [projector]="projector"></os-projector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column-right">
|
||||
<div class="control-group">
|
||||
{{ projector?.scroll }}
|
||||
<button type="button" mat-icon-button (click)="scroll(scrollScaleDirection.Up)">
|
||||
<mat-icon>arrow_upward</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button (click)="scroll(scrollScaleDirection.Down)">
|
||||
<mat-icon>arrow_downward</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button (click)="scroll(scrollScaleDirection.Reset)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="control-group">
|
||||
{{ projector?.scale }}
|
||||
<button type="button" mat-icon-button (click)="scale(scrollScaleDirection.Up)">
|
||||
<mat-icon>zoom_in</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button (click)="scale(scrollScaleDirection.Down)">
|
||||
<mat-icon>zoom_out</mat-icon>
|
||||
</button>
|
||||
<button type="button" mat-icon-button (click)="scale(scrollScaleDirection.Reset)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,20 @@
|
||||
#projector {
|
||||
width: 100%; /*1000px;*/
|
||||
}
|
||||
|
||||
.column-left {
|
||||
display: inline-block;
|
||||
padding-top: 20px;
|
||||
width: 70%;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.column-right {
|
||||
padding-top: 20px;
|
||||
min-width: calc(30% - 25px);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { ProjectorDetailComponent } from './projector-detail.component';
|
||||
import { ProjectorModule } from '../../projector.module';
|
||||
|
||||
describe('ProjectorDetailComponent', () => {
|
||||
let component: ProjectorDetailComponent;
|
||||
let fixture: ComponentFixture<ProjectorDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule, ProjectorModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectorDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,71 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ProjectorRepositoryService, ScrollScaleDirection } from '../../services/projector-repository.service';
|
||||
import { ViewProjector } from '../../models/view-projector';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
|
||||
/**
|
||||
* The projector detail view.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-projector-detail',
|
||||
templateUrl: './projector-detail.component.html',
|
||||
styleUrls: ['./projector-detail.component.scss']
|
||||
})
|
||||
export class ProjectorDetailComponent extends BaseViewComponent implements OnInit {
|
||||
/**
|
||||
* The projector to show.
|
||||
*/
|
||||
public projector: ViewProjector;
|
||||
|
||||
public scrollScaleDirection = ScrollScaleDirection;
|
||||
|
||||
/**
|
||||
* @param titleService
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @param repo
|
||||
* @param route
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: ProjectorRepositoryService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the projector and subscribes to it.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Projector');
|
||||
this.route.params.subscribe(params => {
|
||||
const projectorId = parseInt(params.id, 10) || 1;
|
||||
this.repo.getViewModelObservable(projectorId).subscribe(projector => (this.projector = projector));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scroll
|
||||
* @param direction The direction to send.
|
||||
*/
|
||||
public scroll(direction: ScrollScaleDirection): void {
|
||||
this.repo.scroll(this.projector, direction).then(null, this.raiseError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the scale
|
||||
* @param direction The direction to send.
|
||||
*/
|
||||
public scale(direction: ScrollScaleDirection): void {
|
||||
this.repo.scale(this.projector, direction).then(null, this.raiseError);
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
<os-head-bar [nav]="true" [mainButton]="true" (mainEvent)="onPlusButton()">
|
||||
<!-- Title -->
|
||||
<div class="title-slot">
|
||||
<h2 translate>Projectors</h2>
|
||||
</div>
|
||||
|
||||
<!-- Menu -->
|
||||
<div class="menu-slot">
|
||||
<div *osPerms="'core.can_manage_projector'">
|
||||
<button type="button" mat-icon-button [matMenuTriggerFor]="ellipsisMenu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</os-head-bar>
|
||||
|
||||
<mat-card *ngIf="projectorToCreate">
|
||||
<mat-card-title translate>New Projector</mat-card-title>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="createForm" (keydown)="keyDownFunction($event)">
|
||||
<p>
|
||||
<mat-form-field>
|
||||
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
|
||||
<mat-hint *ngIf="!createForm.controls.name.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>Create</span>
|
||||
</button>
|
||||
<button mat-button (click)="projectorToCreate = null">
|
||||
<span translate>Cancel</span>
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<div id="card-wrapper">
|
||||
<mat-card class="projector-card" *ngFor="let projector of projectors">
|
||||
<mat-card-title>
|
||||
<a [routerLink]="['/projector-site/detail', projector.id]">
|
||||
{{ projector.name }}
|
||||
</a>
|
||||
|
||||
<div class="card-actions">
|
||||
<button mat-icon-button *ngIf="editId !== projector.id" (click)=onEditButton(projector)>
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button *ngIf="editId === projector.id" (click)=onCancelButton(projector)>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button *ngIf="editId === projector.id" (click)=onSaveButton(projector)>
|
||||
<mat-icon>save</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button mat-button (click)=onDeleteButton(projector)>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</mat-card-title>
|
||||
<mat-card-content>
|
||||
<p>TODO: projector</p>
|
||||
<ng-container *ngIf="editId === projector.id">
|
||||
<form [formGroup]="updateForm" (keydown)="keyDownFunction($event, projector)">
|
||||
<p>
|
||||
<!-- Name field -->
|
||||
<mat-form-field>
|
||||
<input formControlName="name" matInput placeholder="{{'Name' | translate}}" required>
|
||||
<mat-hint *ngIf="!createForm.controls.name.valid">
|
||||
<span translate>Required</span>
|
||||
</mat-hint>
|
||||
</mat-form-field>
|
||||
</p><p>
|
||||
<!-- Aspect ratio field -->
|
||||
<mat-radio-group formControlName="aspectRatio" [name]="projector.id">
|
||||
<mat-radio-button *ngFor="let ratio of aspectRatiosKeys" [value]="ratio">
|
||||
{{ ratio }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</p><p>
|
||||
<mat-slider [thumbLabel]="true" formControlName="width" min="800" max="3840" step="10"></mat-slider>
|
||||
{{ updateForm.value.width }}
|
||||
</p>
|
||||
</form>
|
||||
</ng-container>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
<mat-menu #ellipsisMenu="matMenu">
|
||||
<button mat-menu-item>
|
||||
<mat-icon>note</mat-icon>
|
||||
<span translate>Projector messages</span>
|
||||
</button>
|
||||
<button mat-menu-item>
|
||||
<mat-icon>alarm</mat-icon>
|
||||
<span translate>Countdowns</span>
|
||||
</button>
|
||||
</mat-menu>
|
@ -0,0 +1,13 @@
|
||||
#card-wrapper {
|
||||
margin-top: 10px;
|
||||
|
||||
.projector-card {
|
||||
margin: 20px;
|
||||
width: 300px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectorListComponent } from './projector-list.component';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { ProjectorModule } from '../../projector.module';
|
||||
|
||||
describe('ProjectorListComponent', () => {
|
||||
let component: ProjectorListComponent;
|
||||
let fixture: ComponentFixture<ProjectorListComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule, ProjectorModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProjectorListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,225 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { ProjectorRepositoryService } from '../../services/projector-repository.service';
|
||||
import { ViewProjector } from '../../models/view-projector';
|
||||
import { Projector } from 'app/shared/models/core/projector';
|
||||
import { BaseViewComponent } from 'app/site/base/base-view';
|
||||
import { MatSnackBar } from '@angular/material';
|
||||
import { PromptService } from 'app/core/services/prompt.service';
|
||||
|
||||
/**
|
||||
* All supported aspect rations for projectors.
|
||||
*/
|
||||
const aspectRatios: { [ratio: string]: number } = {
|
||||
'4:3': 4 / 3,
|
||||
'16:9': 16 / 9,
|
||||
'16:10': 16 / 10
|
||||
};
|
||||
|
||||
/**
|
||||
* List for all projectors.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-projector-list',
|
||||
templateUrl: './projector-list.component.html',
|
||||
styleUrls: ['./projector-list.component.scss']
|
||||
})
|
||||
export class ProjectorListComponent extends BaseViewComponent implements OnInit {
|
||||
/**
|
||||
* This member is set, if the user is creating a new projector.
|
||||
*/
|
||||
public projectorToCreate: Projector | null;
|
||||
|
||||
/**
|
||||
* The create form.
|
||||
*/
|
||||
public createForm: FormGroup;
|
||||
|
||||
/**
|
||||
* The update form. Will be refreahed for each projector. Just one update
|
||||
* form can be shown per time.
|
||||
*/
|
||||
public updateForm: FormGroup;
|
||||
|
||||
/**
|
||||
* The id of the currently edited projector.
|
||||
*/
|
||||
public editId: number | null = null;
|
||||
|
||||
/**
|
||||
* All aspect ratio keys/strings for the UI.
|
||||
*/
|
||||
public aspectRatiosKeys: string[];
|
||||
|
||||
/**
|
||||
* All projectors.
|
||||
*/
|
||||
public projectors: ViewProjector[];
|
||||
|
||||
/**
|
||||
* Constructor. Initializes all forms.
|
||||
*
|
||||
* @param titleService
|
||||
* @param translate
|
||||
* @param matSnackBar
|
||||
* @param repo
|
||||
* @param formBuilder
|
||||
* @param promptService
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
matSnackBar: MatSnackBar,
|
||||
private repo: ProjectorRepositoryService,
|
||||
private formBuilder: FormBuilder,
|
||||
private promptService: PromptService
|
||||
) {
|
||||
super(titleService, translate, matSnackBar);
|
||||
|
||||
this.aspectRatiosKeys = Object.keys(aspectRatios);
|
||||
|
||||
this.createForm = this.formBuilder.group({
|
||||
name: ['', Validators.required]
|
||||
});
|
||||
this.updateForm = this.formBuilder.group({
|
||||
name: ['', Validators.required],
|
||||
aspectRatio: ['', Validators.required],
|
||||
width: [0, Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches all projectors.
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
super.setTitle('Projectors');
|
||||
this.repo.getViewModelListObservable().subscribe(projectors => (this.projectors = projectors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the create form.
|
||||
*/
|
||||
public onPlusButton(): void {
|
||||
if (!this.projectorToCreate) {
|
||||
this.projectorToCreate = new Projector();
|
||||
this.createForm.setValue({ name: '' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the comment section from the create form.
|
||||
*/
|
||||
public create(): void {
|
||||
if (this.createForm.valid && this.projectorToCreate) {
|
||||
this.projectorToCreate.patchValues(this.createForm.value as Projector);
|
||||
this.repo.create(this.projectorToCreate).then(() => (this.projectorToCreate = null), this.raiseError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event on Key Down in update or create form.
|
||||
*
|
||||
* @param event the keyboard event
|
||||
* @param the current view in scope
|
||||
*/
|
||||
public keyDownFunction(event: KeyboardEvent, projector?: ViewProjector): void {
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
if (projector) {
|
||||
this.onSaveButton(projector);
|
||||
} else {
|
||||
this.create();
|
||||
}
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
if (projector) {
|
||||
this.onCancelButton(projector);
|
||||
} else {
|
||||
this.projectorToCreate = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the aspect ratio of the given projector.
|
||||
* If no matching ratio is found, the first ratio is returned.
|
||||
*
|
||||
* @param projector The projector to check
|
||||
* @returns the found ratio key.
|
||||
*/
|
||||
public getAspectRatioKey(projector: ViewProjector): string {
|
||||
const ratio = projector.width / projector.height;
|
||||
const RATIO_ENVIRONMENT = 0.05;
|
||||
const foundRatioKey = Object.keys(aspectRatios).find(key => {
|
||||
const value = aspectRatios[key];
|
||||
return value >= ratio - RATIO_ENVIRONMENT && value <= ratio + RATIO_ENVIRONMENT;
|
||||
});
|
||||
if (!foundRatioKey) {
|
||||
return Object.keys(aspectRatios)[0];
|
||||
} else {
|
||||
return foundRatioKey;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts editing for the given projector.
|
||||
*
|
||||
* @param projector The projector to edit
|
||||
*/
|
||||
public onEditButton(projector: ViewProjector): void {
|
||||
if (this.editId !== null) {
|
||||
return;
|
||||
}
|
||||
this.editId = projector.id;
|
||||
this.updateForm.reset();
|
||||
this.updateForm.patchValue({
|
||||
name: projector.name,
|
||||
aspectRatio: this.getAspectRatioKey(projector),
|
||||
width: projector.width
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current editing.
|
||||
* @param projector the projector
|
||||
*/
|
||||
public onCancelButton(projector: ViewProjector): void {
|
||||
if (projector.id !== this.editId) {
|
||||
return;
|
||||
}
|
||||
this.editId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the projector
|
||||
*
|
||||
* @param projector The projector to save.
|
||||
*/
|
||||
public onSaveButton(projector: ViewProjector): void {
|
||||
if (projector.id !== this.editId || !this.updateForm.valid) {
|
||||
return;
|
||||
}
|
||||
const updateProjector: Partial<Projector> = {
|
||||
name: this.updateForm.value.name,
|
||||
width: this.updateForm.value.width,
|
||||
height: Math.round(this.updateForm.value.width / aspectRatios[this.updateForm.value.aspectRatio])
|
||||
};
|
||||
this.repo.update(updateProjector, projector);
|
||||
this.editId = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the projector.
|
||||
*
|
||||
* @param projector The projector to delete
|
||||
*/
|
||||
public async onDeleteButton(projector: ViewProjector): Promise<void> {
|
||||
const content = this.translate.instant('Delete') + ` ${projector.name}?`;
|
||||
if (await this.promptService.open('Are you sure?', content)) {
|
||||
this.repo.delete(projector).then(null, this.raiseError);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<div id="container" [osResized]="resizeSubject" [ngStyle]="containerStyle" #container>
|
||||
<div id="projector" [ngStyle]="projectorStyle">
|
||||
<div class="header" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter">
|
||||
<div *ngIf="enableTitle">
|
||||
Header Title
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngFor="let slide of slides" class="content">
|
||||
<os-slide-container [slideData]="slide" [scroll]="scroll" [scale]="scale"></os-slide-container>
|
||||
</div>
|
||||
|
||||
<div class="footer" [ngStyle]="headerFooterStyle" *ngIf="enableHeaderAndFooter">
|
||||
Footer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,36 @@
|
||||
#container {
|
||||
background-color: lightgoldenrodyellow;
|
||||
position: relative;
|
||||
|
||||
#projector {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform-origin: left top;
|
||||
overflow: hidden;
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
z-index: 1;
|
||||
}
|
||||
.content {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.footer {
|
||||
position: absolute;
|
||||
color: white;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ProjectorComponent } from './projector.component';
|
||||
import { E2EImportsModule } from '../../../e2e-imports.module';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { ProjectorModule } from '../../projector.module';
|
||||
|
||||
describe('ProjectorComponent', () => {
|
||||
let component: ProjectorComponent;
|
||||
@ -9,8 +10,7 @@ describe('ProjectorComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [ProjectorComponent]
|
||||
imports: [E2EImportsModule, ProjectorModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
@ -0,0 +1,236 @@
|
||||
import { Component, Input, ViewChild, ElementRef, OnDestroy } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { ViewProjector } from '../../models/view-projector';
|
||||
import { ProjectorDataService, SlideData } from '../../services/projector-data.service';
|
||||
import { Subscription, Subject } from 'rxjs';
|
||||
import { ProjectorRepositoryService } from '../../services/projector-repository.service';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
import { Size } from '../../size';
|
||||
|
||||
/**
|
||||
* THE projector. Cares about scaling and the right size and resolution.
|
||||
* Watches the given projector and creates slide-containers for each projectorelement.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-projector',
|
||||
templateUrl: './projector.component.html',
|
||||
styleUrls: ['./projector.component.scss']
|
||||
})
|
||||
export class ProjectorComponent extends BaseComponent implements OnDestroy {
|
||||
/**
|
||||
* The current projector id.
|
||||
*/
|
||||
private projectorId: number | null = null;
|
||||
|
||||
/**
|
||||
* The projector. Accessors are below.
|
||||
*/
|
||||
private _projector: ViewProjector;
|
||||
|
||||
@Input()
|
||||
public set projector(projector: ViewProjector) {
|
||||
this._projector = projector;
|
||||
// check, if ID changed:
|
||||
const newId = projector ? projector.id : null;
|
||||
if (this.projectorId !== newId) {
|
||||
this.projectorIdChanged(this.projectorId, newId);
|
||||
this.projectorId = newId;
|
||||
}
|
||||
|
||||
// Update scaling, if projector is set.
|
||||
if (projector) {
|
||||
const oldSize: Size = { ...this.currentProjectorSize };
|
||||
this.currentProjectorSize.height = projector.height;
|
||||
this.currentProjectorSize.width = projector.width;
|
||||
if (
|
||||
oldSize.height !== this.currentProjectorSize.height ||
|
||||
oldSize.width !== this.currentProjectorSize.width
|
||||
) {
|
||||
this.updateScaling();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get projector(): ViewProjector {
|
||||
return this._projector;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current projector size. This is for checking,
|
||||
* if the size actually has changed.
|
||||
*/
|
||||
private currentProjectorSize: Size = { width: 0, height: 0 };
|
||||
|
||||
/**
|
||||
* Ths subscription to the projectordata.
|
||||
*/
|
||||
private dataSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* The container element. THis is neede to get the size of the element,
|
||||
* in which the projector must fit and be scaled to.
|
||||
*/
|
||||
@ViewChild('container')
|
||||
private containerElement: ElementRef;
|
||||
|
||||
/**
|
||||
* Dynamic style attributes for the projector.
|
||||
*/
|
||||
public projectorStyle: {
|
||||
transform?: string;
|
||||
width: string;
|
||||
height: string;
|
||||
'background-color': string;
|
||||
} = {
|
||||
width: '0px',
|
||||
height: '0px',
|
||||
'background-color': 'white'
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic style attributes for the header and footer.
|
||||
*/
|
||||
public headerFooterStyle: { 'background-color': string; color: string } = {
|
||||
'background-color': 'blue',
|
||||
color: 'white'
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic style attributes for the container.
|
||||
*/
|
||||
public containerStyle: { height?: string } = {};
|
||||
|
||||
/**
|
||||
* All slides to show on this projector
|
||||
*/
|
||||
public slides: SlideData<object>[] = [];
|
||||
|
||||
/**
|
||||
* The scroll for this projector. 0 is the default.
|
||||
*/
|
||||
public scroll = 0;
|
||||
|
||||
/**
|
||||
* The scale for this projector. 0 is the default.
|
||||
*/
|
||||
public scale = 0;
|
||||
|
||||
/**
|
||||
* The subscription to the projector.
|
||||
*/
|
||||
private projectorSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* A subject that fires, if the container is resized.
|
||||
*/
|
||||
public resizeSubject = new Subject<void>();
|
||||
|
||||
// Some settings for the view from the config.
|
||||
public enableHeaderAndFooter = true;
|
||||
public enableTitle = true;
|
||||
public enableLogo = true;
|
||||
|
||||
/**
|
||||
* Listen to all related config variables. Register the resizeSubject.
|
||||
*
|
||||
* @param titleService
|
||||
* @param translate
|
||||
* @param projectorDataService
|
||||
* @param projectorRepository
|
||||
* @param configService
|
||||
*/
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
private projectorDataService: ProjectorDataService,
|
||||
private projectorRepository: ProjectorRepositoryService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
|
||||
// Get all important config variables.
|
||||
this.configService
|
||||
.get<boolean>('projector_enable_header_footer')
|
||||
.subscribe(val => (this.enableHeaderAndFooter = val));
|
||||
this.configService.get<boolean>('projector_enable_title').subscribe(val => (this.enableTitle = val));
|
||||
this.configService
|
||||
.get<string>('projector_header_fontcolor')
|
||||
.subscribe(val => (this.headerFooterStyle.color = val));
|
||||
this.configService
|
||||
.get<string>('projector_header_backgroundcolor')
|
||||
.subscribe(val => (this.headerFooterStyle['background-color'] = val));
|
||||
this.configService.get<boolean>('projector_enable_logo').subscribe(val => (this.enableLogo = val));
|
||||
this.configService
|
||||
.get<string>('projector_background_color')
|
||||
.subscribe(val => (this.projectorStyle['background-color'] = val));
|
||||
|
||||
// Watches for resizing of the container.
|
||||
this.resizeSubject.subscribe(() => {
|
||||
if (this.containerElement) {
|
||||
this.updateScaling();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the projector to the right format.
|
||||
*/
|
||||
private updateScaling(): void {
|
||||
if (
|
||||
!this.containerElement ||
|
||||
this.currentProjectorSize.width === 0 ||
|
||||
this.containerElement.nativeElement.offsetWidth === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const scale = this.containerElement.nativeElement.offsetWidth / this.currentProjectorSize.width;
|
||||
if (isNaN(scale)) {
|
||||
return;
|
||||
}
|
||||
this.projectorStyle.transform = 'scale(' + scale + ')';
|
||||
this.projectorStyle.width = this.currentProjectorSize.width + 'px';
|
||||
this.projectorStyle.height = this.currentProjectorSize.height + 'px';
|
||||
this.containerStyle.height = Math.round(scale * this.currentProjectorSize.height) + 'px';
|
||||
}
|
||||
|
||||
/**
|
||||
* Called, if the projector id changes.
|
||||
*/
|
||||
private projectorIdChanged(from: number, to: number): void {
|
||||
// Unsubscribe form data and projector subscriptions.
|
||||
if (this.dataSubscription) {
|
||||
this.dataSubscription.unsubscribe();
|
||||
}
|
||||
if (this.projectorSubscription) {
|
||||
this.projectorSubscription.unsubscribe();
|
||||
}
|
||||
if (to > 0) {
|
||||
if (from > 0) {
|
||||
this.projectorDataService.projectorClosed(from);
|
||||
}
|
||||
|
||||
this.dataSubscription = this.projectorDataService
|
||||
.getProjectorObservable(to)
|
||||
.subscribe(data => (this.slides = data || []));
|
||||
this.projectorSubscription = this.projectorRepository.getViewModelObservable(to).subscribe(projector => {
|
||||
this.scroll = projector.scroll;
|
||||
this.scale = projector.scale;
|
||||
});
|
||||
} else if (!to && from > 0) {
|
||||
// no new projector
|
||||
this.projectorDataService.projectorClosed(from);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister the projector from the projectordataservice.
|
||||
*/
|
||||
public ngOnDestroy(): void {
|
||||
if (this.projectorId > 0) {
|
||||
this.projectorDataService.projectorClosed(this.projectorId);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<div id="slide" [ngStyle]="slideStyle"><ng-container #slide></ng-container></div>
|
@ -0,0 +1,3 @@
|
||||
::ng-deep #slide {
|
||||
z-index: 5;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SlideContainerComponent } from './slide-container.component';
|
||||
import { E2EImportsModule } from '../../../../../e2e-imports.module';
|
||||
import { ProjectorModule } from '../../projector.module';
|
||||
|
||||
describe('SlideContainerComponent', () => {
|
||||
let component: SlideContainerComponent;
|
||||
let fixture: ComponentFixture<SlideContainerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule, ProjectorModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SlideContainerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,157 @@
|
||||
import { Component, Input, ViewChild, ViewContainerRef, ComponentRef } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BaseComponent } from 'app/base.component';
|
||||
import { SlideData } from '../../services/projector-data.service';
|
||||
import { DynamicSlideLoader } from 'app/slides/services/dynamic-slide-loader.service';
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { SlideOptions } from 'app/slides/slide-manifest';
|
||||
import { ConfigService } from 'app/core/services/config.service';
|
||||
|
||||
/**
|
||||
* Container for one slide. Cares about the position (scale, scroll) in the projector,
|
||||
* and loading of slides.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-slide-container',
|
||||
templateUrl: './slide-container.component.html',
|
||||
styleUrls: ['./slide-container.component.scss']
|
||||
})
|
||||
export class SlideContainerComponent extends BaseComponent {
|
||||
private previousSlideName: string;
|
||||
|
||||
@ViewChild('slide', { read: ViewContainerRef })
|
||||
private slide: ViewContainerRef;
|
||||
private slideRef: ComponentRef<BaseSlideComponent<object>>;
|
||||
|
||||
/**
|
||||
* The data for this slide. Will be accessed below.
|
||||
*/
|
||||
private _slideData: SlideData<object>;
|
||||
|
||||
@Input()
|
||||
public set slideData(data: SlideData<object>) {
|
||||
// If there is no ata or an error, clear and exit.
|
||||
if (!data || data.error) {
|
||||
// clear slide container:
|
||||
if (this.slide) {
|
||||
this.slide.clear();
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
console.error(data.error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._slideData = data;
|
||||
if (this.previousSlideName !== data.element.name) {
|
||||
this.slideChanged(data.element.name);
|
||||
this.previousSlideName = data.element.name;
|
||||
}
|
||||
this.setDataForComponent();
|
||||
}
|
||||
|
||||
public get slideData(): SlideData<object> {
|
||||
return this._slideData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current projector scroll.
|
||||
*/
|
||||
private _scroll: number;
|
||||
|
||||
/**
|
||||
* Updates the slideStyle, when the scroll changes.
|
||||
*/
|
||||
@Input()
|
||||
public set scroll(value: number) {
|
||||
this._scroll = value;
|
||||
this.updateScroll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the slideStyle, when the scale changes.
|
||||
*/
|
||||
@Input()
|
||||
public set scale(value: number) {
|
||||
if (this.slideOptions.scaleable) {
|
||||
value *= 10;
|
||||
value += 100;
|
||||
this.slideStyle['font-size'] = `${value}%`;
|
||||
} else {
|
||||
this.slideStyle['font-size'] = '100%';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The current slideoptions.
|
||||
*/
|
||||
private slideOptions: SlideOptions = { scaleable: false, scrollable: false };
|
||||
|
||||
/**
|
||||
* Styles for scaling and scrolling.
|
||||
*/
|
||||
public slideStyle: { 'font-size': string; 'margin-top': string } = {
|
||||
'font-size': '100%',
|
||||
'margin-top': '50px'
|
||||
};
|
||||
|
||||
/**
|
||||
* Variable, if the projector header is enabled.
|
||||
*/
|
||||
private headerEnabled = true;
|
||||
|
||||
public constructor(
|
||||
titleService: Title,
|
||||
translate: TranslateService,
|
||||
private dynamicSlideLoader: DynamicSlideLoader,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
super(titleService, translate);
|
||||
|
||||
this.configService.get<boolean>('projector_enable_header_footer').subscribe(val => {
|
||||
this.headerEnabled = val;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the 'margin-top' attribute in the slide styles.
|
||||
*/
|
||||
private updateScroll(): void {
|
||||
if (this.slideOptions.scrollable) {
|
||||
let value = this._scroll;
|
||||
value *= -50;
|
||||
if (this.headerEnabled) {
|
||||
value += 50; // Default offset for the header
|
||||
}
|
||||
this.slideStyle['margin-top'] = `${value}px`;
|
||||
} else {
|
||||
this.slideStyle['margin-top'] = '0px';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the slides via the dynamicSlideLoader. Creates the slide components and provide the slide data to it.
|
||||
*
|
||||
* @param slideName The slide to load.
|
||||
*/
|
||||
private slideChanged(slideName: string): void {
|
||||
this.slideOptions = this.dynamicSlideLoader.getSlideOptions(slideName);
|
||||
this.dynamicSlideLoader.getSlideFactory(slideName).then(slideFactory => {
|
||||
this.slide.clear();
|
||||
this.slideRef = this.slide.createComponent(slideFactory);
|
||||
this.setDataForComponent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* "injects" the slide data into the slide component.
|
||||
*/
|
||||
private setDataForComponent(): void {
|
||||
if (this.slideRef && this.slideRef.instance) {
|
||||
this.slideRef.instance.data = this.slideData;
|
||||
}
|
||||
}
|
||||
}
|
51
client/src/app/site/projector/models/view-projector.ts
Normal file
51
client/src/app/site/projector/models/view-projector.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { Projector, ProjectorElements } from 'app/shared/models/core/projector';
|
||||
|
||||
export class ViewProjector extends BaseViewModel {
|
||||
private _projector: Projector;
|
||||
|
||||
public get projector(): Projector {
|
||||
return this._projector ? this._projector : null;
|
||||
}
|
||||
|
||||
public get id(): number {
|
||||
return this.projector ? this.projector.id : null;
|
||||
}
|
||||
|
||||
public get name(): string {
|
||||
return this.projector ? this.projector.name : null;
|
||||
}
|
||||
|
||||
public get elements(): ProjectorElements {
|
||||
return this.projector ? this.projector.elements : null;
|
||||
}
|
||||
|
||||
public get height(): number {
|
||||
return this.projector ? this.projector.height : null;
|
||||
}
|
||||
|
||||
public get width(): number {
|
||||
return this.projector ? this.projector.width : null;
|
||||
}
|
||||
|
||||
public get scale(): number {
|
||||
return this.projector ? this.projector.scale : null;
|
||||
}
|
||||
|
||||
public get scroll(): number {
|
||||
return this.projector ? this.projector.scroll : null;
|
||||
}
|
||||
|
||||
public constructor(projector?: Projector) {
|
||||
super();
|
||||
this._projector = projector;
|
||||
}
|
||||
|
||||
public getTitle(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public updateValues(projector: Projector): void {
|
||||
console.log('Update projector TODO with vals:', projector);
|
||||
}
|
||||
}
|
21
client/src/app/site/projector/projector-routing.module.ts
Normal file
21
client/src/app/site/projector/projector-routing.module.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { ProjectorListComponent } from './components/projector-list/projector-list.component';
|
||||
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'list',
|
||||
component: ProjectorListComponent
|
||||
},
|
||||
{
|
||||
path: 'detail/:id',
|
||||
component: ProjectorDetailComponent
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ProjectorRoutingModule {}
|
22
client/src/app/site/projector/projector.config.ts
Normal file
22
client/src/app/site/projector/projector.config.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { AppConfig } from '../base/app-config';
|
||||
import { Projector } from 'app/shared/models/core/projector';
|
||||
import { Countdown } from 'app/shared/models/core/countdown';
|
||||
import { ProjectorMessage } from 'app/shared/models/core/projector-message';
|
||||
|
||||
export const ProjectorAppConfig: AppConfig = {
|
||||
name: 'projector',
|
||||
models: [
|
||||
{ collectionString: 'core/projector', model: Projector },
|
||||
{ collectionString: 'core/countdown', model: Countdown },
|
||||
{ collectionString: 'core/projector-message', model: ProjectorMessage }
|
||||
],
|
||||
mainMenuEntries: [
|
||||
{
|
||||
route: '/projector-site/list',
|
||||
displayName: 'Projector',
|
||||
icon: 'videocam',
|
||||
weight: 700,
|
||||
permission: 'core.can_see_projector'
|
||||
}
|
||||
]
|
||||
};
|
13
client/src/app/site/projector/projector.module.spec.ts
Normal file
13
client/src/app/site/projector/projector.module.spec.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ProjectorModule } from './projector.module';
|
||||
|
||||
describe('ProjectorModule', () => {
|
||||
let projectorModule: ProjectorModule;
|
||||
|
||||
beforeEach(() => {
|
||||
projectorModule = new ProjectorModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(projectorModule).toBeTruthy();
|
||||
});
|
||||
});
|
22
client/src/app/site/projector/projector.module.ts
Normal file
22
client/src/app/site/projector/projector.module.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ProjectorRoutingModule } from './projector-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { ProjectorComponent } from './components/projector/projector.component';
|
||||
import { ProjectorListComponent } from './components/projector-list/projector-list.component';
|
||||
import { ProjectorDetailComponent } from './components/projector-detail/projector-detail.component';
|
||||
import { SlideContainerComponent } from './components/slide-container/slide-container.component';
|
||||
import { FullscreenProjectorComponent } from './components/fullscreen-projector/fullscreen-projector.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, ProjectorRoutingModule, SharedModule],
|
||||
declarations: [
|
||||
ProjectorComponent,
|
||||
ProjectorListComponent,
|
||||
ProjectorDetailComponent,
|
||||
SlideContainerComponent,
|
||||
FullscreenProjectorComponent
|
||||
]
|
||||
})
|
||||
export class ProjectorModule {}
|
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||
import { ProjectorDataService } from './projector-data.service';
|
||||
|
||||
describe('ProjectorDataService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectorDataService]
|
||||
});
|
||||
});
|
||||
it('should be created', inject([ProjectorDataService], (service: ProjectorDataService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
103
client/src/app/site/projector/services/projector-data.service.ts
Normal file
103
client/src/app/site/projector/services/projector-data.service.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WebsocketService } from 'app/core/services/websocket.service';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
import { ProjectorElement } from 'app/shared/models/core/projector';
|
||||
|
||||
export interface SlideData<T = object> {
|
||||
data: T;
|
||||
element: ProjectorElement;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ProjectorData = SlideData[];
|
||||
|
||||
interface AllProjectorData {
|
||||
[id: number]: ProjectorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* This service handles the websocket connection for the projector data.
|
||||
* Each projector instance registers itself by calling `getProjectorObservable`.
|
||||
* A projector should deregister itself, when the component is destroyed.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectorDataService {
|
||||
/**
|
||||
* Counts the open projector instances per projector id.
|
||||
*/
|
||||
private openProjectorInstances: { [id: number]: number } = {};
|
||||
|
||||
/**
|
||||
* Holds the current projector data for each projector.
|
||||
*/
|
||||
private currentProjectorData: { [id: number]: BehaviorSubject<ProjectorData | null> } = {};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param websocketService
|
||||
*/
|
||||
public constructor(private websocketService: WebsocketService) {
|
||||
// TODO: On reconnect, we do need to re-inform the server about all needed projectors. This also
|
||||
// updates our projector data, which is great!
|
||||
this.websocketService.getOberservable('projector').subscribe((update: AllProjectorData) => {
|
||||
Object.keys(update).forEach(_id => {
|
||||
const id = parseInt(_id, 10);
|
||||
if (this.currentProjectorData[id]) {
|
||||
this.currentProjectorData[id].next(update[id]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an observable for the projector data.
|
||||
*
|
||||
* @param projectorId The requested projector
|
||||
* @return an observable for the projector data of the given projector.
|
||||
*/
|
||||
public getProjectorObservable(projectorId: number): Observable<ProjectorData | null> {
|
||||
// Count projectors.
|
||||
if (!this.openProjectorInstances[projectorId]) {
|
||||
this.openProjectorInstances[projectorId] = 1;
|
||||
if (!this.currentProjectorData[projectorId]) {
|
||||
this.currentProjectorData[projectorId] = new BehaviorSubject<ProjectorData | null>(null);
|
||||
}
|
||||
} else {
|
||||
this.openProjectorInstances[projectorId]++;
|
||||
}
|
||||
|
||||
// Projector opened the first time.
|
||||
if (this.openProjectorInstances[projectorId] === 1) {
|
||||
this.updateProjectorDataSubscription();
|
||||
}
|
||||
return this.currentProjectorData[projectorId].asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe data from the server, if the last projector was closed.
|
||||
*
|
||||
* @param projectorId the projector.
|
||||
*/
|
||||
public projectorClosed(projectorId: number): void {
|
||||
if (this.openProjectorInstances[projectorId]) {
|
||||
this.openProjectorInstances[projectorId]--;
|
||||
}
|
||||
if (this.openProjectorInstances[projectorId] === 0) {
|
||||
this.updateProjectorDataSubscription();
|
||||
this.currentProjectorData[projectorId].next(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets initial data and keeps reuesting data.
|
||||
*/
|
||||
private updateProjectorDataSubscription(): void {
|
||||
const allActiveProjectorIds = Object.keys(this.openProjectorInstances)
|
||||
.map(id => parseInt(id, 10))
|
||||
.filter(id => this.openProjectorInstances[id] > 0);
|
||||
this.websocketService.send('listenToProjectors', { projector_ids: allActiveProjectorIds });
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ProjectorRepositoryService } from './projector-repository.service';
|
||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||
|
||||
describe('GroupRepositoryService', () => {
|
||||
beforeEach(() =>
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
providers: [ProjectorRepositoryService]
|
||||
})
|
||||
);
|
||||
|
||||
it('should be created', inject([ProjectorRepositoryService], (service: ProjectorRepositoryService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
@ -0,0 +1,115 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BaseRepository } from '../../base/base-repository';
|
||||
import { CollectionStringModelMapperService } from '../../../core/services/collectionStringModelMapper.service';
|
||||
import { DataSendService } from '../../../core/services/data-send.service';
|
||||
import { DataStoreService } from '../../../core/services/data-store.service';
|
||||
import { Identifiable } from '../../../shared/models/base/identifiable';
|
||||
import { ViewProjector } from '../models/view-projector';
|
||||
import { Projector } from '../../../shared/models/core/projector';
|
||||
import { HttpService } from 'app/core/services/http.service';
|
||||
|
||||
/**
|
||||
* Directions for scale and scroll requests.
|
||||
*/
|
||||
export enum ScrollScaleDirection {
|
||||
Up = 'up',
|
||||
Down = 'down',
|
||||
Reset = 'reset'
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages all projector instances.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Projector> {
|
||||
/**
|
||||
* Constructor calls the parent constructor
|
||||
*
|
||||
* @param DS The DataStore
|
||||
* @param mapperService Maps collection strings to classes
|
||||
* @param dataSend sending changed objects
|
||||
* @param http
|
||||
*/
|
||||
public constructor(
|
||||
DS: DataStoreService,
|
||||
mapperService: CollectionStringModelMapperService,
|
||||
private dataSend: DataSendService,
|
||||
private http: HttpService
|
||||
) {
|
||||
super(DS, mapperService, Projector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new projector. Adds the clock as default, stable element
|
||||
*/
|
||||
public async create(projectorData: Partial<Projector>): Promise<Identifiable> {
|
||||
const projector = new Projector();
|
||||
projector.patchValues(projectorData);
|
||||
projector.elements = [{ name: 'core/clock', stable: true }];
|
||||
return await this.dataSend.createModel(projector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a projector.
|
||||
*/
|
||||
public async update(projectorData: Partial<Projector>, viewProjector: ViewProjector): Promise<void> {
|
||||
const projector = new Projector();
|
||||
projector.patchValues(viewProjector.projector);
|
||||
projector.patchValues(projectorData);
|
||||
await this.dataSend.updateModel(projector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given projector.
|
||||
*
|
||||
* @param projector
|
||||
*/
|
||||
public async delete(projector: ViewProjector): Promise<void> {
|
||||
await this.dataSend.deleteModel(projector.projector);
|
||||
}
|
||||
|
||||
public createViewModel(projector: Projector): ViewProjector {
|
||||
return new ViewProjector(projector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the given projector.
|
||||
*
|
||||
* @param projector The projector to scroll
|
||||
* @param direction The direction.
|
||||
*/
|
||||
public async scroll(projector: ViewProjector, direction: ScrollScaleDirection): Promise<void> {
|
||||
this.controlView(projector, direction, 'scroll');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scale the given projector.
|
||||
*
|
||||
* @param projector The projector to scale
|
||||
* @param direction The direction.
|
||||
*/
|
||||
public async scale(projector: ViewProjector, direction: ScrollScaleDirection): Promise<void> {
|
||||
this.controlView(projector, direction, 'scale');
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the view of a projector.
|
||||
*
|
||||
* @param projector The projector to control.
|
||||
* @param direction The direction
|
||||
* @param action The action. Can be scale or scroll.
|
||||
*/
|
||||
private async controlView(
|
||||
projector: ViewProjector,
|
||||
direction: ScrollScaleDirection,
|
||||
action: 'scale' | 'scroll'
|
||||
): Promise<void> {
|
||||
this.http.post(`/rest/core/projector/${projector.id}/control_view`, {
|
||||
action: action,
|
||||
direction: direction
|
||||
});
|
||||
}
|
||||
}
|
4
client/src/app/site/projector/size.ts
Normal file
4
client/src/app/site/projector/size.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
@ -50,6 +50,10 @@ const routes: Routes = [
|
||||
{
|
||||
path: 'history',
|
||||
loadChildren: './history/history.module#HistoryModule'
|
||||
},
|
||||
{
|
||||
path: 'projector-site',
|
||||
loadChildren: './projector/projector.module#ProjectorModule'
|
||||
}
|
||||
],
|
||||
canActivateChild: [AuthGuard]
|
||||
|
@ -71,17 +71,6 @@
|
||||
<span>{{ entry.displayName | translate }}</span>
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
[@navItemAnim]
|
||||
*osPerms="'core.can_see_projector'"
|
||||
mat-list-item
|
||||
routerLink="/projector"
|
||||
routerLinkActive="active"
|
||||
(click)="mobileAutoCloseNav()"
|
||||
>
|
||||
<mat-icon>videocam</mat-icon>
|
||||
<span translate>Projector</span>
|
||||
</a>
|
||||
<mat-divider></mat-divider>
|
||||
<a
|
||||
[@navItemAnim]
|
||||
|
@ -22,12 +22,20 @@
|
||||
<mat-table class="os-listview-table on-transition-fade" [dataSource]="dataSource" matSort>
|
||||
<!-- Selector column -->
|
||||
<ng-container matColumnDef="selector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="checkbox-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)" class="checkbox-cell">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell"></mat-header-cell>
|
||||
<mat-cell *matCellDef="let user" (click)="selectItem(user, $event)" class="icon-cell">
|
||||
<mat-icon>{{ isSelected(user) ? 'check_circle' : '' }}</mat-icon>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Projector column -->
|
||||
<ng-container matColumnDef="projector">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header class="icon-cell">Projector</mat-header-cell>
|
||||
<mat-cell *matCellDef="let user" class="icon-cell">
|
||||
<os-projector-button [object]="user"></os-projector-button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- name column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
|
||||
|
@ -226,7 +226,8 @@ export class UserListComponent extends ListViewBaseComponent<ViewUser> implement
|
||||
* @returns column definition
|
||||
*/
|
||||
public getColumnDefinition(): string[] {
|
||||
const columns = ['name', 'group', 'presence'];
|
||||
// TODO: no projector in mobile view.
|
||||
const columns = ['projector', 'name', 'group', 'presence'];
|
||||
if (this.isMultiSelect) {
|
||||
return ['selector'].concat(columns);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { BaseViewModel } from '../../base/base-view-model';
|
||||
import { User } from '../../../shared/models/users/user';
|
||||
import { Group } from '../../../shared/models/users/group';
|
||||
import { BaseModel } from '../../../shared/models/base/base-model';
|
||||
import { BaseProjectableModel } from 'app/site/base/base-projectable-model';
|
||||
|
||||
export class ViewUser extends BaseViewModel {
|
||||
export class ViewUser extends BaseProjectableModel {
|
||||
private _user: User;
|
||||
private _groups: Group[];
|
||||
|
||||
@ -105,6 +105,18 @@ export class ViewUser extends BaseViewModel {
|
||||
this._groups = groups;
|
||||
}
|
||||
|
||||
public getProjectionDefaultName(): string {
|
||||
return 'users';
|
||||
}
|
||||
|
||||
public getNameForSlide(): string {
|
||||
return User.COLLECTIONSTRING;
|
||||
}
|
||||
|
||||
public isStableSlide(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* required by BaseViewModel. Don't confuse with the users title.
|
||||
*/
|
||||
|
21
client/src/app/slides/all-slides.ts
Normal file
21
client/src/app/slides/all-slides.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { SlideManifest } from './slide-manifest';
|
||||
|
||||
/**
|
||||
* Here, all slides has to be registered.
|
||||
*/
|
||||
export const allSlides: SlideManifest[] = [
|
||||
{
|
||||
slideName: 'motions/motion',
|
||||
path: 'motions/motion',
|
||||
loadChildren: './slides/motions/motion/motions-motion-slide.module#MotionsMotionSlideModule',
|
||||
scaleable: true,
|
||||
scrollable: true
|
||||
},
|
||||
{
|
||||
slideName: 'users/user',
|
||||
path: 'users/user',
|
||||
loadChildren: './slides/users/user/users-user-slide.module#UsersUserSlideModule',
|
||||
scaleable: false,
|
||||
scrollable: false
|
||||
}
|
||||
];
|
16
client/src/app/slides/base-slide-component.ts
Normal file
16
client/src/app/slides/base-slide-component.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Input } from '@angular/core';
|
||||
import { SlideData } from 'app/site/projector/services/projector-data.service';
|
||||
|
||||
/**
|
||||
* Every slide has to extends this base class. It forces the slides
|
||||
* to have an input for the slidedata.
|
||||
*/
|
||||
export abstract class BaseSlideComponent<T extends object> {
|
||||
/**
|
||||
* Each slide must take slide data.
|
||||
*/
|
||||
@Input()
|
||||
public data: SlideData<T>;
|
||||
|
||||
public constructor() {}
|
||||
}
|
22
client/src/app/slides/base-slide-module.ts
Normal file
22
client/src/app/slides/base-slide-module.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/compiler/src/core';
|
||||
import { Type } from '@angular/core';
|
||||
|
||||
import { SharedModule } from 'app/shared/shared.module';
|
||||
import { SLIDE } from './slide-token';
|
||||
import { BaseSlideComponent } from './base-slide-component';
|
||||
|
||||
/**
|
||||
* Generates the configuration for a slide module.
|
||||
*
|
||||
* @param slideComponent The component
|
||||
* @return the Module configuration fo rthe slide module.
|
||||
*/
|
||||
export function makeSlideModule<T extends BaseSlideComponent<object>>(slideComponent: Type<T>): NgModule {
|
||||
return {
|
||||
imports: [CommonModule, SharedModule],
|
||||
declarations: [slideComponent],
|
||||
providers: [{ provide: SLIDE, useValue: slideComponent }],
|
||||
entryComponents: [slideComponent]
|
||||
};
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export interface MotionsMotionSlideData {
|
||||
test: string;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<div>
|
||||
Motion Slide
|
||||
<h1>TEST</h1>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
div {
|
||||
background-color: red;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MotionsMotionSlideComponent } from './motions-motion-slide.component';
|
||||
import { E2EImportsModule } from '../../../../e2e-imports.module';
|
||||
|
||||
describe('MotionsMotionSlideComponent', () => {
|
||||
let component: MotionsMotionSlideComponent;
|
||||
let fixture: ComponentFixture<MotionsMotionSlideComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [E2EImportsModule],
|
||||
declarations: [MotionsMotionSlideComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MotionsMotionSlideComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BaseSlideComponent } from 'app/slides/base-slide-component';
|
||||
import { MotionsMotionSlideData } from './motions-motion-slide-model';
|
||||
|
||||
@Component({
|
||||
selector: 'os-motions-motion-slide',
|
||||
templateUrl: './motions-motion-slide.component.html',
|
||||
styleUrls: ['./motions-motion-slide.component.scss']
|
||||
})
|
||||
export class MotionsMotionSlideComponent extends BaseSlideComponent<MotionsMotionSlideData> implements OnInit {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public ngOnInit(): void {
|
||||
console.log('Hello from motion slide');
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import { MotionsMotionSlideModule } from './motions-motion-slide.module';
|
||||
|
||||
describe('MotionsMotionSlideModule', () => {
|
||||
let motionsMotionSlideModule: MotionsMotionSlideModule;
|
||||
|
||||
beforeEach(() => {
|
||||
motionsMotionSlideModule = new MotionsMotionSlideModule();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
expect(motionsMotionSlideModule).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,7 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { MotionsMotionSlideComponent } from './motions-motion-slide.component';
|
||||
import { makeSlideModule } from 'app/slides/base-slide-module';
|
||||
|
||||
@NgModule(makeSlideModule(MotionsMotionSlideComponent))
|
||||
export class MotionsMotionSlideModule {}
|
@ -0,0 +1,78 @@
|
||||
import { Injectable, Inject, Injector, NgModuleFactoryLoader, ComponentFactory, Type } from '@angular/core';
|
||||
|
||||
import { SlideManifest, SlideOptions } from '../slide-manifest';
|
||||
import { SLIDE } from '../slide-token';
|
||||
import { SLIDE_MANIFESTS } from '../slide-manifest';
|
||||
import { BaseSlideComponent } from '../base-slide-component';
|
||||
|
||||
/**
|
||||
* Cares about loading slides dynamically.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DynamicSlideLoader {
|
||||
public constructor(
|
||||
@Inject(SLIDE_MANIFESTS) private manifests: SlideManifest[],
|
||||
private loader: NgModuleFactoryLoader,
|
||||
private injector: Injector
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Searches the manifest for the given slide name.
|
||||
*
|
||||
* TODO: Improve by loading all manifests in an object with the
|
||||
* slide name as keys in the constructor. It's just a lookup here, then.
|
||||
*
|
||||
* @param slideName The slide to look up.
|
||||
* @returns the slide's manifest.
|
||||
*/
|
||||
private getManifest(slideName: string): SlideManifest {
|
||||
const manifest = this.manifests.find(m => m.slideName === slideName);
|
||||
|
||||
if (!manifest) {
|
||||
throw new Error(`Could not find slide for "${slideName}"`);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slide options for a given slide.
|
||||
*
|
||||
* @param slideName The slide
|
||||
* @returns SlideOptions for the requested slide.
|
||||
*/
|
||||
public getSlideOptions(slideName: string): SlideOptions {
|
||||
return this.getManifest(slideName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronically load the slide's component factory, which is used to create
|
||||
* the slide component.
|
||||
*
|
||||
* @param slideName The slide to search.
|
||||
*/
|
||||
public async getSlideFactory<T extends BaseSlideComponent<object>>(
|
||||
slideName: string
|
||||
): Promise<ComponentFactory<T>> {
|
||||
const manifest = this.getManifest(slideName);
|
||||
|
||||
// Load the module factory.
|
||||
return this.loader.load(manifest.loadChildren).then(ngModuleFactory => {
|
||||
// create the module
|
||||
const moduleRef = ngModuleFactory.create(this.injector);
|
||||
|
||||
// Get the slide provided by the SLIDE-injectiontoken.
|
||||
let dynamicComponentType: Type<T>;
|
||||
try {
|
||||
// Read from the moduleRef injector and locate the dynamic component type
|
||||
dynamicComponentType = moduleRef.injector.get(SLIDE);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'The module for Slide "' + slideName + '" is not configured right: Make usage of makeSlideModule.'
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
// Resolve this component factory
|
||||
return moduleRef.componentFactoryResolver.resolveComponentFactory<T>(dynamicComponentType);
|
||||
});
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user