diff --git a/projects/player/src/app/app.component.ts b/projects/player/src/app/app.component.ts index 919dfe48d88020de823a4ee0a0bcf7b79784f34a..6537a2ff9eb9332325de4568c071cd97a3aeffbb 100644 --- a/projects/player/src/app/app.component.ts +++ b/projects/player/src/app/app.component.ts @@ -1,18 +1,23 @@ import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { Unit } from '../../../common/unit'; +import { Unit, UnitPage } from '../../../common/unit'; import { FormService } from '../../../common/form.service'; import { FormControlElement, ValueChangeElement } from '../../../common/form'; import { VeronaSubscriptionService } from './services/verona-subscription.service'; import { VeronaPostService } from './services/verona-post.service'; import { NativeEventService } from './services/native-event.service'; +import { + PlayerConfig, RunningState, + VopNavigationDeniedNotification, + VopStartCommand +} from './models/verona'; @Component({ selector: 'player-aspect', template: ` <form [formGroup]="form"> <mat-tab-group mat-align-tabs="start"> - <mat-tab *ngFor="let page of unit.pages; let i = index" label="Seite {{i+1}}"> + <mat-tab *ngFor="let page of pages; let i = index" label="Seite {{i+1}}"> <app-page [parentForm]="form" [page]="page"></app-page> </mat-tab> </mat-tab-group> @@ -24,28 +29,27 @@ import { NativeEventService } from './services/native-event.service'; }) export class AppComponent { form: FormGroup = new FormGroup({}); - unit: Unit = { - pages: [] - }; + pages: UnitPage[] = []; + playerConfig!: PlayerConfig; constructor(private formService: FormService, private veronaSubscriptionService: VeronaSubscriptionService, private veronaPostService: VeronaPostService, private nativeEventService: NativeEventService) { - this.subscribe(); + this.initSubscriptions(); veronaPostService.sendVopReadyNotification(); } - private subscribe(): void { + private initSubscriptions(): void { this.formService.elementValueChanged .subscribe((value: ValueChangeElement): void => this.onElementValueChanges(value)); this.formService.controlAdded .subscribe((control: FormControlElement): void => this.addControl(control)); this.veronaSubscriptionService.vopStartCommand - .subscribe((data: any): void => this.onStart(data)); + .subscribe((message: VopStartCommand): void => this.onStart(message)); this.veronaSubscriptionService.vopNavigationDeniedNotification - .subscribe((data: any): void => this.onNavigationDenied(data)); + .subscribe((message: VopNavigationDeniedNotification): void => this.onNavigationDenied(message)); this.nativeEventService.scrollY .subscribe((y: number): void => this.onScrollY(y)); @@ -53,21 +57,46 @@ export class AppComponent { .subscribe((focused: boolean): void => this.onFocus(focused)); } - private onStart(data: any): void { - console.log('player: onStart', data); - this.veronaPostService.sessionId = data.sessionId; - this.unit = JSON.parse(data.unitDefinition); - // playerStartData.unitStateData = data.unitState?.dataParts?.all; + private get validPages():Record<string, string>[] { + return this.pages.map((page:UnitPage, index:number) => { + const validPage: Record<string, string> = {}; + validPage[`page${index}`] = `Seite ${index + 1}`; + return validPage; + }); + } + + private onStart(message: VopStartCommand): void { + // eslint-disable-next-line no-console + console.log('player: onStart', message); this.initForm(); + this.initPlayer(message); + const values = { + playerState: + { + state: 'running' as RunningState, + validPages: this.validPages, + currentPage: 'page0' + } + }; + this.veronaPostService.sendVopStateChangedNotification(values); } private initForm(): void { this.form = new FormGroup({}); - this.form.valueChanges.subscribe(v => this.onFormChanges(v)); + this.form.valueChanges + .subscribe((formValues: any): void => this.onFormChanges(formValues)); } - private onNavigationDenied(data: any): void { - console.log('player: onNavigationDenied', data); + private initPlayer(message: VopStartCommand): void { + this.veronaPostService.sessionId = message.sessionId; + const unit: Unit = message.unitDefinition ? JSON.parse(message.unitDefinition) : []; + this.pages = unit.pages; + this.playerConfig = message.playerConfig || {}; + } + + private onNavigationDenied(message: VopNavigationDeniedNotification): void { + // eslint-disable-next-line no-console + console.log('player: onNavigationDenied', message); this.form.markAllAsTouched(); } @@ -80,19 +109,27 @@ export class AppComponent { console.log(`Player: onElementValueChanges - ${value.id}: ${value.values[0]} -> ${value.values[1]}`); }; - private onFormChanges(value: unknown): void { - const allValues: string = JSON.stringify(value); + private onFormChanges(formValues: unknown): void { // eslint-disable-next-line no-console - console.log('Player: emit valueChanged', allValues); - this.veronaPostService.sendVopStateChangedNotification(allValues); + console.log('player: onFormChanges', formValues); + // TODO: map by page and? section not all + const values = { + unitState: { + dataParts: { + all: JSON.stringify(formValues) + } + } + }; + this.veronaPostService.sendVopStateChangedNotification(values); } private onScrollY = (y: number): void => { + // eslint-disable-next-line no-console console.log('player: onScrollY', y); }; - // TODO private onFocus(focused: boolean): void { + // eslint-disable-next-line no-console console.log('player: onFocus', focused); this.veronaPostService.sendVopWindowFocusChangedNotification(focused); } diff --git a/projects/player/src/app/models/verona.ts b/projects/player/src/app/models/verona.ts new file mode 100644 index 0000000000000000000000000000000000000000..0980668fb85203993d199b2b6c065090c6b0260d --- /dev/null +++ b/projects/player/src/app/models/verona.ts @@ -0,0 +1,111 @@ +export type NavigationTarget = 'first' | 'last' | 'previous' | 'next' | 'end'; + +export type RunningState = 'running' | 'stopped'; + +export interface PlayerConfig { + unitNumber?: number; + unitTitle?: number; + unitId?: number; + stateReportPolicy?: 'none' | 'eager' | 'on-demand'; + pagingMode?: 'separate' | 'concat-scroll' | 'concat-scroll-snap'; + logPolicy?: 'lean' | 'rich' | 'debug' | 'disabled'; + startPage?: string; + enabledNavigationTargets?: NavigationTarget[] +} + +export interface UnitState { + dataParts?: Record<string, string>; + presentationProgress?: 'none' | 'some' | 'complete'; + responseProgress?: 'none' | 'some' | 'complete'; + unitStateDataType?: string; +} + +export interface PlayerState { + state: RunningState ; + validPages?: Record<string, string>[]; + currentPage?: string; +} + +export interface LogData{ + timeStamp: number, + key: string, + content?: string +} + +export interface VopStartCommand { + type: 'vopStartCommand'; + sessionId: string; + unitDefinition?: string; + unitDefinitionType?: string; + unitState?: UnitState; + playerConfig?: PlayerConfig; +} + +export interface VopNavigationDeniedNotification { + type: 'vopNavigationDeniedNotification'; + sessionId: string; + reason?: Array<'presentationIncomplete' | 'responsesIncomplete'> +} + +export interface VopPageNavigationCommand { + type: 'vopPageNavigationCommand'; + sessionId: string; + target: string; +} + +export interface VopGetStateRequest { + type: 'vopGetStateRequest'; + sessionId: string; + stop: boolean; +} + +export interface VopStopCommand { + type: 'vopStopCommand'; + sessionId: string; +} + +export interface VopContinueCommand { + type: 'vopContinueCommand'; + sessionId: string; +} + +export interface VopReadyNotification { + type: 'vopReadyNotification'; + apiVersion: string; + notSupportedApiFeatures?: string; + supportedUnitDefinitionTypes?: string; + supportedUnitStateDataTypes?: string; +} + +export interface VopStateChangedNotification { + type: 'vopStateChangedNotification'; + sessionId: string; + timeStamp: number; + unitState?: UnitState; + playerState?: PlayerState; + log?: LogData[]; +} + +export interface VopUnitNavigationRequestedNotification { + type: 'vopUnitNavigationRequestedNotification'; + sessionId: string; + target: 'first' | 'last' | 'previous' | 'next' | 'end'; +} + +export interface VopWindowFocusChangedNotification { + type: 'vopWindowFocusChangedNotification'; + timeStamp: number; + hasFocus: boolean; +} + +export type VopMessage = + VopStartCommand | + VopNavigationDeniedNotification | + VopPageNavigationCommand | + VopGetStateRequest | + VopStopCommand | + VopContinueCommand | + VopReadyNotification | + VopStateChangedNotification | + VopWindowFocusChangedNotification | + VopUnitNavigationRequestedNotification; diff --git a/projects/player/src/app/services/verona-post.service.ts b/projects/player/src/app/services/verona-post.service.ts index 1644be7d5010d78a8a5aa2bf03e88df6f3da15ca..b4bc85fc9734d065965079282fd3b031c3d30d6f 100644 --- a/projects/player/src/app/services/verona-post.service.ts +++ b/projects/player/src/app/services/verona-post.service.ts @@ -1,11 +1,14 @@ import { Inject, Injectable } from '@angular/core'; import { DOCUMENT } from '@angular/common'; +import { + LogData, NavigationTarget, PlayerState, UnitState, VopMessage +} from '../models/verona'; @Injectable({ providedIn: 'root' }) export class VeronaPostService { - private playerMetadata!: any; + private readonly playerMetadata!: NamedNodeMap; private _sessionId!: string; constructor(@Inject(DOCUMENT) private document: Document) { @@ -18,38 +21,46 @@ export class VeronaPostService { private isStandalone = (): boolean => window === window.parent; - private send(message: any): void { - // prevend posts in local dev mode + private send(message: VopMessage): void { + // prevent posts in local (dev) mode if (!this.isStandalone()) { window.parent.postMessage(message, '*'); } } - sendVopStateChangedNotification(value: unknown) : void { + sendVopStateChangedNotification(values: { + unitState?: UnitState, + playerState?: PlayerState, + log?: LogData[] + }): void { this.send({ type: 'vopStateChangedNotification', sessionId: this._sessionId, timeStamp: Date.now(), - unitState: { - dataParts: { - all: value - }, - unitStateDataType: this.playerMetadata.getNamedItem('data-supported-unit-state-data-types').value - } + ...(values) }); } sendVopReadyNotification(): void { - this.send({ - type: 'vopReadyNotification', - apiVersion: this.playerMetadata.getNamedItem('data-api-version').value, - notSupportedApiFeatures: this.playerMetadata.getNamedItem('data-not-supported-api-features').value, - supportedUnitDefinitionTypes: this.playerMetadata.getNamedItem('data-supported-unit-definition-types').value, - supportedUnitStateDataTypes: this.playerMetadata.getNamedItem('data-supported-unit-state-data-types').value - }); + if (this.playerMetadata) { + this.send({ + type: 'vopReadyNotification', + apiVersion: + this.playerMetadata.getNamedItem('data-api-version')?.value || '', + notSupportedApiFeatures: + this.playerMetadata.getNamedItem('data-not-supported-api-features')?.value, + supportedUnitDefinitionTypes: + this.playerMetadata.getNamedItem('data-supported-unit-definition-types')?.value, + supportedUnitStateDataTypes: + this.playerMetadata.getNamedItem('data-supported-unit-state-data-types')?.value + }); + } else { + // eslint-disable-next-line no-console + console.warn('player: no playerMetadata defined'); + } } - sendVopUnitNavigationRequestedNotification = (target: string): void => { + sendVopUnitNavigationRequestedNotification = (target: NavigationTarget): void => { this.send({ type: 'vopUnitNavigationRequestedNotification', sessionId: this._sessionId, @@ -60,7 +71,7 @@ export class VeronaPostService { sendVopWindowFocusChangedNotification = (focused: boolean): void => { this.send({ type: 'vopWindowFocusChangedNotification', - sessionId: this._sessionId, + timeStamp: Date.now(), hasFocus: focused }); }; diff --git a/projects/player/src/app/services/verona-subscription.service.ts b/projects/player/src/app/services/verona-subscription.service.ts index a192d0bd409bc1a861a9fc8f6751777fa6aeb219..78a3bbca26d4705ad6fd70c90a8a68f77ba68079 100644 --- a/projects/player/src/app/services/verona-subscription.service.ts +++ b/projects/player/src/app/services/verona-subscription.service.ts @@ -1,28 +1,32 @@ import { Injectable } from '@angular/core'; import { fromEvent, Observable, Subject } from 'rxjs'; +import { VopMessage, VopNavigationDeniedNotification, VopStartCommand } from '../models/verona'; @Injectable({ providedIn: 'root' }) export class VeronaSubscriptionService { - private _vopStartCommand = new Subject<any>(); - private _vopNavigationDeniedNotification = new Subject<any>(); + private _vopStartCommand = new Subject<VopStartCommand>(); + private _vopNavigationDeniedNotification = new Subject<VopNavigationDeniedNotification>(); constructor() { fromEvent(window, 'message') .subscribe((e: Event): void => { - this.handleMessage((e as MessageEvent).data); + const message = (e as MessageEvent).data as VopMessage; + this.handleMessage(message); }); } - handleMessage(messageData: any): void { + private handleMessage(messageData: VopMessage): void { switch (messageData.type) { case 'vopStartCommand': + // eslint-disable-next-line no-console console.log('Player: _vopStartCommand ', messageData); this._vopStartCommand.next(messageData); break; case 'vopNavigationDeniedNotification': + // eslint-disable-next-line no-console console.log('Player: _vopNavigationDeniedNotification ', messageData); this._vopNavigationDeniedNotification.next(messageData); break; @@ -32,15 +36,16 @@ export class VeronaSubscriptionService { case 'vopStopCommand': case 'vopContinueCommand': default: + // eslint-disable-next-line no-console console.warn(`player: got message of unknown type ${messageData.type}`); } } - get vopStartCommand(): Observable<any> { + get vopStartCommand(): Observable<VopStartCommand> { return this._vopStartCommand.asObservable(); } - get vopNavigationDeniedNotification(): Observable<any> { + get vopNavigationDeniedNotification(): Observable<VopNavigationDeniedNotification> { return this._vopNavigationDeniedNotification.asObservable(); } } diff --git a/projects/player/src/index.html b/projects/player/src/index.html index 43c033de1cc778a7900634da5dac3c04fc00f31a..7d96da93c8fcc2b05306429f714ab6867b82065f 100644 --- a/projects/player/src/index.html +++ b/projects/player/src/index.html @@ -46,7 +46,13 @@ unitState: { dataParts: {} }, - playerConfig: {} + playerConfig: { + stateReportPolicy: 'eager', + pagingMode: 'separate', + logPolicy: 'rich', + startPage: 'page0', + enabledNavigationTargets: ['next'] + } }, '*'); } const input = document.getElementById('fileInput');