diff --git a/projects/common/assets/next-unit-button.svg b/projects/common/assets/next-unit-button.svg new file mode 100644 index 0000000000000000000000000000000000000000..8c38cdfaf70a75a07934acb55eaf7580e166ba21 --- /dev/null +++ b/projects/common/assets/next-unit-button.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="0 0 100 100"> + <circle cx="50%" cy="50%" r="50" fill="#b3fe5b"/> + <path d="M 45 35 L 60 50 L 45 65" stroke="black" stroke-width="5" fill="none" /> +</svg> diff --git a/projects/common/components/unit-nav-next.component.ts b/projects/common/components/unit-nav-next.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..80e7c88f679c19362480df984213fe4f54dee6fb --- /dev/null +++ b/projects/common/components/unit-nav-next.component.ts @@ -0,0 +1,30 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'aspect-unit-nav-next', + standalone: true, + imports: [], + template: ` + <div class="unit-nav-next"> + Hier geht’s weiter + <input type="image" src="assets/next-unit-button.svg" alt="Navigationsknopf zu nächster Unit" + (click)="navigate.emit()"> + </div> + `, + styles: ` + .unit-nav-next { + font-size: 20px; + float: right; + } + .unit-nav-next input { + vertical-align: middle; + margin: 5px; + } + .unit-nav-next input:hover { + filter: brightness(90%); + } + ` +}) +export class UnitNavNextComponent { + @Output() navigate = new EventEmitter(); +} diff --git a/projects/common/models/unit.ts b/projects/common/models/unit.ts index e1c40a125948f6bbacfb1b3900c0578da31b4fd3..7d23bbc8f8f243702a00b51ec0d1d932fc6defcc 100644 --- a/projects/common/models/unit.ts +++ b/projects/common/models/unit.ts @@ -15,6 +15,7 @@ export class Unit implements UnitProperties { pages: Page[]; enableSectionNumbering: boolean = false; sectionNumberingPosition: 'left' | 'above' = 'left'; + showUnitNavNext: boolean = false; constructor(unit?: UnitProperties) { if (unit && isValid(unit)) { @@ -23,6 +24,7 @@ export class Unit implements UnitProperties { this.pages = unit.pages.map(page => new Page(page)); this.enableSectionNumbering = unit.enableSectionNumbering; this.sectionNumberingPosition = unit.sectionNumberingPosition; + this.showUnitNavNext = unit.showUnitNavNext; } else { if (environment.strictInstantiation) { throw new InstantiationEror('Error at unit instantiation'); @@ -32,6 +34,7 @@ export class Unit implements UnitProperties { this.pages = unit?.pages.map(page => new Page(page)) || [new Page()]; if (unit?.enableSectionNumbering !== undefined) this.enableSectionNumbering = unit.enableSectionNumbering; if (unit?.sectionNumberingPosition !== undefined) this.sectionNumberingPosition = unit.sectionNumberingPosition; + if (unit?.showUnitNavNext !== undefined) this.showUnitNavNext = unit.showUnitNavNext; } } @@ -58,7 +61,8 @@ function isValid(blueprint?: UnitProperties): boolean { blueprint.type !== undefined && blueprint.pages !== undefined && blueprint.enableSectionNumbering !== undefined && - blueprint.sectionNumberingPosition !== undefined; + blueprint.sectionNumberingPosition !== undefined && + blueprint.showUnitNavNext !== undefined; } export interface UnitProperties { @@ -68,4 +72,5 @@ export interface UnitProperties { pages: Page[]; enableSectionNumbering: boolean; sectionNumberingPosition: 'left' | 'above'; + showUnitNavNext: boolean; } diff --git a/projects/common/pipes/is-enabled-navigation-target.pipe.ts b/projects/common/pipes/is-enabled-navigation-target.pipe.ts index dc65615b676a4280b2968ef9e4b50d1742837a61..01c1a4c2f9d3faa24dcace56eb8e7baad1f26229 100644 --- a/projects/common/pipes/is-enabled-navigation-target.pipe.ts +++ b/projects/common/pipes/is-enabled-navigation-target.pipe.ts @@ -1,19 +1,22 @@ import { Pipe, PipeTransform } from '@angular/core'; -import { ButtonElement } from 'common/models/elements/button/button'; +import { UnitNavParam } from 'common/models/elements/button/button'; import { NavigationTarget } from 'player/modules/verona/models/verona'; +import { StateVariable } from 'common/models/state-variable'; @Pipe({ standalone: true, name: 'isEnabledNavigationTarget' }) export class IsEnabledNavigationTargetPipe implements PipeTransform { - transform(elementModel: ButtonElement, enabledNavigationTargets: NavigationTarget[] | undefined): boolean { + transform(action: string | null, + param: UnitNavParam | number | string | StateVariable | null, + enabledNavigationTargets: NavigationTarget[] | undefined): boolean { if (!enabledNavigationTargets) { return true; } - if (elementModel.action === 'unitNav') { + if (action === 'unitNav') { return enabledNavigationTargets - .includes(elementModel.actionParam as NavigationTarget); + .includes(param as NavigationTarget); } return true; } diff --git a/projects/editor/src/app/app.module.ts b/projects/editor/src/app/app.module.ts index d39c066f9614b88b196ff636ebe8d2757f82ef4e..16bae6c42b95f49a2a238c637be2b48a9db3834f 100644 --- a/projects/editor/src/app/app.module.ts +++ b/projects/editor/src/app/app.module.ts @@ -29,6 +29,7 @@ import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipDefaultOptions } from '@angular/ import { MeasurePipe } from 'common/pipes/measure.pipe'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatBadgeModule } from '@angular/material/badge'; +import { UnitNavNextComponent } from 'common/components/unit-nav-next.component'; import { StateVariablesDialogComponent } from './components/dialogs/state-variables-dialog/state-variables-dialog.component'; @@ -220,7 +221,8 @@ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = { SizeInputPanelComponent, MeasurePipe, SectionComponent, - RichTextEditorComponent + RichTextEditorComponent, + UnitNavNextComponent ], providers: [ { provide: APIService, useExisting: VeronaAPIService }, diff --git a/projects/editor/src/app/components/unit-view/page/page-view.component.html b/projects/editor/src/app/components/unit-view/page/page-view.component.html index 9ea17ac12cc4ee40ea54ab3de212305073776fbe..fc07e1272583a40803785df81096026ba5de2769 100644 --- a/projects/editor/src/app/components/unit-view/page/page-view.component.html +++ b/projects/editor/src/app/components/unit-view/page/page-view.component.html @@ -43,6 +43,7 @@ (sectionSelected)="selectionService.selectedPageIndex = pageIndex"> </aspect-editor-section-view> </div> + <aspect-unit-nav-next *ngIf="isLastPage && unitService.unit.showUnitNavNext"></aspect-unit-nav-next> </div> <button mat-fab extended class="add-section-button" (click)="addSection(pageIndex)"> diff --git a/projects/editor/src/app/components/unit-view/page/page-view.component.ts b/projects/editor/src/app/components/unit-view/page/page-view.component.ts index de456ff590d6319e492ac7c40665b8f1f66a94f1..4bd812a0084b5e6e25cb6bcc1323abe6cd16c503 100644 --- a/projects/editor/src/app/components/unit-view/page/page-view.component.ts +++ b/projects/editor/src/app/components/unit-view/page/page-view.component.ts @@ -35,6 +35,7 @@ export class PageViewComponent implements OnInit, OnDestroy { @Input() page!: Page; @Input() pageIndex!: number; @Input() singlePageMode: boolean = false; + @Input() isLastPage: boolean = false; @Output() pagesChanged = new EventEmitter(); @ViewChildren(SectionComponent) sectionComponents!: QueryList<SectionComponent>; diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.html b/projects/editor/src/app/components/unit-view/unit-view.component.html index f1b418d83dfe25acf8863a322b3654deb0949fc2..0be486e19d6287944926e37c2f5f57aace0b198f 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.html +++ b/projects/editor/src/app/components/unit-view/unit-view.component.html @@ -77,7 +77,8 @@ (unitService.unit.pages[0].alwaysVisible ? unitService.unit.pages.slice(1) : unitService.unit.pages); let pageIndex = index" [page]="page" - [pageIndex]="pageIndex + (unitService.unit.pages[0].alwaysVisible ? 1 : 0)"> + [pageIndex]="pageIndex + (unitService.unit.pages[0].alwaysVisible ? 1 : 0)" + [isLastPage]="pageIndex == unitService.unit.pages.length - 1"> </aspect-editor-page-view> </div> <button mat-fab extended class="add-page-button" [matTooltip]="'Weitere Seite anlegen'" (click)="addPage()"> @@ -115,7 +116,7 @@ <mat-tab [disabled]="true" class="align-right"> <ng-template mat-tab-label> <button mat-icon-button [matMenuTriggerFor]="numberingMenu"> - <mat-icon>format_list_numbered</mat-icon> + <mat-icon>settings</mat-icon> </button> <mat-menu #numberingMenu="matMenu" (click)="$event.stopPropagation()"> <div [style.padding]="'0 20px 10px'" (click)="$event.stopPropagation()"> @@ -127,6 +128,10 @@ <mat-checkbox [disabled]="!numberingInput.checked" (change)="setSectionNumberingPosition($event)"> vertikale Ausrichtung </mat-checkbox> + <h3>Navigation</h3> + <mat-checkbox [matTooltip]="'Wird am Ende der letzten Seite angezeigt'" (change)="setUnitNavNext($event)"> + Navigationsknopf zur nächsten Unit anfügen + </mat-checkbox> </div> </mat-menu> </ng-template> diff --git a/projects/editor/src/app/components/unit-view/unit-view.component.ts b/projects/editor/src/app/components/unit-view/unit-view.component.ts index 17d3c45804e0f26a070bd1a3a80f8b00b95db671..204531dfba7fd63aa8ebfd88fe17411310cc2476 100644 --- a/projects/editor/src/app/components/unit-view/unit-view.component.ts +++ b/projects/editor/src/app/components/unit-view/unit-view.component.ts @@ -53,4 +53,8 @@ export class UnitViewComponent { setExpertMode(event: MatCheckboxChange) { this.unitService.setSectionExpertMode(event.checked); } + + setUnitNavNext(event: MatCheckboxChange) { + this.unitService.setUnitNavNext(event.checked); + } } diff --git a/projects/editor/src/app/services/unit-services/unit.service.ts b/projects/editor/src/app/services/unit-services/unit.service.ts index 21e9fd836feb2b394464c75e5bcc4b0dc6b11416..98b3d462e3d63668ec426e3ab6469c6c99eb107d 100644 --- a/projects/editor/src/app/services/unit-services/unit.service.ts +++ b/projects/editor/src/app/services/unit-services/unit.service.ts @@ -230,6 +230,11 @@ export class UnitService { this.updateSectionCounter(); } + setUnitNavNext(isEnabled: boolean) { + this.unit.showUnitNavNext = isEnabled; + this.updateUnitDefinition(); + } + getSelectedPage() { return this.unit.pages[this.selectionService.selectedPageIndex]; } diff --git a/projects/player/src/app/app.module.ts b/projects/player/src/app/app.module.ts index 6c6071afa76eb8822c4a7f3469265bbbc330ad3e..3aed1aae1abb86388f581007f584b77d9c9c17cf 100644 --- a/projects/player/src/app/app.module.ts +++ b/projects/player/src/app/app.module.ts @@ -24,6 +24,7 @@ import { import { IsEnabledNavigationTargetPipe } from 'common/pipes/is-enabled-navigation-target.pipe'; import { MarkingPanelComponent } from 'common/components/text/marking-panel.component'; import { ErrorService } from 'player/src/app/services/error.service'; +import { UnitNavNextComponent } from 'common/components/unit-nav-next.component'; import { AppComponent } from './app.component'; import { PageComponent } from './components/page/page.component'; import { SectionComponent } from './components/section/section.component'; @@ -123,7 +124,8 @@ import { IsValidPagePipe } from './pipes/is-valid-page.pipe'; TableComponent, MarkablesContainerComponent, IsEnabledNavigationTargetPipe, - MarkingPanelComponent + MarkingPanelComponent, + UnitNavNextComponent ], providers: [ { provide: APIService, useExisting: MetaDataService }, diff --git a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html index 9f1fb1cf9d30110a1949c93434fcde8eb6cb762e..284743be6d25d0cd31185d4ad691eac52bacb43f 100644 --- a/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html +++ b/projects/player/src/app/components/elements/interactive-group-element/interactive-group-element.component.html @@ -1,8 +1,9 @@ <aspect-button *ngIf="elementModel.type === 'button'" #elementComponent - [class.hide-navigation]="!(elementModel | cast: ButtonElement | - isEnabledNavigationTarget: navigationService.enabledNavigationTargets.value)" + [class.hide-navigation]="!((elementModel | cast: ButtonElement).action | + isEnabledNavigationTarget: (elementModel | cast: ButtonElement).actionParam: + navigationService.enabledNavigationTargets.value)" [elementModel]="elementModel | cast: ButtonElement" (buttonActionEvent)="applyButtonAction($event)"> </aspect-button> diff --git a/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.html b/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.html index 043f9e48eccd735967fa9ba97c7c8b9ebea542f8..99491cf3f8d63df65f36d9f3d4f0a026f0209afb 100644 --- a/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.html +++ b/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.html @@ -100,6 +100,8 @@ [scrollPageIndex]="i" [page]="page" [sectionNumbering]="sectionNumbering" + [showUnitNavNext]="showUnitNavNext" + [isPresentedPagesComplete]="isPresentedPagesComplete" (isVisibleIndexChange)="setIsVisibleIndexPages($event)"> </aspect-page> <aspect-page-nav-button *ngIf="scrollPageMode==='buttons' && (i | hasNextPage : isVisibleIndexPages.value)" @@ -137,6 +139,8 @@ [page]="page" [isLastPage]="last" [sectionNumbering]="sectionNumbering" + [showUnitNavNext]="showUnitNavNext" + [isPresentedPagesComplete]="isPresentedPagesComplete" (isVisibleIndexChange)="setIsVisibleIndexPages($event)" (selectedIndexChange)="setSelectedIndex($event)"> </aspect-page> diff --git a/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.ts b/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.ts index 9c1dc2aa0c00ff0ff346c66633aa89fb6cf99f87..21794e46baf98ec7764aa068cc4420713e5374ec 100644 --- a/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.ts +++ b/projects/player/src/app/components/layouts/pages-layout/pages-layout.component.ts @@ -27,6 +27,8 @@ export class PagesLayoutComponent implements OnInit, AfterViewInit, OnDestroy { @Input() scrollPages!: Page[]; @Input() hasScrollPages!: boolean; @Input() alwaysVisiblePagePosition!: 'top' | 'bottom' | 'left' | 'right'; + @Input() isPresentedPagesComplete!: boolean; + @Input() showUnitNavNext!: boolean; @Input() sectionNumbering!: { enableSectionNumbering: boolean, sectionNumberingPosition: 'left' | 'above' }; selectedIndex: number = 0; diff --git a/projects/player/src/app/components/page/page.component.html b/projects/player/src/app/components/page/page.component.html index ec5c5a25126f7e477eddcb97ab53c8ea781f52e9..c4973e5a152f6d40b3552f30d9eca5ac05345d9c 100644 --- a/projects/player/src/app/components/page/page.component.html +++ b/projects/player/src/app/components/page/page.component.html @@ -15,6 +15,11 @@ [sectionNumbering]="sectionNumbering" (isVisibleIndexChange)="setIsVisibleIndexSections($event)"> </aspect-section> + <aspect-unit-nav-next *ngIf="isLastPage && showUnitNavNext && isPresentedPagesComplete && ('unitNav' | + isEnabledNavigationTarget: 'next': + navigationService.enabledNavigationTargets.value)" + (navigate)="veronaPostService.sendVopUnitNavigationRequestedNotification('next')"> + </aspect-unit-nav-next> </div> <div aspectInViewDetection detectionType="bottom" diff --git a/projects/player/src/app/components/page/page.component.ts b/projects/player/src/app/components/page/page.component.ts index 02dfc56860eac866d95c6721ab67fca07db59c57..18218dd94c4ad1913645e808c85a21a19e7b7620 100644 --- a/projects/player/src/app/components/page/page.component.ts +++ b/projects/player/src/app/components/page/page.component.ts @@ -4,6 +4,8 @@ import { import { Page } from 'common/models/page'; import { PagingMode } from 'player/modules/verona/models/verona'; import { IsVisibleIndex } from 'player/src/app/models/is-visible-index.interface'; +import { VeronaPostService } from 'player/modules/verona/services/verona-post.service'; +import { NavigationService } from 'player/src/app/services/navigation.service'; @Component({ selector: 'aspect-page', @@ -17,6 +19,8 @@ export class PageComponent { @Input() pageIndex!: number; @Input() scrollPageIndex!: number; @Input() pagingMode!: PagingMode; + @Input() isPresentedPagesComplete!: boolean; + @Input() showUnitNavNext!: boolean; @Input() sectionNumbering: { enableSectionNumbering: boolean, sectionNumberingPosition: 'left' | 'above' } | undefined; @@ -25,7 +29,10 @@ export class PageComponent { @Output() isVisibleIndexChange = new EventEmitter<IsVisibleIndex>(); isVisibleIndexSections: IsVisibleIndex[] = []; - constructor(public elementRef: ElementRef) {} + constructor( + public elementRef: ElementRef, + public navigationService: NavigationService, + public veronaPostService: VeronaPostService) {} setIsVisibleIndexSections(changedSection: IsVisibleIndex): void { let section = this.isVisibleIndexSections diff --git a/projects/player/src/app/components/unit/unit.component.html b/projects/player/src/app/components/unit/unit.component.html index c5892676255fac47de48189b16583d4515817613..6ea196b2d2c01c3a22991b9dafdf8c4047655bf3 100644 --- a/projects/player/src/app/components/unit/unit.component.html +++ b/projects/player/src/app/components/unit/unit.component.html @@ -1,6 +1,7 @@ <ng-content></ng-content> <aspect-player-layout *ngIf="pages?.length" aspectUnitState + [presentationProgressStatus]="presentationProgressStatus" aspectPlayerState [isVisibleIndexPages]="pagesLayout.isVisibleIndexPages" [currentPageIndex]="pagesLayout.selectedIndex"> @@ -12,6 +13,8 @@ [alwaysVisiblePagePosition]="(pages | alwaysVisiblePage)?.alwaysVisiblePagePosition || 'left'" [scrollPages]="pages | scrollPages" [hasScrollPages]="(pages | scrollPages).length > 0" - [scrollPageMode]="playerConfig.pagingMode || 'separate'"> + [scrollPageMode]="playerConfig.pagingMode || 'separate'" + [showUnitNavNext]="showUnitNavNext" + [isPresentedPagesComplete]="presentationProgressStatus.value === 'complete'"> </aspect-pages-layout> </aspect-player-layout> diff --git a/projects/player/src/app/components/unit/unit.component.ts b/projects/player/src/app/components/unit/unit.component.ts index d6ce8571e2f1329b09f9043c5917f2d25d27284e..6c03e6d07a29f07073862b710c852c3e780fe4e3 100644 --- a/projects/player/src/app/components/unit/unit.component.ts +++ b/projects/player/src/app/components/unit/unit.component.ts @@ -1,5 +1,10 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { PlayerConfig, VopPlayerConfigChangedNotification, VopStartCommand } from 'player/modules/verona/models/verona'; +import { + PlayerConfig, + Progress, + VopPlayerConfigChangedNotification, + VopStartCommand +} from 'player/modules/verona/models/verona'; import { Unit } from 'common/models/unit'; import { LogService } from 'player/modules/logging/services/log.service'; import { InputElement } from 'common/models/elements/element'; @@ -21,6 +26,7 @@ import { StateVariableStateService } from 'player/src/app/services/state-variabl import { TranslateService } from '@ngx-translate/core'; import { SectionCounter } from 'common/util/section-counter'; import { NavigationService } from 'player/src/app/services/navigation.service'; +import { BehaviorSubject } from 'rxjs'; @Component({ selector: 'aspect-unit', @@ -30,11 +36,14 @@ import { NavigationService } from 'player/src/app/services/navigation.service'; export class UnitComponent implements OnInit { pages: Page[] = []; playerConfig: PlayerConfig = {}; + showUnitNavNext: boolean = false; sectionNumbering: { enableSectionNumbering: boolean, sectionNumberingPosition: 'left' | 'above' } = { enableSectionNumbering: false, sectionNumberingPosition: 'left' }; + presentationProgressStatus: BehaviorSubject<Progress> = new BehaviorSubject<Progress>('none'); + constructor(public unitStateService: UnitStateService, public stateVariableStateService: StateVariableStateService, private metaDataService: MetaDataService, @@ -68,6 +77,7 @@ export class UnitComponent implements OnInit { enableSectionNumbering: unit.enableSectionNumbering, sectionNumberingPosition: unit.sectionNumberingPosition }; + this.showUnitNavNext = unit.showUnitNavNext; this.setPlayerConfig(message.playerConfig || {}); this.metaDataService.resourceURL = this.playerConfig.directDownloadUrl; this.veronaPostService.sessionID = message.sessionId; @@ -135,6 +145,7 @@ export class UnitComponent implements OnInit { } private reset(): void { + this.presentationProgressStatus.next('none'); this.pages = []; this.playerConfig = {}; this.anchorService.reset(); diff --git a/projects/player/src/app/directives/unit-state.directive.ts b/projects/player/src/app/directives/unit-state.directive.ts index 4c357abb81bfecd5c328be6f3a30edc907987e61..a631b139e33f6329b48da12eaa80e770b6e10695 100644 --- a/projects/player/src/app/directives/unit-state.directive.ts +++ b/projects/player/src/app/directives/unit-state.directive.ts @@ -1,5 +1,5 @@ -import { Directive, OnDestroy, OnInit } from '@angular/core'; -import { debounceTime, merge, Subject } from 'rxjs'; +import { Directive, Input, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, debounceTime, merge, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { Progress, UnitState } from 'player/modules/verona/models/verona'; import { VeronaSubscriptionService } from 'player/modules/verona/services/verona-subscription.service'; @@ -15,6 +15,7 @@ import { ValidationService } from '../services/validation.service'; }) export class UnitStateDirective implements OnInit, OnDestroy { private ngUnsubscribe = new Subject<void>(); + @Input() presentationProgressStatus!: BehaviorSubject<Progress>; constructor( private unitStateService: UnitStateService, @@ -40,11 +41,16 @@ export class UnitStateDirective implements OnInit, OnDestroy { } private get presentationProgress(): Progress { + if (this.presentationProgressStatus.value === 'complete') return 'complete'; if (this.mediaPlayerService.areMediaElementsRegistered()) { const mediaStatus = this.mediaPlayerService.mediaStatus; - return mediaStatus === this.unitStateService.presentedPagesProgress ? mediaStatus : 'some'; + this.presentationProgressStatus + .next(mediaStatus === this.unitStateService.presentedPagesProgress ? mediaStatus : 'some'); + } else { + this.presentationProgressStatus + .next(this.unitStateService.presentedPagesProgress); } - return this.unitStateService.presentedPagesProgress; + return this.presentationProgressStatus.value; } private sendVopStateChangedNotification(): void {