MOBILE-3648 lesson: Implement lesson player
This commit is contained in:
		
							parent
							
								
									71bcb07c74
								
							
						
					
					
						commit
						90b3add5df
					
				@ -1,5 +1,4 @@
 | 
				
			|||||||
<ion-list>
 | 
					<ion-list>
 | 
				
			||||||
    <ion-radio-group>
 | 
					 | 
				
			||||||
    <ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
 | 
					    <ion-item *ngFor="let type of types" class="addon-calendar-event" [ngClass]="['addon-calendar-eventtype-'+type]">
 | 
				
			||||||
        <ion-icon [name]="typeIcons[type]" slot="start"></ion-icon>
 | 
					        <ion-icon [name]="typeIcons[type]" slot="start"></ion-icon>
 | 
				
			||||||
        <ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
 | 
					        <ion-label>{{ 'addon.calendar.' + type + 'events' | translate}}</ion-label>
 | 
				
			||||||
@ -8,13 +7,12 @@
 | 
				
			|||||||
    <ion-item-divider *ngIf="filter.course || filter.category || filter.group">
 | 
					    <ion-item-divider *ngIf="filter.course || filter.category || filter.group">
 | 
				
			||||||
        <ion-label></ion-label>
 | 
					        <ion-label></ion-label>
 | 
				
			||||||
    </ion-item-divider>
 | 
					    </ion-item-divider>
 | 
				
			||||||
        <ion-list *ngIf="filter.course || filter.category || filter.group">
 | 
					    <ng-container *ngIf="filter.course || filter.category || filter.group">
 | 
				
			||||||
        <ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
 | 
					        <ion-radio-group [(ngModel)]="courseId" (ionChange)="onChange()">
 | 
				
			||||||
            <ion-item class="ion-text-wrap" *ngFor="let course of courses">
 | 
					            <ion-item class="ion-text-wrap" *ngFor="let course of courses">
 | 
				
			||||||
                <ion-label><core-format-text [text]="course.fullname"></core-format-text></ion-label>
 | 
					                <ion-label><core-format-text [text]="course.fullname"></core-format-text></ion-label>
 | 
				
			||||||
                    <ion-radio slot="start" value="{{course.id}}"></ion-radio>
 | 
					                <ion-radio slot="end" value="{{course.id}}"></ion-radio>
 | 
				
			||||||
            </ion-item>
 | 
					            </ion-item>
 | 
				
			||||||
        </ion-radio-group>
 | 
					        </ion-radio-group>
 | 
				
			||||||
        </ion-list>
 | 
					    </ng-container>
 | 
				
			||||||
    </ion-radio-group>
 | 
					 | 
				
			||||||
</ion-list>
 | 
					</ion-list>
 | 
				
			||||||
 | 
				
			|||||||
@ -157,18 +157,18 @@
 | 
				
			|||||||
                            </ion-label>
 | 
					                            </ion-label>
 | 
				
			||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-item>
 | 
					                        <ion-item>
 | 
				
			||||||
                            <ion-radio slot="start" value="0"></ion-radio>
 | 
					                            <ion-radio slot="end" value="0"></ion-radio>
 | 
				
			||||||
                            <ion-label>{{ 'addon.calendar.durationnone' | translate }}</ion-label>
 | 
					                            <ion-label>{{ 'addon.calendar.durationnone' | translate }}</ion-label>
 | 
				
			||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-item  (click)="selectDuration('1')">
 | 
					                        <ion-item  (click)="selectDuration('1')">
 | 
				
			||||||
                            <ion-radio slot="start" value="1"></ion-radio>
 | 
					                            <ion-radio slot="end" value="1"></ion-radio>
 | 
				
			||||||
                            <ion-label>{{ 'addon.calendar.durationuntil' | translate }}</ion-label>
 | 
					                            <ion-label>{{ 'addon.calendar.durationuntil' | translate }}</ion-label>
 | 
				
			||||||
                            <ion-datetime formControlName="timedurationuntil"
 | 
					                            <ion-datetime formControlName="timedurationuntil"
 | 
				
			||||||
                                [placeholder]="'addon.calendar.durationuntil' | translate"
 | 
					                                [placeholder]="'addon.calendar.durationuntil' | translate"
 | 
				
			||||||
                                [displayFormat]="dateFormat" [disabled]="form.controls.duration.value != 1"></ion-datetime>
 | 
					                                [displayFormat]="dateFormat" [disabled]="form.controls.duration.value != 1"></ion-datetime>
 | 
				
			||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-item (click)="selectDuration('2')">
 | 
					                        <ion-item (click)="selectDuration('2')">
 | 
				
			||||||
                            <ion-radio slot="start" value="2"></ion-radio>
 | 
					                            <ion-radio slot="end" value="2"></ion-radio>
 | 
				
			||||||
                            <ion-label>{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
 | 
					                            <ion-label>{{ 'addon.calendar.durationminutes' | translate }}</ion-label>
 | 
				
			||||||
                            <ion-input type="number" name="timedurationminutes" slot="end"
 | 
					                            <ion-input type="number" name="timedurationminutes" slot="end"
 | 
				
			||||||
                                [placeholder]="'addon.calendar.durationminutes' | translate"
 | 
					                                [placeholder]="'addon.calendar.durationminutes' | translate"
 | 
				
			||||||
@ -203,11 +203,11 @@
 | 
				
			|||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-item>
 | 
					                        <ion-item>
 | 
				
			||||||
                            <ion-label>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</ion-label>
 | 
					                            <ion-label>{{ 'addon.calendar.repeateditall' | translate:{$a: otherEventsCount} }}</ion-label>
 | 
				
			||||||
                            <ion-radio slot="start" [value]="1"></ion-radio>
 | 
					                            <ion-radio slot="end" [value]="1"></ion-radio>
 | 
				
			||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-item>
 | 
					                        <ion-item>
 | 
				
			||||||
                            <ion-label>{{ 'addon.calendar.repeateditthis' | translate }}</ion-label>
 | 
					                            <ion-label>{{ 'addon.calendar.repeateditthis' | translate }}</ion-label>
 | 
				
			||||||
                            <ion-radio slot="start" [value]="0"></ion-radio>
 | 
					                            <ion-radio slot="end" [value]="0"></ion-radio>
 | 
				
			||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                    </ion-radio-group>
 | 
					                    </ion-radio-group>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -21,11 +21,13 @@ import { TranslateModule } from '@ngx-translate/core';
 | 
				
			|||||||
import { CoreSharedModule } from '@/core/shared.module';
 | 
					import { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
					import { CoreCourseComponentsModule } from '@features/course/components/components.module';
 | 
				
			||||||
import { AddonModLessonIndexComponent } from './index/index';
 | 
					import { AddonModLessonIndexComponent } from './index/index';
 | 
				
			||||||
 | 
					import { AddonModLessonMenuModalPage } from './menu-modal/menu-modal';
 | 
				
			||||||
import { AddonModLessonPasswordModalComponent } from './password-modal/password-modal';
 | 
					import { AddonModLessonPasswordModalComponent } from './password-modal/password-modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
    declarations: [
 | 
					    declarations: [
 | 
				
			||||||
        AddonModLessonIndexComponent,
 | 
					        AddonModLessonIndexComponent,
 | 
				
			||||||
 | 
					        AddonModLessonMenuModalPage,
 | 
				
			||||||
        AddonModLessonPasswordModalComponent,
 | 
					        AddonModLessonPasswordModalComponent,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    imports: [
 | 
					    imports: [
 | 
				
			||||||
@ -40,6 +42,7 @@ import { AddonModLessonPasswordModalComponent } from './password-modal/password-
 | 
				
			|||||||
    ],
 | 
					    ],
 | 
				
			||||||
    exports: [
 | 
					    exports: [
 | 
				
			||||||
        AddonModLessonIndexComponent,
 | 
					        AddonModLessonIndexComponent,
 | 
				
			||||||
 | 
					        AddonModLessonMenuModalPage,
 | 
				
			||||||
        AddonModLessonPasswordModalComponent,
 | 
					        AddonModLessonPasswordModalComponent,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
				
			|||||||
@ -64,7 +64,7 @@
 | 
				
			|||||||
                        </ion-item>
 | 
					                        </ion-item>
 | 
				
			||||||
                        <ion-button expand="block" type="submit">
 | 
					                        <ion-button expand="block" type="submit">
 | 
				
			||||||
                            {{ 'addon.mod_lesson.continue' | translate }}
 | 
					                            {{ 'addon.mod_lesson.continue' | translate }}
 | 
				
			||||||
                            <core-icon slot="end" name="fas-arrow-right"></core-icon>
 | 
					                            <core-icon slot="end" name="fas-chevron-right"></core-icon>
 | 
				
			||||||
                        </ion-button>
 | 
					                        </ion-button>
 | 
				
			||||||
                        <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
 | 
					                        <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
 | 
				
			||||||
                        <input type="submit" class="core-submit-hidden-enter" />
 | 
					                        <input type="submit" class="core-submit-hidden-enter" />
 | 
				
			||||||
@ -73,13 +73,17 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                <core-loading [hideUntil]="!showSpinner">
 | 
					                <core-loading [hideUntil]="!showSpinner">
 | 
				
			||||||
                    <ion-list *ngIf="(lesson && !preventReasons.length) || retakeToReview">
 | 
					                    <ion-list *ngIf="(lesson && !preventReasons.length) || retakeToReview">
 | 
				
			||||||
                        <ion-item class="ion-text-wrap" *ngIf="retakeToReview">
 | 
					                        <ng-container *ngIf="retakeToReview">
 | 
				
			||||||
                            <!-- A retake was finished in a synchronization, allow reviewing it. -->
 | 
					                            <!-- A retake was finished in a synchronization, allow reviewing it. -->
 | 
				
			||||||
 | 
					                            <ion-item class="ion-text-wrap" lines="none">
 | 
				
			||||||
                                <ion-label class="ion-padding-bottom">
 | 
					                                <ion-label class="ion-padding-bottom">
 | 
				
			||||||
                                    {{ 'addon.mod_lesson.retakefinishedinsync' | translate }}
 | 
					                                    {{ 'addon.mod_lesson.retakefinishedinsync' | translate }}
 | 
				
			||||||
                                </ion-label>
 | 
					                                </ion-label>
 | 
				
			||||||
                            <ion-button expand="block" (click)="review()">{{ 'addon.mod_lesson.review' | translate }}</ion-button>
 | 
					 | 
				
			||||||
                            </ion-item>
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					                            <ion-button class="ion-text-wrap ion-margin" expand="block" (click)="review()">
 | 
				
			||||||
 | 
					                                {{ 'addon.mod_lesson.review' | translate }}
 | 
				
			||||||
 | 
					                            </ion-button>
 | 
				
			||||||
 | 
					                        </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <ng-container *ngIf="lesson && !preventReasons.length">
 | 
					                        <ng-container *ngIf="lesson && !preventReasons.length">
 | 
				
			||||||
                            <ion-item class="ion-text-wrap" *ngIf="leftDuringTimed && !lesson.timelimit && !finishedOffline">
 | 
					                            <ion-item class="ion-text-wrap" *ngIf="leftDuringTimed && !lesson.timelimit && !finishedOffline">
 | 
				
			||||||
@ -103,15 +107,16 @@
 | 
				
			|||||||
                                </ion-label>
 | 
					                                </ion-label>
 | 
				
			||||||
                            </ion-item>
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <ion-item class="ion-text-wrap" *ngIf="leftDuringTimed && lesson.timelimit && lesson.retake &&
 | 
					                            <ng-container *ngIf="leftDuringTimed && lesson.timelimit && lesson.retake && !finishedOffline">
 | 
				
			||||||
                                !finishedOffline">
 | 
					                                <ion-item class="ion-text-wrap">
 | 
				
			||||||
                                    <!-- User left during the session with time limit and retakes allowed, ask to continue. -->
 | 
					                                    <!-- User left during the session with time limit and retakes allowed, ask to continue. -->
 | 
				
			||||||
                                    <ion-label [innerHTML]="'addon.mod_lesson.leftduringtimed' | translate"></ion-label>
 | 
					                                    <ion-label [innerHTML]="'addon.mod_lesson.leftduringtimed' | translate"></ion-label>
 | 
				
			||||||
                                <ion-button expand="block" (click)="start(false)">
 | 
					 | 
				
			||||||
                                    {{ 'addon.mod_lesson.continue' | translate }}
 | 
					 | 
				
			||||||
                                    <ion-icon name="fas-arrow-right" slot="end"></ion-icon>
 | 
					 | 
				
			||||||
                                </ion-button>
 | 
					 | 
				
			||||||
                                </ion-item>
 | 
					                                </ion-item>
 | 
				
			||||||
 | 
					                                <ion-button class="ion-text-wrap ion-margin" expand="block" (click)="start(false)">
 | 
				
			||||||
 | 
					                                    {{ 'addon.mod_lesson.continue' | translate }}
 | 
				
			||||||
 | 
					                                    <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
				
			||||||
 | 
					                                </ion-button>
 | 
				
			||||||
 | 
					                            </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <ion-item class="ion-text-wrap" *ngIf="leftDuringTimed && lesson.timelimit && !lesson.retake">
 | 
					                            <ion-item class="ion-text-wrap" *ngIf="leftDuringTimed && lesson.timelimit && !lesson.retake">
 | 
				
			||||||
                                <!-- User left during the session with time limit and retakes not allowed.
 | 
					                                <!-- User left during the session with time limit and retakes not allowed.
 | 
				
			||||||
@ -119,22 +124,24 @@
 | 
				
			|||||||
                                <ion-label [innerHTML]="'addon.mod_lesson.leftduringtimednoretake' | translate"></ion-label>
 | 
					                                <ion-label [innerHTML]="'addon.mod_lesson.leftduringtimednoretake' | translate"></ion-label>
 | 
				
			||||||
                            </ion-item>
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <ion-item class="ion-text-wrap" *ngIf="!leftDuringTimed && !finishedOffline">
 | 
					                            <ng-container *ngIf="!leftDuringTimed && !finishedOffline">
 | 
				
			||||||
                                <!-- User hasn't left during the session, show a start button. -->
 | 
					                                <!-- User hasn't left during the session, show a start button. -->
 | 
				
			||||||
                                <ion-button expand="block" *ngIf="!canManage" (click)="start(false)">
 | 
					                                <ion-button class="ion-text-wrap ion-margin" expand="block" *ngIf="!canManage"
 | 
				
			||||||
 | 
					                                    (click)="start(false)">
 | 
				
			||||||
                                    {{ 'core.start' | translate }}
 | 
					                                    {{ 'core.start' | translate }}
 | 
				
			||||||
                                    <ion-icon name="fas-arrow-right" slot="end"></ion-icon>
 | 
					                                    <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
				
			||||||
                                </ion-button>
 | 
					                                </ion-button>
 | 
				
			||||||
                                <ion-button expand="block" *ngIf="canManage" (click)="start(false)">
 | 
					                                <ion-button class="ion-text-wrap ion-margin" expand="block" *ngIf="canManage"
 | 
				
			||||||
 | 
					                                    (click)="start(false)">
 | 
				
			||||||
                                    {{ 'addon.mod_lesson.preview' | translate }}
 | 
					                                    {{ 'addon.mod_lesson.preview' | translate }}
 | 
				
			||||||
                                    <ion-icon name="fas-search" slot="end"></ion-icon>
 | 
					                                    <ion-icon name="fas-search" slot="end"></ion-icon>
 | 
				
			||||||
                                </ion-button>
 | 
					                                </ion-button>
 | 
				
			||||||
                            </ion-item>
 | 
					                            </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            <ion-button class="ion-text-wrap" *ngIf="finishedOffline" expand="block" (click)="start(true)">
 | 
					                            <ion-button class="ion-text-wrap" *ngIf="finishedOffline" expand="block" (click)="start(true)">
 | 
				
			||||||
                                <!-- There's an attempt finished in offline. Let the user continue, showing the end of lesson. -->
 | 
					                                <!-- There's an attempt finished in offline. Let the user continue, showing the end of lesson. -->
 | 
				
			||||||
                                {{ 'addon.mod_lesson.continue' | translate }}
 | 
					                                {{ 'addon.mod_lesson.continue' | translate }}
 | 
				
			||||||
                                <ion-icon name="fas-arrow-right" slot="end"></ion-icon>
 | 
					                                <ion-icon name="fas-chevron-right" slot="end"></ion-icon>
 | 
				
			||||||
                            </ion-button>
 | 
					                            </ion-button>
 | 
				
			||||||
                        </ng-container>
 | 
					                        </ng-container>
 | 
				
			||||||
                    </ion-list>
 | 
					                    </ion-list>
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ import { CoreCourse } from '@features/course/services/course';
 | 
				
			|||||||
import { CoreUser } from '@features/user/services/user';
 | 
					import { CoreUser } from '@features/user/services/user';
 | 
				
			||||||
import { IonContent, IonInput } from '@ionic/angular';
 | 
					import { IonContent, IonInput } from '@ionic/angular';
 | 
				
			||||||
import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
					import { CoreGroupInfo, CoreGroups } from '@services/groups';
 | 
				
			||||||
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
import { CoreDomUtils } from '@services/utils/dom';
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
import { CoreTextUtils } from '@services/utils/text';
 | 
					import { CoreTextUtils } from '@services/utils/text';
 | 
				
			||||||
import { CoreTimeUtils } from '@services/utils/time';
 | 
					import { CoreTimeUtils } from '@services/utils/time';
 | 
				
			||||||
@ -329,21 +330,6 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
				
			|||||||
        super.ionViewDidLeave();
 | 
					        super.ionViewDidLeave();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.tabsComponent?.ionViewDidLeave();
 | 
					        this.tabsComponent?.ionViewDidLeave();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        // @todo if (this.navCtrl.getActive().component.name != 'AddonModLessonPlayerPage') {
 | 
					 | 
				
			||||||
        //     return;
 | 
					 | 
				
			||||||
        // }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // Detect if anything was sent to server.
 | 
					 | 
				
			||||||
        this.hasPlayed = true;
 | 
					 | 
				
			||||||
        this.dataSentObserver?.off();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.dataSentObserver = CoreEvents.on<AddonModLessonDataSentData>(AddonModLessonProvider.DATA_SENT_EVENT, (data) => {
 | 
					 | 
				
			||||||
            // Ignore launch sending because it only affects timers.
 | 
					 | 
				
			||||||
            if (data.lessonId === this.lesson?.id && data.type != 'launch') {
 | 
					 | 
				
			||||||
                this.dataSent = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }, this.siteId);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -418,34 +404,45 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
				
			|||||||
     * @param continueLast Whether to continue the last retake.
 | 
					     * @param continueLast Whether to continue the last retake.
 | 
				
			||||||
     * @return Promise resolved when done.
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					 | 
				
			||||||
    protected async playLesson(continueLast?: boolean): Promise<void> {
 | 
					    protected async playLesson(continueLast?: boolean): Promise<void> {
 | 
				
			||||||
        if (!this.lesson || !this.accessInfo) {
 | 
					        if (!this.lesson || !this.accessInfo) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // @todo
 | 
					 | 
				
			||||||
        // Calculate the pageId to load. If there is timelimit, lesson is always restarted from the start.
 | 
					        // Calculate the pageId to load. If there is timelimit, lesson is always restarted from the start.
 | 
				
			||||||
        // let pageId: number | undefined;
 | 
					        let pageId: number | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // if (this.hasOffline) {
 | 
					        if (this.hasOffline) {
 | 
				
			||||||
        //     if (continueLast) {
 | 
					            if (continueLast) {
 | 
				
			||||||
        //         pageId = await AddonModLesson.instance.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount, {
 | 
					                pageId = await AddonModLesson.instance.getLastPageSeen(this.lesson.id, this.accessInfo.attemptscount, {
 | 
				
			||||||
        //             cmId: this.module!.id,
 | 
					                    cmId: this.module!.id,
 | 
				
			||||||
        //         });
 | 
					                });
 | 
				
			||||||
        //     } else {
 | 
					            } else {
 | 
				
			||||||
        //         pageId = this.accessInfo.firstpageid;
 | 
					                pageId = this.accessInfo.firstpageid;
 | 
				
			||||||
        //     }
 | 
					            }
 | 
				
			||||||
        // } else if (this.leftDuringTimed && !this.lesson.timelimit) {
 | 
					        } else if (this.leftDuringTimed && !this.lesson.timelimit) {
 | 
				
			||||||
        //     pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
 | 
					            pageId = continueLast ? this.accessInfo.lastpageseen : this.accessInfo.firstpageid;
 | 
				
			||||||
        // }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // this.navCtrl.push('AddonModLessonPlayerPage', {
 | 
					        CoreNavigator.instance.navigate('../player', {
 | 
				
			||||||
        //     courseId: this.courseId,
 | 
					            params: {
 | 
				
			||||||
        //     lessonId: this.lesson.id,
 | 
					                courseId: this.courseId,
 | 
				
			||||||
        //     pageId: pageId,
 | 
					                lessonId: this.lesson.id,
 | 
				
			||||||
        //     password: this.password,
 | 
					                pageId: pageId,
 | 
				
			||||||
        // });
 | 
					                password: this.password,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Detect if anything was sent to server.
 | 
				
			||||||
 | 
					        this.hasPlayed = true;
 | 
				
			||||||
 | 
					        this.dataSentObserver?.off();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.dataSentObserver = CoreEvents.on<AddonModLessonDataSentData>(AddonModLessonProvider.DATA_SENT_EVENT, (data) => {
 | 
				
			||||||
 | 
					            // Ignore launch sending because it only affects timers.
 | 
				
			||||||
 | 
					            if (data.lessonId === this.lesson?.id && data.type != 'launch') {
 | 
				
			||||||
 | 
					                this.dataSent = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, this.siteId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
@ -472,19 +469,21 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
 | 
				
			|||||||
     * Review the lesson.
 | 
					     * Review the lesson.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    review(): void {
 | 
					    review(): void {
 | 
				
			||||||
        if (!this.retakeToReview) {
 | 
					        if (!this.retakeToReview || !this.lesson) {
 | 
				
			||||||
            // No retake to review, stop.
 | 
					            // No retake to review, stop.
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // @todo this.navCtrl.push('AddonModLessonPlayerPage', {
 | 
					        CoreNavigator.instance.navigate('../player', {
 | 
				
			||||||
        //     courseId: this.courseId,
 | 
					            params: {
 | 
				
			||||||
        //     lessonId: this.lesson.id,
 | 
					                courseId: this.courseId,
 | 
				
			||||||
        //     pageId: this.retakeToReview.pageid,
 | 
					                lessonId: this.lesson.id,
 | 
				
			||||||
        //     password: this.password,
 | 
					                pageId: this.retakeToReview.pageid,
 | 
				
			||||||
        //     review: true,
 | 
					                password: this.password,
 | 
				
			||||||
        //     retake: this.retakeToReview.retake
 | 
					                review: true,
 | 
				
			||||||
        // });
 | 
					                retake: this.retakeToReview.retake,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										49
									
								
								src/addons/mod/lesson/components/menu-modal/menu-modal.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/addons/mod/lesson/components/menu-modal/menu-modal.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					<ion-header>
 | 
				
			||||||
 | 
					    <ion-toolbar>
 | 
				
			||||||
 | 
					        <ion-title>{{ pageInstance?.lesson?.name }}</ion-title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <ion-buttons slot="end">
 | 
				
			||||||
 | 
					            <ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
 | 
				
			||||||
 | 
					                <core-icon slot="icon-only" name="fas-times"></core-icon>
 | 
				
			||||||
 | 
					            </ion-button>
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					    </ion-toolbar>
 | 
				
			||||||
 | 
					</ion-header>
 | 
				
			||||||
 | 
					<ion-content class="addon-mod_lesson-menu-modal">
 | 
				
			||||||
 | 
					    <nav>
 | 
				
			||||||
 | 
					        <ion-list *ngIf="pageInstance">
 | 
				
			||||||
 | 
					            <!-- Media file. -->
 | 
				
			||||||
 | 
					            <ng-container *ngIf="pageInstance.mediaFile">
 | 
				
			||||||
 | 
					                <ion-item-divider>
 | 
				
			||||||
 | 
					                    <ion-label><h2>{{ 'addon.mod_lesson.linkedmedia' | translate }}</h2></ion-label>
 | 
				
			||||||
 | 
					                </ion-item-divider>
 | 
				
			||||||
 | 
					                <core-file [file]="pageInstance.mediaFile" [component]="pageInstance.component"
 | 
				
			||||||
 | 
					                    [componentId]="pageInstance.lesson?.coursemodule">
 | 
				
			||||||
 | 
					                </core-file>
 | 
				
			||||||
 | 
					            </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Lesson menu. -->
 | 
				
			||||||
 | 
					            <ng-container *ngIf="pageInstance.displayMenu">
 | 
				
			||||||
 | 
					                <ion-item-divider>
 | 
				
			||||||
 | 
					                    <ion-label><h2>{{ 'addon.mod_lesson.lessonmenu' | translate }}</h2></ion-label>
 | 
				
			||||||
 | 
					                </ion-item-divider>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-center" *ngIf="pageInstance.loadingMenu">
 | 
				
			||||||
 | 
					                    <ion-label><ion-spinner></ion-spinner></ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <div *ngIf="!pageInstance.loadingMenu">
 | 
				
			||||||
 | 
					                    <ng-container *ngFor="let page of pageInstance.lessonPages">
 | 
				
			||||||
 | 
					                        <ion-item class="ion-text-wrap" *ngIf="page.display && page.displayinmenublock" (click)="loadPage(page.id)"
 | 
				
			||||||
 | 
					                            [ngClass]='{"core-selected-item": !pageInstance.eolData && pageInstance.currentPage == page.id}'
 | 
				
			||||||
 | 
					                            button detail="true">
 | 
				
			||||||
 | 
					                            <ion-label>
 | 
				
			||||||
 | 
					                                <core-format-text [text]="page.title" contextLevel="module" [courseId]="pageInstance.courseId"
 | 
				
			||||||
 | 
					                                    [contextInstanceId]="pageInstance.lesson?.coursemodule">
 | 
				
			||||||
 | 
					                                </core-format-text>
 | 
				
			||||||
 | 
					                            </ion-label>
 | 
				
			||||||
 | 
					                        </ion-item>
 | 
				
			||||||
 | 
					                    </ng-container>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </ng-container>
 | 
				
			||||||
 | 
					        </ion-list>
 | 
				
			||||||
 | 
					    </nav>
 | 
				
			||||||
 | 
					</ion-content>
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/addons/mod/lesson/components/menu-modal/menu-modal.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/addons/mod/lesson/components/menu-modal/menu-modal.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Component, Input } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { ModalController } from '@singletons';
 | 
				
			||||||
 | 
					import { AddonModLessonPlayerPage } from '../../pages/player/player';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Modal that renders the lesson menu and media file.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'page-addon-mod-lesson-menu-modal',
 | 
				
			||||||
 | 
					    templateUrl: 'menu-modal.html',
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonModLessonMenuModalPage {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * The instance of the page that opened the modal. We use the instance instead of the needed attributes for these reasons:
 | 
				
			||||||
 | 
					     *     - We want the user to be able to see the media file while the menu is being loaded, so we need to be able to update
 | 
				
			||||||
 | 
					     *       the menu dynamically based on the data retrieved by the page that opened the modal.
 | 
				
			||||||
 | 
					     *     - The onDidDismiss function takes a while to be called, making the app seem slow. This way we can directly call
 | 
				
			||||||
 | 
					     *       the functions we need without having to wait for the modal to be dismissed.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    @Input() pageInstance?: AddonModLessonPlayerPage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Close modal.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    closeModal(): void {
 | 
				
			||||||
 | 
					        ModalController.instance.dismiss();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load a certain page.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId The page ID to load.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    loadPage(pageId: number): void {
 | 
				
			||||||
 | 
					        this.pageInstance?.changePage(pageId);
 | 
				
			||||||
 | 
					        this.closeModal();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -20,7 +20,7 @@
 | 
				
			|||||||
        </ion-item>
 | 
					        </ion-item>
 | 
				
			||||||
        <ion-button expand="block" type="submit">
 | 
					        <ion-button expand="block" type="submit">
 | 
				
			||||||
            {{ 'addon.mod_lesson.continue' | translate }}
 | 
					            {{ 'addon.mod_lesson.continue' | translate }}
 | 
				
			||||||
            <core-icon slot="end" name="fas-arrow-right"></core-icon>
 | 
					            <core-icon slot="end" name="fas-chevron-right"></core-icon>
 | 
				
			||||||
        </ion-button>
 | 
					        </ion-button>
 | 
				
			||||||
        <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
 | 
					        <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
 | 
				
			||||||
        <input type="submit" class="core-submit-hidden-enter" />
 | 
					        <input type="submit" class="core-submit-hidden-enter" />
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,10 @@ const routes: Routes = [
 | 
				
			|||||||
        path: 'index',
 | 
					        path: 'index',
 | 
				
			||||||
        loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
 | 
					        loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        path: 'player',
 | 
				
			||||||
 | 
					        loadChildren: () => import('./pages/player/player.module').then( m => m.AddonModLessonPlayerPageModule),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										292
									
								
								src/addons/mod/lesson/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										292
									
								
								src/addons/mod/lesson/pages/player/player.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,292 @@
 | 
				
			|||||||
 | 
					<ion-header>
 | 
				
			||||||
 | 
					    <ion-toolbar>
 | 
				
			||||||
 | 
					        <ion-buttons slot="start">
 | 
				
			||||||
 | 
					            <ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					        <ion-title>
 | 
				
			||||||
 | 
					            <core-format-text [text]="title" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                [courseId]="courseId">
 | 
				
			||||||
 | 
					            </core-format-text>
 | 
				
			||||||
 | 
					        </ion-title>
 | 
				
			||||||
 | 
					        <ion-buttons slot="end">
 | 
				
			||||||
 | 
					            <ion-button *ngIf="displayMenu || mediaFile" [attr.aria-label]="'addon.mod_lesson.lessonmenu' | translate"
 | 
				
			||||||
 | 
					                (click)="showMenu()">
 | 
				
			||||||
 | 
					                <ion-icon name="bookmark" slot="icon-only"></ion-icon>
 | 
				
			||||||
 | 
					            </ion-button>
 | 
				
			||||||
 | 
					        </ion-buttons>
 | 
				
			||||||
 | 
					    </ion-toolbar>
 | 
				
			||||||
 | 
					</ion-header>
 | 
				
			||||||
 | 
					<ion-content>
 | 
				
			||||||
 | 
					    <core-loading [hideUntil]="loaded">
 | 
				
			||||||
 | 
					        <!-- Info messages. Only show the first one. -->
 | 
				
			||||||
 | 
					        <ion-card class="core-info-card" *ngIf="lesson && messages?.length">
 | 
				
			||||||
 | 
					            <ion-item>
 | 
				
			||||||
 | 
					                <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
				
			||||||
 | 
					                <ion-label>{{ messages[0].message }}</ion-label>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					        </ion-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div *ngIf="lesson" [ngClass]='{"addon-mod_lesson-slideshow": lesson.slideshow}'
 | 
				
			||||||
 | 
					            [ngStyle]="{'width': lessonWidth, 'height': lessonHeight}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <core-timer *ngIf="endTime" [endTime]="endTime" (finished)="timeUp()"
 | 
				
			||||||
 | 
					                [timerText]="'addon.mod_lesson.timeremaining' | translate">
 | 
				
			||||||
 | 
					            </core-timer>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Retake and ongoing score. -->
 | 
				
			||||||
 | 
					            <ion-item class="ion-text-wrap" *ngIf="showRetake && !eolData && !processData">
 | 
				
			||||||
 | 
					                <p>{{ 'addon.mod_lesson.attempt' | translate:{$a: retake} }}</p>
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					            <ion-item *ngIf="pageData && pageData.ongoingscore && !eolData && !processData"
 | 
				
			||||||
 | 
					                class="addon-mod_lesson-ongoingscore ion-text-wrap">
 | 
				
			||||||
 | 
					                {{ pageData.ongoingscore }}
 | 
				
			||||||
 | 
					            </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Page content. -->
 | 
				
			||||||
 | 
					            <ion-card *ngIf="!eolData && !processData">
 | 
				
			||||||
 | 
					                <!-- Content page. -->
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="!question && pageContent">
 | 
				
			||||||
 | 
					                    <core-format-text [component]="component" [componentId]="lesson.coursemodule" [text]="pageContent"
 | 
				
			||||||
 | 
					                        contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                    </core-format-text>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <!-- Question page. -->
 | 
				
			||||||
 | 
					                <!-- We need to set ngIf loaded to make formGroup directive restart every time a page changes, see MOBILE-2540. -->
 | 
				
			||||||
 | 
					                <form *ngIf="question && loaded" ion-list [formGroup]="questionForm" #questionFormEl
 | 
				
			||||||
 | 
					                    (ngSubmit)="submitQuestion($event)">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <ion-item-divider class="ion-text-wrap" *ngIf="pageContent">
 | 
				
			||||||
 | 
					                        <core-format-text [component]="component" [componentId]="lesson?.coursemodule" [text]="pageContent"
 | 
				
			||||||
 | 
					                            contextLevel="module" [contextInstanceId]="lesson.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                        </core-format-text>
 | 
				
			||||||
 | 
					                    </ion-item-divider>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <!-- Render a different input depending on the type of the question. -->
 | 
				
			||||||
 | 
					                    <ng-container [ngSwitch]="question.template">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <!-- Short answer. -->
 | 
				
			||||||
 | 
					                        <ion-item class="ion-text-wrap" *ngSwitchCase="'shortanswer'">
 | 
				
			||||||
 | 
					                            <ion-input [type]="question.input!.type" placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}"
 | 
				
			||||||
 | 
					                                [id]="question.input!.id" [formControlName]="question.input!.name" autocorrect="off"
 | 
				
			||||||
 | 
					                                [maxlength]="question.input!.maxlength">
 | 
				
			||||||
 | 
					                            </ion-input>
 | 
				
			||||||
 | 
					                        </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <!-- Essay. -->
 | 
				
			||||||
 | 
					                        <ng-container *ngSwitchCase="'essay'">
 | 
				
			||||||
 | 
					                            <ion-item *ngIf="question.textarea">
 | 
				
			||||||
 | 
					                                <core-rich-text-editor placeholder="{{ 'addon.mod_lesson.youranswer' | translate }}"
 | 
				
			||||||
 | 
					                                    [control]="question.control" [component]="component" [componentId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                    [autoSave]="true" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                    elementId="answer_editor">
 | 
				
			||||||
 | 
					                                </core-rich-text-editor>
 | 
				
			||||||
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					                            <ion-item class="ion-text-wrap" *ngIf="!question.textarea && question.useranswer">
 | 
				
			||||||
 | 
					                                <p class="item-heading">{{ 'addon.mod_lesson.youranswer' | translate }}</p>
 | 
				
			||||||
 | 
					                                <p>
 | 
				
			||||||
 | 
					                                    <core-format-text [component]="component" [componentId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                        [text]="question.useranswer" contextLevel="module"
 | 
				
			||||||
 | 
					                                        [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                                    </core-format-text>
 | 
				
			||||||
 | 
					                                </p>
 | 
				
			||||||
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					                        </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <!-- Multichoice. -->
 | 
				
			||||||
 | 
					                        <ng-container *ngSwitchCase="'multichoice'">
 | 
				
			||||||
 | 
					                            <!-- Single choice. -->
 | 
				
			||||||
 | 
					                            <ion-radio-group *ngIf="!question.multi" [formControlName]="question.controlName">
 | 
				
			||||||
 | 
					                                <ion-item class="ion-text-wrap" *ngFor="let option of question.options" lines="none">
 | 
				
			||||||
 | 
					                                    <ion-label>
 | 
				
			||||||
 | 
					                                        <core-format-text [component]="component" [componentId]="lesson.coursemodule"
 | 
				
			||||||
 | 
					                                            [text]="option.text" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                            [courseId]="courseId">
 | 
				
			||||||
 | 
					                                        </core-format-text>
 | 
				
			||||||
 | 
					                                    </ion-label>
 | 
				
			||||||
 | 
					                                    <ion-radio slot="end" [id]="option.id" [value]="option.value" [disabled]="option.disabled">
 | 
				
			||||||
 | 
					                                    </ion-radio>
 | 
				
			||||||
 | 
					                                </ion-item>
 | 
				
			||||||
 | 
					                            </ion-radio-group>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <!-- Multiple choice. -->
 | 
				
			||||||
 | 
					                            <ng-container *ngIf="question.multi">
 | 
				
			||||||
 | 
					                                <ion-item class="ion-text-wrap" *ngFor="let option of question.options" lines="none">
 | 
				
			||||||
 | 
					                                    <ion-label>
 | 
				
			||||||
 | 
					                                        <core-format-text [component]="component" [componentId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                            [text]="option.text" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                            [courseId]="courseId">
 | 
				
			||||||
 | 
					                                        </core-format-text>
 | 
				
			||||||
 | 
					                                    </ion-label>
 | 
				
			||||||
 | 
					                                    <ion-checkbox [id]="option.id" [formControlName]="option.name" slot="end"></ion-checkbox>
 | 
				
			||||||
 | 
					                                </ion-item>
 | 
				
			||||||
 | 
					                            </ng-container>
 | 
				
			||||||
 | 
					                        </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        <!-- Matching. -->
 | 
				
			||||||
 | 
					                        <ng-container *ngSwitchCase="'matching'">
 | 
				
			||||||
 | 
					                            <ion-item class="ion-text-wrap" *ngFor="let row of question.rows">
 | 
				
			||||||
 | 
					                                <ion-grid>
 | 
				
			||||||
 | 
					                                    <ion-row>
 | 
				
			||||||
 | 
					                                        <ion-col>
 | 
				
			||||||
 | 
					                                            <p><core-format-text id="addon-mod_lesson-matching-{{row.id}}" [component]="component"
 | 
				
			||||||
 | 
					                                                [componentId]="lesson?.coursemodule" [text]="row.text" contextLevel="module"
 | 
				
			||||||
 | 
					                                                [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                                            </core-format-text></p>
 | 
				
			||||||
 | 
					                                        </ion-col>
 | 
				
			||||||
 | 
					                                        <ion-col>
 | 
				
			||||||
 | 
					                                            <ion-select [id]="row.id" [formControlName]="row.name" interface="action-sheet"
 | 
				
			||||||
 | 
					                                                [attr.aria-labelledby]="'addon-mod_lesson-matching-' + row.id">
 | 
				
			||||||
 | 
					                                                <ion-select-option *ngFor="let option of row.options" [value]="option.value">
 | 
				
			||||||
 | 
					                                                    {{option.label}}
 | 
				
			||||||
 | 
					                                                </ion-select-option>
 | 
				
			||||||
 | 
					                                            </ion-select>
 | 
				
			||||||
 | 
					                                        </ion-col>
 | 
				
			||||||
 | 
					                                    </ion-row>
 | 
				
			||||||
 | 
					                                </ion-grid>
 | 
				
			||||||
 | 
					                            </ion-item>
 | 
				
			||||||
 | 
					                        </ng-container>
 | 
				
			||||||
 | 
					                    </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <ion-button expand="block" type="submit" class="ion-text-wrap ion-margin button-no-uppercase">
 | 
				
			||||||
 | 
					                        {{ question.submitLabel }}
 | 
				
			||||||
 | 
					                    </ion-button>
 | 
				
			||||||
 | 
					                    <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 -->
 | 
				
			||||||
 | 
					                    <input type="submit" class="core-submit-hidden-enter" />
 | 
				
			||||||
 | 
					                </form>
 | 
				
			||||||
 | 
					            </ion-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Page buttons and progress. -->
 | 
				
			||||||
 | 
					            <ion-list *ngIf="!eolData && !processData">
 | 
				
			||||||
 | 
					                <ion-grid *ngIf="pageButtons?.length" class="ion-text-wrap addon-mod_lesson-pagebuttons">
 | 
				
			||||||
 | 
					                    <ion-row class="ion-align-items-center">
 | 
				
			||||||
 | 
					                        <ion-col *ngFor="let button of pageButtons" size="12" size-md="6" size-lg="3" col-xl>
 | 
				
			||||||
 | 
					                            <ion-button expand="block" fill="outline" [id]="button.id"
 | 
				
			||||||
 | 
					                                (click)="buttonClicked(button.data)" class="ion-text-wrap button-no-uppercase">
 | 
				
			||||||
 | 
					                                {{ button.content }}
 | 
				
			||||||
 | 
					                            </ion-button>
 | 
				
			||||||
 | 
					                        </ion-col>
 | 
				
			||||||
 | 
					                    </ion-row>
 | 
				
			||||||
 | 
					                </ion-grid>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="lesson?.progressbar && !canManage && pageData">
 | 
				
			||||||
 | 
					                    <ion-label>
 | 
				
			||||||
 | 
					                        {{ 'addon.mod_lesson.progresscompleted' | translate:{$a: pageData.progress} }}
 | 
				
			||||||
 | 
					                        <core-progress-bar [progress]="pageData.progress"></core-progress-bar>
 | 
				
			||||||
 | 
					                    </ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <div class="core-info-card" *ngIf="lesson?.progressbar && canManage">
 | 
				
			||||||
 | 
					                    <ion-item>
 | 
				
			||||||
 | 
					                        <ion-icon name="fas-info-circle" slot="start"></ion-icon>
 | 
				
			||||||
 | 
					                        <ion-label>{{ 'addon.mod_lesson.progressbarteacherwarning2' | translate }}</ion-label>
 | 
				
			||||||
 | 
					                    </ion-item>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </ion-list>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- End of lesson reached. -->
 | 
				
			||||||
 | 
					            <ion-card *ngIf="eolData && !processData">
 | 
				
			||||||
 | 
					                <div class="core-warning-card" *ngIf="eolData.offline?.value">
 | 
				
			||||||
 | 
					                    <ion-item>
 | 
				
			||||||
 | 
					                        <ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
 | 
				
			||||||
 | 
					                        <ion-label>{{ 'addon.mod_lesson.finishretakeoffline' | translate }}</ion-label>
 | 
				
			||||||
 | 
					                    </ion-item>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <ion-card-header class="ion-text-wrap" *ngIf="eolData.gradelesson">
 | 
				
			||||||
 | 
					                    <ion-card-subtitle>{{ 'addon.mod_lesson.congratulations' | translate }}</ion-card-subtitle>
 | 
				
			||||||
 | 
					                </ion-card-header>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.notenoughtimespent" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.notenoughtimespent.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.numberofpagesviewed" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.numberofpagesviewed.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.youshouldview" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.youshouldview.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.numberofcorrectanswers" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.numberofcorrectanswers.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.displayscorewithessays" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label [innerHTML]="eolData.displayscorewithessays.message"></ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="!eolData.displayscorewithessays && eolData.displayscorewithoutessays"
 | 
				
			||||||
 | 
					                    lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.displayscorewithoutessays.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.yourcurrentgradeisoutof" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.yourcurrentgradeisoutof.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.eolstudentoutoftimenoanswers" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.eolstudentoutoftimenoanswers.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.welldone" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.welldone.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="lesson.progressbar && eolData.progresscompleted" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>
 | 
				
			||||||
 | 
					                        {{ 'addon.mod_lesson.progresscompleted' | translate:{$a: eolData.progresscompleted.value} }}
 | 
				
			||||||
 | 
					                        <core-progress-bar [progress]="eolData.progresscompleted.value"></core-progress-bar>
 | 
				
			||||||
 | 
					                    </ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.displayofgrade" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.displayofgrade.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-button *ngIf="eolData.reviewlesson" expand="block" class="ion-text-wrap ion-margin button-no-uppercase"
 | 
				
			||||||
 | 
					                    (click)="reviewLesson(reviewPageId!)">
 | 
				
			||||||
 | 
					                    {{ 'addon.mod_lesson.reviewlesson' | translate }}
 | 
				
			||||||
 | 
					                </ion-button>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="eolData.modattemptsnoteacher" lines="none">
 | 
				
			||||||
 | 
					                    <ion-label>{{ eolData.modattemptsnoteacher.message }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <!-- If activity link was successfully formatted, render the button. -->
 | 
				
			||||||
 | 
					                <ion-button *ngIf="activityLink && activityLink.formatted"
 | 
				
			||||||
 | 
					                    expand="block" color="light" [href]="activityLink.href" core-link [capture]="true"
 | 
				
			||||||
 | 
					                    class="ion-text-wrap ion-margin button-no-uppercase">
 | 
				
			||||||
 | 
					                    <core-format-text [text]="activityLink.label" contextLevel="module"
 | 
				
			||||||
 | 
					                        [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                    </core-format-text>
 | 
				
			||||||
 | 
					                </ion-button>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="activityLink && !activityLink.formatted"
 | 
				
			||||||
 | 
					                    lines="none">
 | 
				
			||||||
 | 
					                    <!-- Activity link wasn't formatted, render the original link. -->
 | 
				
			||||||
 | 
					                    <ion-label>
 | 
				
			||||||
 | 
					                        <core-format-text [text]="activityLink.label" contextLevel="module"
 | 
				
			||||||
 | 
					                            [contextInstanceId]="lesson?.coursemodule" [courseId]="courseId">
 | 
				
			||||||
 | 
					                        </core-format-text>
 | 
				
			||||||
 | 
					                    </ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					            </ion-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- Feedback returned when processing an action. -->
 | 
				
			||||||
 | 
					            <ion-list *ngIf="processData">
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="processData.ongoingscore && !processData.reviewmode" >
 | 
				
			||||||
 | 
					                    <ion-label>{{ processData.ongoingscore }}</ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					                <ion-item class="ion-text-wrap" *ngIf="!processData.reviewmode || review">
 | 
				
			||||||
 | 
					                    <ion-label>
 | 
				
			||||||
 | 
					                        <div *ngIf="!processData.reviewmode">
 | 
				
			||||||
 | 
					                            <core-format-text [component]="component" [componentId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                [text]="processData.feedback" contextLevel="module" [contextInstanceId]="lesson?.coursemodule"
 | 
				
			||||||
 | 
					                                [courseId]="courseId">
 | 
				
			||||||
 | 
					                            </core-format-text>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div *ngIf="review">
 | 
				
			||||||
 | 
					                            <p>{{ 'addon.mod_lesson.gotoendoflesson' | translate }}</p>
 | 
				
			||||||
 | 
					                            <p>{{ 'addon.mod_lesson.or' | translate }}</p>
 | 
				
			||||||
 | 
					                            <p>{{ 'addon.mod_lesson.continuetonextpage' | translate }}</p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </ion-label>
 | 
				
			||||||
 | 
					                </ion-item>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <ion-button expand="block" class="ion-text-wrap ion-margin" color="light" *ngIf="review"
 | 
				
			||||||
 | 
					                    (click)="changePage(LESSON_EOL)">
 | 
				
			||||||
 | 
					                    {{ 'addon.mod_lesson.finish' | translate }}
 | 
				
			||||||
 | 
					                </ion-button>
 | 
				
			||||||
 | 
					                <ion-button expand="block" class="ion-text-wrap ion-margin" color="light" *ngFor="let button of processDataButtons"
 | 
				
			||||||
 | 
					                    (click)="changePage(button.pageId, true)">
 | 
				
			||||||
 | 
					                    {{ button.label | translate }}
 | 
				
			||||||
 | 
					                </ion-button>
 | 
				
			||||||
 | 
					            </ion-list>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    </core-loading>
 | 
				
			||||||
 | 
					</ion-content>
 | 
				
			||||||
							
								
								
									
										51
									
								
								src/addons/mod/lesson/pages/player/player.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/addons/mod/lesson/pages/player/player.module.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { NgModule } from '@angular/core';
 | 
				
			||||||
 | 
					import { RouterModule, Routes } from '@angular/router';
 | 
				
			||||||
 | 
					import { CommonModule } from '@angular/common';
 | 
				
			||||||
 | 
					import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 | 
				
			||||||
 | 
					import { IonicModule } from '@ionic/angular';
 | 
				
			||||||
 | 
					import { TranslateModule } from '@ngx-translate/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CoreSharedModule } from '@/core/shared.module';
 | 
				
			||||||
 | 
					import { AddonModLessonPlayerPage } from './player';
 | 
				
			||||||
 | 
					import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
 | 
				
			||||||
 | 
					import { CanLeaveGuard } from '@guards/can-leave';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const routes: Routes = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        path: '',
 | 
				
			||||||
 | 
					        component: AddonModLessonPlayerPage,
 | 
				
			||||||
 | 
					        canDeactivate: [CanLeaveGuard],
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@NgModule({
 | 
				
			||||||
 | 
					    imports: [
 | 
				
			||||||
 | 
					        RouterModule.forChild(routes),
 | 
				
			||||||
 | 
					        CommonModule,
 | 
				
			||||||
 | 
					        IonicModule,
 | 
				
			||||||
 | 
					        TranslateModule.forChild(),
 | 
				
			||||||
 | 
					        FormsModule,
 | 
				
			||||||
 | 
					        ReactiveFormsModule,
 | 
				
			||||||
 | 
					        CoreSharedModule,
 | 
				
			||||||
 | 
					        CoreEditorComponentsModule,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    declarations: [
 | 
				
			||||||
 | 
					        AddonModLessonPlayerPage,
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    exports: [RouterModule],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonModLessonPlayerPageModule {}
 | 
				
			||||||
							
								
								
									
										46
									
								
								src/addons/mod/lesson/pages/player/player.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/addons/mod/lesson/pages/player/player.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					:host ::ng-deep {
 | 
				
			||||||
 | 
					    .addon-mod_lesson-slideshow {
 | 
				
			||||||
 | 
					        max-width: 100%;
 | 
				
			||||||
 | 
					        max-height: 100%;
 | 
				
			||||||
 | 
					        margin: 0 auto;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    table {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        margin-top: 1.5rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tr:nth-child(odd) {
 | 
				
			||||||
 | 
					            background-color: var(--gray-lighter);
 | 
				
			||||||
 | 
					            // @include darkmode() {
 | 
				
			||||||
 | 
					            //     background-color: $core-dark-item-divider-bg-color;
 | 
				
			||||||
 | 
					            // }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        tr:last-child td {
 | 
				
			||||||
 | 
					            border-bottom: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        td {
 | 
				
			||||||
 | 
					            padding: 5px;
 | 
				
			||||||
 | 
					            line-height: 1.5;
 | 
				
			||||||
 | 
					            border-bottom: 1px solid var(--gray);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // @todo
 | 
				
			||||||
 | 
					    // .item-ios table {
 | 
				
			||||||
 | 
					    //     @extend .card-ios;
 | 
				
			||||||
 | 
					    //     @include darkmode() {
 | 
				
			||||||
 | 
					    //         color: $white;
 | 
				
			||||||
 | 
					    //         background-color: $core-dark-item-bg-color;
 | 
				
			||||||
 | 
					    //     }
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // .item-md table {
 | 
				
			||||||
 | 
					    //     @extend .card-md;
 | 
				
			||||||
 | 
					    //     @include darkmode() {
 | 
				
			||||||
 | 
					    //         color: $white;
 | 
				
			||||||
 | 
					    //         background-color: $core-dark-item-bg-color;
 | 
				
			||||||
 | 
					    //     }
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										806
									
								
								src/addons/mod/lesson/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										806
									
								
								src/addons/mod/lesson/pages/player/player.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,806 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Component, OnInit, OnDestroy, ViewChild, ChangeDetectorRef, ElementRef } from '@angular/core';
 | 
				
			||||||
 | 
					import { FormBuilder, FormGroup } from '@angular/forms';
 | 
				
			||||||
 | 
					import { IonContent } from '@ionic/angular';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CoreError } from '@classes/errors/error';
 | 
				
			||||||
 | 
					import { CanLeave } from '@guards/can-leave';
 | 
				
			||||||
 | 
					import { CoreApp } from '@services/app';
 | 
				
			||||||
 | 
					import { CoreNavigator } from '@services/navigator';
 | 
				
			||||||
 | 
					import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
 | 
				
			||||||
 | 
					import { CoreSync } from '@services/sync';
 | 
				
			||||||
 | 
					import { CoreDomUtils } from '@services/utils/dom';
 | 
				
			||||||
 | 
					import { CoreUrlUtils } from '@services/utils/url';
 | 
				
			||||||
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
 | 
					import { CoreWSExternalFile } from '@services/ws';
 | 
				
			||||||
 | 
					import { ModalController, Translate } from '@singletons';
 | 
				
			||||||
 | 
					import { CoreEvents } from '@singletons/events';
 | 
				
			||||||
 | 
					import { AddonModLessonMenuModalPage } from '../../components/menu-modal/menu-modal';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    AddonModLesson,
 | 
				
			||||||
 | 
					    AddonModLessonEOLPageDataEntry,
 | 
				
			||||||
 | 
					    AddonModLessonFinishRetakeResponse,
 | 
				
			||||||
 | 
					    AddonModLessonGetAccessInformationWSResponse,
 | 
				
			||||||
 | 
					    AddonModLessonGetPageDataWSResponse,
 | 
				
			||||||
 | 
					    AddonModLessonGetPagesPageWSData,
 | 
				
			||||||
 | 
					    AddonModLessonLaunchAttemptWSResponse,
 | 
				
			||||||
 | 
					    AddonModLessonLessonWSData,
 | 
				
			||||||
 | 
					    AddonModLessonMessageWSData,
 | 
				
			||||||
 | 
					    AddonModLessonPageWSData,
 | 
				
			||||||
 | 
					    AddonModLessonPossibleJumps,
 | 
				
			||||||
 | 
					    AddonModLessonProcessPageOptions,
 | 
				
			||||||
 | 
					    AddonModLessonProcessPageResponse,
 | 
				
			||||||
 | 
					    AddonModLessonProvider,
 | 
				
			||||||
 | 
					} from '../../services/lesson';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    AddonModLessonActivityLink,
 | 
				
			||||||
 | 
					    AddonModLessonHelper,
 | 
				
			||||||
 | 
					    AddonModLessonPageButton,
 | 
				
			||||||
 | 
					    AddonModLessonQuestion,
 | 
				
			||||||
 | 
					} from '../../services/lesson-helper';
 | 
				
			||||||
 | 
					import { AddonModLessonOffline } from '../../services/lesson-offline';
 | 
				
			||||||
 | 
					import { AddonModLessonSync } from '../../services/lesson-sync';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Page that allows attempting and reviewing a lesson.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'page-addon-mod-lesson-player',
 | 
				
			||||||
 | 
					    templateUrl: 'player.html',
 | 
				
			||||||
 | 
					    styleUrls: ['player.scss'],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class AddonModLessonPlayerPage implements OnInit, OnDestroy, CanLeave {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @ViewChild(IonContent) content?: IonContent;
 | 
				
			||||||
 | 
					    @ViewChild('questionFormEl') formElement?: ElementRef;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    component = AddonModLessonProvider.COMPONENT;
 | 
				
			||||||
 | 
					    readonly LESSON_EOL = AddonModLessonProvider.LESSON_EOL;
 | 
				
			||||||
 | 
					    questionForm?: FormGroup; // The FormGroup for question pages.
 | 
				
			||||||
 | 
					    title?: string; // The page title.
 | 
				
			||||||
 | 
					    lesson?: AddonModLessonLessonWSData; // The lesson object.
 | 
				
			||||||
 | 
					    currentPage?: number; // Current page being viewed.
 | 
				
			||||||
 | 
					    review?: boolean; // Whether the user is reviewing.
 | 
				
			||||||
 | 
					    messages: AddonModLessonMessageWSData[] = []; // Messages to display to the user.
 | 
				
			||||||
 | 
					    canManage?: boolean; // Whether the user can manage the lesson.
 | 
				
			||||||
 | 
					    retake?: number; // Current retake number.
 | 
				
			||||||
 | 
					    showRetake?: boolean; // Whether the retake number needs to be displayed.
 | 
				
			||||||
 | 
					    lessonWidth?: string; // Width of the lesson (if slideshow mode).
 | 
				
			||||||
 | 
					    lessonHeight?: string; // Height of the lesson (if slideshow mode).
 | 
				
			||||||
 | 
					    endTime?: number; // End time of the lesson if it's timed.
 | 
				
			||||||
 | 
					    pageData?: AddonModLessonGetPageDataWSResponse; // Current page data.
 | 
				
			||||||
 | 
					    pageContent?: string; // Current page contents.
 | 
				
			||||||
 | 
					    pageButtons?: AddonModLessonPageButton[]; // List of buttons of the current page.
 | 
				
			||||||
 | 
					    question?: AddonModLessonQuestion; // Question of the current page (if it's a question page).
 | 
				
			||||||
 | 
					    eolData?: Record<string, AddonModLessonEOLPageDataEntry>; // Data for EOL page (if current page is EOL).
 | 
				
			||||||
 | 
					    processData?: AddonModLessonProcessPageResponse; // Data to display after processing a page.
 | 
				
			||||||
 | 
					    processDataButtons: ProcessDataButton[] = []; // Buttons to display after processing a page.
 | 
				
			||||||
 | 
					    loaded?: boolean; // Whether data has been loaded.
 | 
				
			||||||
 | 
					    displayMenu?: boolean; // Whether the lesson menu should be displayed.
 | 
				
			||||||
 | 
					    originalData?: Record<string, unknown>; // Original question data. It is used to check if data has changed.
 | 
				
			||||||
 | 
					    reviewPageId?: number; // Page to open if the user wants to review the attempt.
 | 
				
			||||||
 | 
					    courseId!: number; // The course ID the lesson belongs to.
 | 
				
			||||||
 | 
					    lessonPages?: AddonModLessonPageWSData[]; // Lesson pages (for the lesson menu).
 | 
				
			||||||
 | 
					    loadingMenu?: boolean; // Whether the lesson menu is being loaded.
 | 
				
			||||||
 | 
					    mediaFile?: CoreWSExternalFile; // Media file of the lesson.
 | 
				
			||||||
 | 
					    activityLink?: AddonModLessonActivityLink; // Next activity link data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected lessonId!: number; // Lesson ID.
 | 
				
			||||||
 | 
					    protected password?: string; // Lesson password (if any).
 | 
				
			||||||
 | 
					    protected forceLeave = false; // If true, don't perform any check when leaving the view.
 | 
				
			||||||
 | 
					    protected offline?: boolean; // Whether we are in offline mode.
 | 
				
			||||||
 | 
					    protected accessInfo?: AddonModLessonGetAccessInformationWSResponse; // Lesson access info.
 | 
				
			||||||
 | 
					    protected jumps?: AddonModLessonPossibleJumps; // All possible jumps.
 | 
				
			||||||
 | 
					    protected firstPageLoaded?: boolean; // Whether the first page has been loaded.
 | 
				
			||||||
 | 
					    protected retakeToReview?: number; // Retake to review.
 | 
				
			||||||
 | 
					    protected menuShown = false; // Whether menu is shown.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(
 | 
				
			||||||
 | 
					        protected changeDetector: ChangeDetectorRef,
 | 
				
			||||||
 | 
					        protected formBuilder: FormBuilder,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Component being initialized.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async ngOnInit(): Promise<void> {
 | 
				
			||||||
 | 
					        const lessonId = CoreNavigator.instance.getRouteNumberParam('lessonId');
 | 
				
			||||||
 | 
					        const courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
 | 
				
			||||||
 | 
					        if (!lessonId || !courseId) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModal('No lesson ID or course ID supplied.');
 | 
				
			||||||
 | 
					            this.forceLeave = true;
 | 
				
			||||||
 | 
					            CoreNavigator.instance.back();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.lessonId = lessonId;
 | 
				
			||||||
 | 
					        this.courseId = courseId;
 | 
				
			||||||
 | 
					        this.password = CoreNavigator.instance.getRouteParam('password');
 | 
				
			||||||
 | 
					        this.review = !!CoreNavigator.instance.getRouteBooleanParam('review');
 | 
				
			||||||
 | 
					        this.currentPage = CoreNavigator.instance.getRouteNumberParam('pageId');
 | 
				
			||||||
 | 
					        this.retakeToReview = CoreNavigator.instance.getRouteNumberParam('retake');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Block the lesson so it cannot be synced.
 | 
				
			||||||
 | 
					        CoreSync.instance.blockOperation(this.component, this.lessonId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Fetch the Lesson data.
 | 
				
			||||||
 | 
					            const success = await this.fetchLessonData();
 | 
				
			||||||
 | 
					            if (success) {
 | 
				
			||||||
 | 
					                // Review data loaded or new retake started, remove any retake being finished in sync.
 | 
				
			||||||
 | 
					                AddonModLessonSync.instance.deleteRetakeFinishedInSync(this.lessonId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Component being destroyed.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ngOnDestroy(): void {
 | 
				
			||||||
 | 
					        // Unblock the lesson so it can be synced.
 | 
				
			||||||
 | 
					        CoreSync.instance.unblockOperation(this.component, this.lessonId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check if we can leave the page or not.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return Resolved if we can leave it, rejected if not.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async canLeave(): Promise<boolean> {
 | 
				
			||||||
 | 
					        if (this.forceLeave || !this.questionForm) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.question && !this.eolData && !this.processData && this.originalData) {
 | 
				
			||||||
 | 
					            // Question shown. Check if there is any change.
 | 
				
			||||||
 | 
					            if (!CoreUtils.instance.basicLeftCompare(this.questionForm.getRawValue(), this.originalData, 3)) {
 | 
				
			||||||
 | 
					                await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.confirmcanceledit'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Runs when the page is about to leave and no longer be the active page.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ionViewWillLeave(): void {
 | 
				
			||||||
 | 
					        if (this.menuShown) {
 | 
				
			||||||
 | 
					            ModalController.instance.dismiss();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * A button was clicked.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param data Button data.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    buttonClicked(data: Record<string, string>): void {
 | 
				
			||||||
 | 
					        this.processPage(data);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Call a function and go offline if allowed and the call fails.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param func Function to call.
 | 
				
			||||||
 | 
					     * @param options Options passed to the function.
 | 
				
			||||||
 | 
					     * @return Promise resolved in success, rejected otherwise.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async callFunction<T>(func: () => Promise<T>, options: CommonOptions): Promise<T> {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            return await func();
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            if (this.offline || this.review || !AddonModLesson.instance.isLessonOffline(this.lesson!)) {
 | 
				
			||||||
 | 
					                // Already offline or not allowed.
 | 
				
			||||||
 | 
					                throw error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (CoreUtils.instance.isWebServiceError(error)) {
 | 
				
			||||||
 | 
					                // WebService returned an error, cannot perform the action.
 | 
				
			||||||
 | 
					                throw error;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Go offline and retry.
 | 
				
			||||||
 | 
					            this.offline = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get the possible jumps now.
 | 
				
			||||||
 | 
					            this.jumps = await AddonModLesson.instance.getPagesPossibleJumps(this.lesson!.id, {
 | 
				
			||||||
 | 
					                cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					                readingStrategy: CoreSitesReadingStrategy.PreferCache,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Call the function again with offline mode and the new jumps.
 | 
				
			||||||
 | 
					            options.readingStrategy = CoreSitesReadingStrategy.PreferCache;
 | 
				
			||||||
 | 
					            options.jumps = this.jumps;
 | 
				
			||||||
 | 
					            options.offline = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return func();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Change the page from menu or when continuing from a feedback page.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId Page to load.
 | 
				
			||||||
 | 
					     * @param ignoreCurrent If true, allow loading current page.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async changePage(pageId: number, ignoreCurrent?: boolean): Promise<void> {
 | 
				
			||||||
 | 
					        if (!ignoreCurrent && !this.eolData && this.currentPage == pageId) {
 | 
				
			||||||
 | 
					            // Page already loaded, stop.
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loaded = true;
 | 
				
			||||||
 | 
					        this.messages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await this.loadPage(pageId);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading page');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the lesson data and load the page.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return Promise resolved with true if success, resolved with false otherwise.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async fetchLessonData(): Promise<boolean> {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            // Wait for any ongoing sync to finish. We won't sync a lesson while it's being played.
 | 
				
			||||||
 | 
					            await AddonModLessonSync.instance.waitForSync(this.lessonId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.lesson = await AddonModLesson.instance.getLessonById(this.courseId, this.lessonId);
 | 
				
			||||||
 | 
					            this.title = this.lesson.name; // Temporary title.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If lesson has offline data already, use offline mode.
 | 
				
			||||||
 | 
					            this.offline = await AddonModLessonOffline.instance.hasOfflineData(this.lessonId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!this.offline && !CoreApp.instance.isOnline() && AddonModLesson.instance.isLessonOffline(this.lesson) &&
 | 
				
			||||||
 | 
					                !this.review) {
 | 
				
			||||||
 | 
					                // Lesson doesn't have offline data, but it allows offline and the device is offline. Use offline mode.
 | 
				
			||||||
 | 
					                this.offline = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const options = {
 | 
				
			||||||
 | 
					                cmId: this.lesson.coursemodule,
 | 
				
			||||||
 | 
					                readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            this.accessInfo = await this.callFunction<AddonModLessonGetAccessInformationWSResponse>(
 | 
				
			||||||
 | 
					                AddonModLesson.instance.getAccessInformation.bind(AddonModLesson.instance, this.lesson.id, options),
 | 
				
			||||||
 | 
					                options,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const promises: Promise<void>[] = [];
 | 
				
			||||||
 | 
					            this.canManage = this.accessInfo.canmanage;
 | 
				
			||||||
 | 
					            this.retake = this.accessInfo.attemptscount;
 | 
				
			||||||
 | 
					            this.showRetake = !this.currentPage && this.retake > 0; // Only show it in first page if it isn't the first retake.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.accessInfo.preventaccessreasons.length) {
 | 
				
			||||||
 | 
					                // If it's a password protected lesson and we have the password, allow playing it.
 | 
				
			||||||
 | 
					                const preventReason = AddonModLesson.instance.getPreventAccessReason(this.accessInfo, !!this.password, this.review);
 | 
				
			||||||
 | 
					                if (preventReason) {
 | 
				
			||||||
 | 
					                    // Lesson cannot be played, show message and go back.
 | 
				
			||||||
 | 
					                    throw new CoreError(preventReason.message);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.review && this.retakeToReview != this.accessInfo.attemptscount - 1) {
 | 
				
			||||||
 | 
					                // Reviewing a retake that isn't the last one. Error.
 | 
				
			||||||
 | 
					                throw new CoreError(Translate.instance.instant('addon.mod_lesson.errorreviewretakenotlast'));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.password) {
 | 
				
			||||||
 | 
					                // Lesson uses password, get the whole lesson object.
 | 
				
			||||||
 | 
					                const options = {
 | 
				
			||||||
 | 
					                    password: this.password,
 | 
				
			||||||
 | 
					                    cmId: this.lesson.coursemodule,
 | 
				
			||||||
 | 
					                    readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					                promises.push(this.callFunction<AddonModLessonLessonWSData>(
 | 
				
			||||||
 | 
					                    AddonModLesson.instance.getLessonWithPassword.bind(AddonModLesson.instance, this.lesson.id, options),
 | 
				
			||||||
 | 
					                    options,
 | 
				
			||||||
 | 
					                ).then((lesson) => {
 | 
				
			||||||
 | 
					                    this.lesson = lesson;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.offline) {
 | 
				
			||||||
 | 
					                // Offline mode, get the list of possible jumps to allow navigation.
 | 
				
			||||||
 | 
					                promises.push(AddonModLesson.instance.getPagesPossibleJumps(this.lesson.id, {
 | 
				
			||||||
 | 
					                    cmId: this.lesson.coursemodule,
 | 
				
			||||||
 | 
					                    readingStrategy: CoreSitesReadingStrategy.PreferCache,
 | 
				
			||||||
 | 
					                }).then((jumpList) => {
 | 
				
			||||||
 | 
					                    this.jumps = jumpList;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await Promise.all(promises);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.mediaFile = this.lesson.mediafiles?.[0];
 | 
				
			||||||
 | 
					            this.lessonWidth = this.lesson.slideshow ? CoreDomUtils.instance.formatPixelsSize(this.lesson.mediawidth!) : '';
 | 
				
			||||||
 | 
					            this.lessonHeight = this.lesson.slideshow ? CoreDomUtils.instance.formatPixelsSize(this.lesson.mediaheight!) : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await this.launchRetake(this.currentPage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.review && this.retakeToReview && CoreUtils.instance.isWebServiceError(error)) {
 | 
				
			||||||
 | 
					                // The user cannot review the retake. Unmark the retake as being finished in sync.
 | 
				
			||||||
 | 
					                await AddonModLessonSync.instance.deleteRetakeFinishedInSync(this.lessonId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | 
				
			||||||
 | 
					            this.forceLeave = true;
 | 
				
			||||||
 | 
					            CoreNavigator.instance.back();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Finish the retake.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param outOfTime Whether the retake is finished because the user ran out of time.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async finishRetake(outOfTime?: boolean): Promise<void> {
 | 
				
			||||||
 | 
					        this.messages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.offline && CoreApp.instance.isOnline()) {
 | 
				
			||||||
 | 
					            // Offline mode but the app is online. Try to sync the data.
 | 
				
			||||||
 | 
					            const result = await CoreUtils.instance.ignoreErrors(
 | 
				
			||||||
 | 
					                AddonModLessonSync.instance.syncLesson(this.lesson!.id, true, true),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (result?.warnings?.length) {
 | 
				
			||||||
 | 
					                // Some data was deleted. Check if the retake has changed.
 | 
				
			||||||
 | 
					                const info = await AddonModLesson.instance.getAccessInformation(this.lesson!.id, {
 | 
				
			||||||
 | 
					                    cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (info.attemptscount != this.accessInfo!.attemptscount) {
 | 
				
			||||||
 | 
					                    // The retake has changed. Leave the view and show the error.
 | 
				
			||||||
 | 
					                    this.forceLeave = true;
 | 
				
			||||||
 | 
					                    CoreNavigator.instance.back();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    throw new CoreError(result.warnings[0]);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Retake hasn't changed, show the warning and finish the retake in offline.
 | 
				
			||||||
 | 
					                CoreDomUtils.instance.showErrorModal(result.warnings[0]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.offline = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Now finish the retake.
 | 
				
			||||||
 | 
					        const options = {
 | 
				
			||||||
 | 
					            password: this.password,
 | 
				
			||||||
 | 
					            outOfTime,
 | 
				
			||||||
 | 
					            review: this.review,
 | 
				
			||||||
 | 
					            offline: this.offline,
 | 
				
			||||||
 | 
					            accessInfo: this.accessInfo,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        const data = await this.callFunction<AddonModLessonFinishRetakeResponse>(
 | 
				
			||||||
 | 
					            AddonModLesson.instance.finishRetake.bind(AddonModLesson.instance, this.lesson, this.courseId, options),
 | 
				
			||||||
 | 
					            options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.title = this.lesson!.name;
 | 
				
			||||||
 | 
					        this.eolData = data.data;
 | 
				
			||||||
 | 
					        this.messages = this.messages.concat(data.messages);
 | 
				
			||||||
 | 
					        this.processData = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: 'lesson' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Format activity link if present.
 | 
				
			||||||
 | 
					        if (this.eolData.activitylink) {
 | 
				
			||||||
 | 
					            this.activityLink = AddonModLessonHelper.instance.formatActivityLink(<string> this.eolData.activitylink.value);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.activityLink = undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Format review lesson if present.
 | 
				
			||||||
 | 
					        if (this.eolData.reviewlesson) {
 | 
				
			||||||
 | 
					            const params = CoreUrlUtils.instance.extractUrlParams(<string> this.eolData.reviewlesson.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!params || !params.pageid) {
 | 
				
			||||||
 | 
					                // No pageid in the URL, the user cannot review (probably didn't answer any question).
 | 
				
			||||||
 | 
					                delete this.eolData.reviewlesson;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.reviewPageId = Number(params.pageid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Jump to a certain page after performing an action.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId The page to load.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async jumpToPage(pageId: number): Promise<void> {
 | 
				
			||||||
 | 
					        if (pageId === 0) {
 | 
				
			||||||
 | 
					            // Not a valid page, return to entry view.
 | 
				
			||||||
 | 
					            // This happens, for example, when the user clicks to go to previous page and there is no previous page.
 | 
				
			||||||
 | 
					            this.forceLeave = true;
 | 
				
			||||||
 | 
					            CoreNavigator.instance.back();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        } else if (pageId == AddonModLessonProvider.LESSON_EOL) {
 | 
				
			||||||
 | 
					            // End of lesson reached.
 | 
				
			||||||
 | 
					            return this.finishRetake();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Load new page.
 | 
				
			||||||
 | 
					        this.messages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.loadPage(pageId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Start or continue a retake.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId The page to load.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async launchRetake(pageId?: number): Promise<void> {
 | 
				
			||||||
 | 
					        let data: AddonModLessonLaunchAttemptWSResponse | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.review) {
 | 
				
			||||||
 | 
					            // Review mode, no need to launch the retake.
 | 
				
			||||||
 | 
					        } else if (!this.offline) {
 | 
				
			||||||
 | 
					            // Not in offline mode, launch the retake.
 | 
				
			||||||
 | 
					            data = await AddonModLesson.instance.launchRetake(this.lesson!.id, this.password, pageId);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Check if there is a finished offline retake.
 | 
				
			||||||
 | 
					            const finished = await AddonModLessonOffline.instance.hasFinishedRetake(this.lesson!.id);
 | 
				
			||||||
 | 
					            if (finished) {
 | 
				
			||||||
 | 
					                // Always show EOL page.
 | 
				
			||||||
 | 
					                pageId = AddonModLessonProvider.LESSON_EOL;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.currentPage = pageId || this.accessInfo!.firstpageid;
 | 
				
			||||||
 | 
					        this.messages = data?.messages || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.lesson!.timelimit && !this.accessInfo!.canmanage) {
 | 
				
			||||||
 | 
					            // Get the last lesson timer.
 | 
				
			||||||
 | 
					            const timers = await AddonModLesson.instance.getTimers(this.lesson!.id, {
 | 
				
			||||||
 | 
					                cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					                readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.endTime = timers[timers.length - 1].starttime + this.lesson!.timelimit;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.loadPage(this.currentPage);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load the lesson menu.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async loadMenu(): Promise<void> {
 | 
				
			||||||
 | 
					        if (this.loadingMenu) {
 | 
				
			||||||
 | 
					            // Already loading.
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            this.loadingMenu = true;
 | 
				
			||||||
 | 
					            const options = {
 | 
				
			||||||
 | 
					                password: this.password,
 | 
				
			||||||
 | 
					                cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					                readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const pages = await this.callFunction<AddonModLessonGetPagesPageWSData[]>(
 | 
				
			||||||
 | 
					                AddonModLesson.instance.getPages.bind(AddonModLesson.instance, this.lessonId, options),
 | 
				
			||||||
 | 
					                options,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.lessonPages = pages.map((entry) => entry.page);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading menu.');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loadingMenu = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Load a certain page.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId The page to load.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async loadPage(pageId: number): Promise<void> {
 | 
				
			||||||
 | 
					        if (pageId == AddonModLessonProvider.LESSON_EOL) {
 | 
				
			||||||
 | 
					            // End of lesson reached.
 | 
				
			||||||
 | 
					            return this.finishRetake();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const options = {
 | 
				
			||||||
 | 
					            password: this.password,
 | 
				
			||||||
 | 
					            review: this.review,
 | 
				
			||||||
 | 
					            includeContents: true,
 | 
				
			||||||
 | 
					            cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					            readingStrategy: this.offline ? CoreSitesReadingStrategy.PreferCache : CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					            accessInfo: this.accessInfo,
 | 
				
			||||||
 | 
					            jumps: this.jumps,
 | 
				
			||||||
 | 
					            includeOfflineData: true,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const data = await this.callFunction<AddonModLessonGetPageDataWSResponse>(
 | 
				
			||||||
 | 
					            AddonModLesson.instance.getPageData.bind(AddonModLesson.instance, this.lesson, pageId, options),
 | 
				
			||||||
 | 
					            options,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data.newpageid == AddonModLessonProvider.LESSON_EOL) {
 | 
				
			||||||
 | 
					            // End of lesson reached.
 | 
				
			||||||
 | 
					            return this.finishRetake();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.pageData = data;
 | 
				
			||||||
 | 
					        this.title = data.page!.title;
 | 
				
			||||||
 | 
					        this.pageContent = AddonModLessonHelper.instance.getPageContentsFromPageData(data);
 | 
				
			||||||
 | 
					        this.loaded = true;
 | 
				
			||||||
 | 
					        this.currentPage = pageId;
 | 
				
			||||||
 | 
					        this.messages = this.messages.concat(data.messages);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Page loaded, hide EOL and feedback data if shown.
 | 
				
			||||||
 | 
					        this.eolData = this.processData = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (AddonModLesson.instance.isQuestionPage(data.page!.type)) {
 | 
				
			||||||
 | 
					            // Create an empty FormGroup without controls, they will be added in getQuestionFromPageData.
 | 
				
			||||||
 | 
					            this.questionForm = this.formBuilder.group({});
 | 
				
			||||||
 | 
					            this.pageButtons = [];
 | 
				
			||||||
 | 
					            this.question = AddonModLessonHelper.instance.getQuestionFromPageData(this.questionForm, data);
 | 
				
			||||||
 | 
					            this.originalData = this.questionForm.getRawValue(); // Use getRawValue to include disabled values.
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.pageButtons = AddonModLessonHelper.instance.getPageButtonsFromHtml(data.pagecontent || '');
 | 
				
			||||||
 | 
					            this.question = undefined;
 | 
				
			||||||
 | 
					            this.originalData = undefined;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data.displaymenu && !this.displayMenu) {
 | 
				
			||||||
 | 
					            // Load the menu.
 | 
				
			||||||
 | 
					            this.loadMenu();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        this.displayMenu = !!data.displaymenu;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!this.firstPageLoaded) {
 | 
				
			||||||
 | 
					            this.firstPageLoaded = true;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.showRetake = false;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Process a page, sending some data.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param data The data to send.
 | 
				
			||||||
 | 
					     * @param formSubmitted Whether a form was submitted.
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    protected async processPage(data: Record<string, unknown>, formSubmitted?: boolean): Promise<void> {
 | 
				
			||||||
 | 
					        this.loaded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const options: AddonModLessonProcessPageOptions = {
 | 
				
			||||||
 | 
					            password: this.password,
 | 
				
			||||||
 | 
					            review: this.review,
 | 
				
			||||||
 | 
					            offline: this.offline,
 | 
				
			||||||
 | 
					            accessInfo: this.accessInfo,
 | 
				
			||||||
 | 
					            jumps: this.jumps,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            const result = await this.callFunction<AddonModLessonProcessPageResponse>(
 | 
				
			||||||
 | 
					                AddonModLesson.instance.processPage.bind(
 | 
				
			||||||
 | 
					                    AddonModLesson.instance,
 | 
				
			||||||
 | 
					                    this.lesson,
 | 
				
			||||||
 | 
					                    this.courseId,
 | 
				
			||||||
 | 
					                    this.pageData,
 | 
				
			||||||
 | 
					                    data,
 | 
				
			||||||
 | 
					                    options,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                options,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (formSubmitted) {
 | 
				
			||||||
 | 
					                CoreDomUtils.instance.triggerFormSubmittedEvent(
 | 
				
			||||||
 | 
					                    this.formElement,
 | 
				
			||||||
 | 
					                    result.sent,
 | 
				
			||||||
 | 
					                    CoreSites.instance.getCurrentSiteId(),
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!this.offline && !this.review && AddonModLesson.instance.isLessonOffline(this.lesson!)) {
 | 
				
			||||||
 | 
					                // Lesson allows offline and the user changed some data in server. Update cached data.
 | 
				
			||||||
 | 
					                const retake = this.accessInfo!.attemptscount;
 | 
				
			||||||
 | 
					                const options = {
 | 
				
			||||||
 | 
					                    cmId: this.lesson!.coursemodule,
 | 
				
			||||||
 | 
					                    readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Update in background the list of content pages viewed or question attempts.
 | 
				
			||||||
 | 
					                if (AddonModLesson.instance.isQuestionPage(this.pageData?.page?.type || -1)) {
 | 
				
			||||||
 | 
					                    AddonModLesson.instance.getQuestionsAttemptsOnline(this.lessonId, retake, options);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    AddonModLesson.instance.getContentPagesViewedOnline(this.lessonId, retake, options);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (result.nodefaultresponse || result.inmediatejump) {
 | 
				
			||||||
 | 
					                // Don't display feedback or force a redirect to a new page. Load the new page.
 | 
				
			||||||
 | 
					                return await this.jumpToPage(result.newpageid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Not inmediate jump, show the feedback.
 | 
				
			||||||
 | 
					            result.feedback = AddonModLessonHelper.instance.removeQuestionFromFeedback(result.feedback);
 | 
				
			||||||
 | 
					            this.messages = result.messages;
 | 
				
			||||||
 | 
					            this.processData = result;
 | 
				
			||||||
 | 
					            this.processDataButtons = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.lesson!.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
 | 
				
			||||||
 | 
					                    !result.maxattemptsreached && !result.reviewmode) {
 | 
				
			||||||
 | 
					                // User can try again, show button to do so.
 | 
				
			||||||
 | 
					                this.processDataButtons.push({
 | 
				
			||||||
 | 
					                    label: 'addon.mod_lesson.reviewquestionback',
 | 
				
			||||||
 | 
					                    pageId: this.currentPage!,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Button to continue.
 | 
				
			||||||
 | 
					            if (this.lesson!.review && !result.correctanswer && !result.noanswer && !result.isessayquestion &&
 | 
				
			||||||
 | 
					                    !result.maxattemptsreached) {
 | 
				
			||||||
 | 
					                /* If both the "Yes, I'd like to try again" and "No, I just want to go on to the next question" point to the
 | 
				
			||||||
 | 
					                    same page then don't show the "No, I just want to go on to the next question" button. It's confusing. */
 | 
				
			||||||
 | 
					                if (this.pageData!.page!.id != result.newpageid) {
 | 
				
			||||||
 | 
					                    // Button to continue the lesson (the page to go is configured by the teacher).
 | 
				
			||||||
 | 
					                    this.processDataButtons.push({
 | 
				
			||||||
 | 
					                        label: 'addon.mod_lesson.reviewquestioncontinue',
 | 
				
			||||||
 | 
					                        pageId: result.newpageid,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                this.processDataButtons.push({
 | 
				
			||||||
 | 
					                    label: 'addon.mod_lesson.continue',
 | 
				
			||||||
 | 
					                    pageId: result.newpageid,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'Error processing page');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Review the lesson.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param pageId Page to load.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async reviewLesson(pageId: number): Promise<void> {
 | 
				
			||||||
 | 
					        this.loaded = false;
 | 
				
			||||||
 | 
					        this.review = true;
 | 
				
			||||||
 | 
					        this.offline = false; // Don't allow offline mode in review.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await this.loadPage(pageId);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading page');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Submit a question.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param e Event.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    submitQuestion(e: Event): void {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.loaded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Use getRawValue to include disabled values.
 | 
				
			||||||
 | 
					        const data = AddonModLessonHelper.instance.prepareQuestionData(this.question!, this.questionForm!.getRawValue());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.processPage(data, true).finally(() => {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Time up.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async timeUp(): Promise<void> {
 | 
				
			||||||
 | 
					        // Time up called, hide the timer.
 | 
				
			||||||
 | 
					        this.endTime = undefined;
 | 
				
			||||||
 | 
					        this.loaded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            await this.finishRetake(true);
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					            CoreDomUtils.instance.showErrorModalDefault(error, 'Error finishing attempt');
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            this.loaded = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Show the navigation modal.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return Promise resolved when done.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    async showMenu(): Promise<void> {
 | 
				
			||||||
 | 
					        this.menuShown = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const menuModal = await ModalController.instance.create({
 | 
				
			||||||
 | 
					            component: AddonModLessonMenuModalPage,
 | 
				
			||||||
 | 
					            componentProps: {
 | 
				
			||||||
 | 
					                pageInstance: this,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            cssClass: 'core-modal-lateral',
 | 
				
			||||||
 | 
					            showBackdrop: true,
 | 
				
			||||||
 | 
					            backdropDismiss: true,
 | 
				
			||||||
 | 
					            // @todo enterAnimation: 'core-modal-lateral-transition',
 | 
				
			||||||
 | 
					            // leaveAnimation: 'core-modal-lateral-transition',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await menuModal.present();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await menuModal.onWillDismiss();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.menuShown = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Common options for functions called using callFunction.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					type CommonOptions = CoreSitesCommonWSOptions & {
 | 
				
			||||||
 | 
					    jumps?: AddonModLessonPossibleJumps;
 | 
				
			||||||
 | 
					    offline?: boolean;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Button displayed after processing a page.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					type ProcessDataButton = {
 | 
				
			||||||
 | 
					    label: string;
 | 
				
			||||||
 | 
					    pageId: number;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@ -42,7 +42,7 @@ export class AddonModLessonHelperProvider {
 | 
				
			|||||||
     * @param activityLink HTML of the activity link.
 | 
					     * @param activityLink HTML of the activity link.
 | 
				
			||||||
     * @return Formatted data.
 | 
					     * @return Formatted data.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    formatActivityLink(activityLink: string): {formatted: boolean; label: string; href: string} {
 | 
					    formatActivityLink(activityLink: string): AddonModLessonActivityLink {
 | 
				
			||||||
        const element = CoreDomUtils.instance.convertToElement(activityLink);
 | 
					        const element = CoreDomUtils.instance.convertToElement(activityLink);
 | 
				
			||||||
        const anchor = element.querySelector('a');
 | 
					        const anchor = element.querySelector('a');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -264,7 +264,7 @@ export class AddonModLessonHelperProvider {
 | 
				
			|||||||
                value: input.value,
 | 
					                value: input.value,
 | 
				
			||||||
                checked: !!input.checked,
 | 
					                checked: !!input.checked,
 | 
				
			||||||
                disabled: !!input.disabled,
 | 
					                disabled: !!input.disabled,
 | 
				
			||||||
                text: parent?.innerHTML.trim() || '',
 | 
					                text: '',
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (option.checked || multiChoiceQuestion.multi) {
 | 
					            if (option.checked || multiChoiceQuestion.multi) {
 | 
				
			||||||
@ -277,6 +277,7 @@ export class AddonModLessonHelperProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // Remove the input and use the rest of the parent contents as the label.
 | 
					            // Remove the input and use the rest of the parent contents as the label.
 | 
				
			||||||
            input.remove();
 | 
					            input.remove();
 | 
				
			||||||
 | 
					            option.text = parent?.innerHTML.trim() || '';
 | 
				
			||||||
            multiChoiceQuestion.options!.push(option);
 | 
					            multiChoiceQuestion.options!.push(option);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -601,7 +602,7 @@ export type AddonModLessonPageButton = {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Generic question data.
 | 
					 * Generic question data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonQuestion = {
 | 
					export type AddonModLessonQuestionBasicData = {
 | 
				
			||||||
    template: string; // Name of the template to use.
 | 
					    template: string; // Name of the template to use.
 | 
				
			||||||
    submitLabel: string; // Text to display in submit.
 | 
					    submitLabel: string; // Text to display in submit.
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -609,7 +610,7 @@ export type AddonModLessonQuestion = {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Multichoice question data.
 | 
					 * Multichoice question data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonMultichoiceQuestion = AddonModLessonQuestion & {
 | 
					export type AddonModLessonMultichoiceQuestion = AddonModLessonQuestionBasicData & {
 | 
				
			||||||
    multi: boolean; // Whether it allows multiple answers.
 | 
					    multi: boolean; // Whether it allows multiple answers.
 | 
				
			||||||
    options: AddonModLessonMultichoiceOption[]; // Options for multichoice question.
 | 
					    options: AddonModLessonMultichoiceOption[]; // Options for multichoice question.
 | 
				
			||||||
    controlName?: string; // Name of the form control, for single choice.
 | 
					    controlName?: string; // Name of the form control, for single choice.
 | 
				
			||||||
@ -618,14 +619,14 @@ export type AddonModLessonMultichoiceQuestion = AddonModLessonQuestion & {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Short answer or numeric question data.
 | 
					 * Short answer or numeric question data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonInputQuestion = AddonModLessonQuestion & {
 | 
					export type AddonModLessonInputQuestion = AddonModLessonQuestionBasicData & {
 | 
				
			||||||
    input?: AddonModLessonQuestionInput; // Text input for text/number questions.
 | 
					    input?: AddonModLessonQuestionInput; // Text input for text/number questions.
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Essay question data.
 | 
					 * Essay question data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonEssayQuestion = AddonModLessonQuestion & {
 | 
					export type AddonModLessonEssayQuestion = AddonModLessonQuestionBasicData & {
 | 
				
			||||||
    useranswer?: string; // User answer, for reviewing.
 | 
					    useranswer?: string; // User answer, for reviewing.
 | 
				
			||||||
    textarea?: AddonModLessonTextareaData; // Data for the textarea.
 | 
					    textarea?: AddonModLessonTextareaData; // Data for the textarea.
 | 
				
			||||||
    control?: FormControl; // Form control.
 | 
					    control?: FormControl; // Form control.
 | 
				
			||||||
@ -634,7 +635,7 @@ export type AddonModLessonEssayQuestion = AddonModLessonQuestion & {
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * Matching question data.
 | 
					 * Matching question data.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonMatchingQuestion = AddonModLessonQuestion & {
 | 
					export type AddonModLessonMatchingQuestion = AddonModLessonQuestionBasicData & {
 | 
				
			||||||
    rows: AddonModLessonMatchingRow[];
 | 
					    rows: AddonModLessonMatchingRow[];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -721,3 +722,18 @@ export type AddonModLessonSelectAnswerData = {
 | 
				
			|||||||
 */
 | 
					 */
 | 
				
			||||||
export type AddonModLessonAnswerData =
 | 
					export type AddonModLessonAnswerData =
 | 
				
			||||||
    AddonModLessonCheckboxAnswerData | AddonModLessonTextAnswerData | AddonModLessonSelectAnswerData | string;
 | 
					    AddonModLessonCheckboxAnswerData | AddonModLessonTextAnswerData | AddonModLessonSelectAnswerData | string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Any possible question data.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonModLessonQuestion = AddonModLessonQuestionBasicData & Partial<AddonModLessonMultichoiceQuestion> &
 | 
				
			||||||
 | 
					Partial<AddonModLessonInputQuestion> & Partial<AddonModLessonEssayQuestion> & Partial<AddonModLessonMatchingQuestion>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Activity link data.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export type AddonModLessonActivityLink = {
 | 
				
			||||||
 | 
					    formatted: boolean;
 | 
				
			||||||
 | 
					    label: string;
 | 
				
			||||||
 | 
					    href: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -2342,7 +2342,7 @@ export class AddonModLessonProvider {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (entry.reason == 'lessonopen' || entry.reason == 'lessonclosed') {
 | 
					            if (entry.reason == 'lessonopen' || entry.reason == 'lessonclosed') {
 | 
				
			||||||
                // Time restrictions are the most prioritary, return it.
 | 
					                // Time restrictions are the most prioritary, return it.
 | 
				
			||||||
                return reason;
 | 
					                return entry;
 | 
				
			||||||
            } else if (entry.reason == 'passwordprotectedlesson') {
 | 
					            } else if (entry.reason == 'passwordprotectedlesson') {
 | 
				
			||||||
                if (!ignorePassword) {
 | 
					                if (!ignorePassword) {
 | 
				
			||||||
                    // Treat password before all other reasons.
 | 
					                    // Treat password before all other reasons.
 | 
				
			||||||
 | 
				
			|||||||
@ -313,7 +313,7 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
 | 
				
			|||||||
            this.showNextButton = false;
 | 
					            this.showNextButton = false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const currentIndex = await this.slides!.getActiveIndex();
 | 
					        const currentIndex = await this.slides?.getActiveIndex();
 | 
				
			||||||
        if (this.shouldSlideToInitial && currentIndex != this.selectedIndex) {
 | 
					        if (this.shouldSlideToInitial && currentIndex != this.selectedIndex) {
 | 
				
			||||||
            // Current tab has changed, don't slide to initial anymore.
 | 
					            // Current tab has changed, don't slide to initial anymore.
 | 
				
			||||||
            this.shouldSlideToInitial = false;
 | 
					            this.shouldSlideToInitial = false;
 | 
				
			||||||
@ -331,6 +331,11 @@ export class CoreTabsBaseComponent<T extends CoreTabBase> implements OnInit, Aft
 | 
				
			|||||||
        this.slideChanged();
 | 
					        this.slideChanged();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.calculateTabBarHeight();
 | 
					        this.calculateTabBarHeight();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // @todo: This call to update() can trigger JS errors in the console if tabs are re-loaded and there's only 1 tab.
 | 
				
			||||||
 | 
					        // For some reason, swiper.slides is undefined inside the Slides class, and the swiper is marked as destroyed.
 | 
				
			||||||
 | 
					        // Changing *ngIf="hideUntil" to [hidden] doesn't solve the issue, and it causes another error to be raised.
 | 
				
			||||||
 | 
					        // This can be tested in lesson as a student, play a lesson and go back to the entry page.
 | 
				
			||||||
        await this.slides!.update();
 | 
					        await this.slides!.update();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
 | 
					        if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
 | 
				
			|||||||
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
 | 
					import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
 | 
				
			||||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
 | 
					import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
 | 
				
			||||||
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
 | 
					import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
 | 
				
			||||||
 | 
					import { CoreTimerComponent } from './timer/timer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { CoreDirectivesModule } from '@directives/directives.module';
 | 
					import { CoreDirectivesModule } from '@directives/directives.module';
 | 
				
			||||||
import { CorePipesModule } from '@pipes/pipes.module';
 | 
					import { CorePipesModule } from '@pipes/pipes.module';
 | 
				
			||||||
@ -74,6 +75,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
 | 
				
			|||||||
        CoreUserAvatarComponent,
 | 
					        CoreUserAvatarComponent,
 | 
				
			||||||
        CoreDynamicComponent,
 | 
					        CoreDynamicComponent,
 | 
				
			||||||
        CoreSendMessageFormComponent,
 | 
					        CoreSendMessageFormComponent,
 | 
				
			||||||
 | 
					        CoreTimerComponent,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    imports: [
 | 
					    imports: [
 | 
				
			||||||
        CommonModule,
 | 
					        CommonModule,
 | 
				
			||||||
@ -109,6 +111,7 @@ import { CorePipesModule } from '@pipes/pipes.module';
 | 
				
			|||||||
        CoreUserAvatarComponent,
 | 
					        CoreUserAvatarComponent,
 | 
				
			||||||
        CoreDynamicComponent,
 | 
					        CoreDynamicComponent,
 | 
				
			||||||
        CoreSendMessageFormComponent,
 | 
					        CoreSendMessageFormComponent,
 | 
				
			||||||
 | 
					        CoreTimerComponent,
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class CoreComponentsModule {}
 | 
					export class CoreComponentsModule {}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										11
									
								
								src/core/components/timer/core-timer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/core/components/timer/core-timer.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<ion-item lines="none" class="core-timer" role="timer"
 | 
				
			||||||
 | 
					    [ngClass]="{'ion-text-center': align == 'center', 'ion-text-end': align == 'right'}">
 | 
				
			||||||
 | 
					    <ion-icon name="fas-clock" slot="start" role="presentation"></ion-icon>
 | 
				
			||||||
 | 
					    <ion-label>
 | 
				
			||||||
 | 
					        <span *ngIf="timeLeft && timeLeft > 0 && timerText" class="core-timer-text">{{ timerText }}</span>
 | 
				
			||||||
 | 
					        <span *ngIf="timeLeft && timeLeft > 0" class="core-timer-time-left">{{ timeLeft | coreSecondsToHMS }}</span>
 | 
				
			||||||
 | 
					        <span class="core-timesup" *ngIf="timeLeft !== undefined && timeLeft <= 0">
 | 
				
			||||||
 | 
					            {{ 'core.timesup' | translate }}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					    </ion-label>
 | 
				
			||||||
 | 
					</ion-item>
 | 
				
			||||||
							
								
								
									
										29
									
								
								src/core/components/timer/timer.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/core/components/timer/timer.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
				
			|||||||
 | 
					$core-timer-warn-color: #cb3d4d !default;
 | 
				
			||||||
 | 
					$core-timer-iterations: 15 !default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					:host {
 | 
				
			||||||
 | 
					    .core-timer {
 | 
				
			||||||
 | 
					        --background: transparent !important;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .core-timer-time-left, .core-timesup {
 | 
				
			||||||
 | 
					            font-weight: bold;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        span {
 | 
				
			||||||
 | 
					            margin-right: 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Create the timer warning colors.
 | 
				
			||||||
 | 
					        @for $i from 0 through $core-timer-iterations {
 | 
				
			||||||
 | 
					            &.core-timer-timeleft-#{$i} {
 | 
				
			||||||
 | 
					                background-color: rgba($core-timer-warn-color, 1 - ($i / $core-timer-iterations)) !important;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                @if $i <= $core-timer-iterations / 2 {
 | 
				
			||||||
 | 
					                    label, span, ion-icon {
 | 
				
			||||||
 | 
					                        color: var(--white);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								src/core/components/timer/timer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/core/components/timer/timer.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ElementRef } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { CoreTimeUtils } from '@services/utils/time';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * This directive shows a timer in format HH:MM:SS. When the countdown reaches 0, a function is called.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * Usage:
 | 
				
			||||||
 | 
					 * <core-timer [endTime]="endTime" (finished)="timeUp()" [timerText]="'addon.mod_quiz.timeleft' | translate"></core-timer>
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					    selector: 'core-timer',
 | 
				
			||||||
 | 
					    templateUrl: 'core-timer.html',
 | 
				
			||||||
 | 
					    styleUrls: ['timer.scss'],
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class CoreTimerComponent implements OnInit, OnDestroy {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Input() endTime?: string | number; // Timestamp (in seconds) when the timer should end.
 | 
				
			||||||
 | 
					    @Input() timerText?: string; // Text to show next to the timer. If not defined, no text shown.
 | 
				
			||||||
 | 
					    @Input() timeLeftClass?: string; // Name of the class to apply with each second. By default, 'core-timer-timeleft-'.
 | 
				
			||||||
 | 
					    @Input() align?: string; // Where to align the time and text. Defaults to 'left'. Other values: 'center', 'right'.
 | 
				
			||||||
 | 
					    @Output() finished = new EventEmitter<void>(); // Will emit an event when the timer reaches 0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    timeLeft?: number; // Seconds left to end.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected timeInterval?: number;
 | 
				
			||||||
 | 
					    protected element?: HTMLElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(
 | 
				
			||||||
 | 
					        protected elementRef: ElementRef,
 | 
				
			||||||
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Component being initialized.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ngOnInit(): void {
 | 
				
			||||||
 | 
					        const timeLeftClass = this.timeLeftClass || 'core-timer-timeleft-';
 | 
				
			||||||
 | 
					        const endTime = Math.round(Number(this.endTime));
 | 
				
			||||||
 | 
					        const container: HTMLElement | undefined = this.elementRef.nativeElement.querySelector('.core-timer');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!endTime) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check time left every 200ms.
 | 
				
			||||||
 | 
					        this.timeInterval = window.setInterval(() => {
 | 
				
			||||||
 | 
					            this.timeLeft = endTime - CoreTimeUtils.instance.timestamp();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (this.timeLeft < 0) {
 | 
				
			||||||
 | 
					                // Time is up! Stop the timer and call the finish function.
 | 
				
			||||||
 | 
					                clearInterval(this.timeInterval);
 | 
				
			||||||
 | 
					                this.finished.emit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If the time has nearly expired, change the color.
 | 
				
			||||||
 | 
					            if (this.timeLeft < 100 && container && !container.classList.contains(timeLeftClass + this.timeLeft)) {
 | 
				
			||||||
 | 
					                // Time left has changed. Remove previous classes and add the new one.
 | 
				
			||||||
 | 
					                container.classList.remove(timeLeftClass + (this.timeLeft + 1));
 | 
				
			||||||
 | 
					                container.classList.remove(timeLeftClass + (this.timeLeft + 2));
 | 
				
			||||||
 | 
					                container.classList.add(timeLeftClass + this.timeLeft);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 200);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Component destroyed.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    ngOnDestroy(): void {
 | 
				
			||||||
 | 
					        clearInterval(this.timeInterval);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -21,11 +21,11 @@
 | 
				
			|||||||
            <ion-radio-group formControlName="field">
 | 
					            <ion-radio-group formControlName="field">
 | 
				
			||||||
                <ion-item>
 | 
					                <ion-item>
 | 
				
			||||||
                    <ion-label>{{ 'core.login.username' | translate }}</ion-label>
 | 
					                    <ion-label>{{ 'core.login.username' | translate }}</ion-label>
 | 
				
			||||||
                    <ion-radio slot="start" value="username"></ion-radio>
 | 
					                    <ion-radio slot="end" value="username"></ion-radio>
 | 
				
			||||||
                </ion-item>
 | 
					                </ion-item>
 | 
				
			||||||
                <ion-item>
 | 
					                <ion-item>
 | 
				
			||||||
                    <ion-label>{{ 'core.user.email' | translate }}</ion-label>
 | 
					                    <ion-label>{{ 'core.user.email' | translate }}</ion-label>
 | 
				
			||||||
                    <ion-radio slot="start" value="email"></ion-radio>
 | 
					                    <ion-radio slot="end" value="email"></ion-radio>
 | 
				
			||||||
                </ion-item>
 | 
					                </ion-item>
 | 
				
			||||||
            </ion-radio-group>
 | 
					            </ion-radio-group>
 | 
				
			||||||
            <ion-item>
 | 
					            <ion-item>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										43
									
								
								src/core/guards/can-leave.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/core/guards/can-leave.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					// (C) Copyright 2015 Moodle Pty Ltd.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Licensed under the Apache License, Version 2.0 (the "License");
 | 
				
			||||||
 | 
					// you may not use this file except in compliance with the License.
 | 
				
			||||||
 | 
					// You may obtain a copy of the License at
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//     http://www.apache.org/licenses/LICENSE-2.0
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Unless required by applicable law or agreed to in writing, software
 | 
				
			||||||
 | 
					// distributed under the License is distributed on an "AS IS" BASIS,
 | 
				
			||||||
 | 
					// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
				
			||||||
 | 
					// See the License for the specific language governing permissions and
 | 
				
			||||||
 | 
					// limitations under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Injectable } from '@angular/core';
 | 
				
			||||||
 | 
					import { CanDeactivate } from '@angular/router';
 | 
				
			||||||
 | 
					import { CoreUtils } from '@services/utils/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Injectable({ providedIn: 'root' })
 | 
				
			||||||
 | 
					export class CanLeaveGuard implements CanDeactivate<unknown> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async canDeactivate(component: unknown | null): Promise<boolean> {
 | 
				
			||||||
 | 
					        if (!this.isCanLeave(component)) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return CoreUtils.instance.ignoreErrors(component.canLeave(), false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isCanLeave(component: unknown | null): component is CanLeave {
 | 
				
			||||||
 | 
					        return component !== null && 'canLeave' in <CanLeave> component;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface CanLeave {
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Check whether the user can leave the current route.
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @return Promise resolved with true if can leave, resolved with false or rejected if cannot leave.
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    canLeave: () => Promise<boolean>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,3 +1,5 @@
 | 
				
			|||||||
 | 
					@import "./globals.mixins.ionic.scss";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common styles.
 | 
					// Common styles.
 | 
				
			||||||
.text-left           { text-align: left; }
 | 
					.text-left           { text-align: left; }
 | 
				
			||||||
.text-right          { text-align: right; }
 | 
					.text-right          { text-align: right; }
 | 
				
			||||||
@ -139,6 +141,25 @@ ion-toolbar {
 | 
				
			|||||||
    z-index: 100000 !important;
 | 
					    z-index: 100000 !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (min-height: 400px) and (min-width: 300px) {
 | 
				
			||||||
 | 
					    .core-modal-lateral {
 | 
				
			||||||
 | 
					        // @todo @include core-split-area-end();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .modal-wrapper {
 | 
				
			||||||
 | 
					            position: absolute;
 | 
				
			||||||
 | 
					            @include position(0 !important, 0 !important, 0 !important, auto);
 | 
				
			||||||
 | 
					            display: block;
 | 
				
			||||||
 | 
					            height: 100% !important;
 | 
				
			||||||
 | 
					            width: auto;
 | 
				
			||||||
 | 
					            min-width: 300px;
 | 
				
			||||||
 | 
					            box-shadow: 0 28px 48px rgba(0, 0, 0, 0.4);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        ion-backdrop {
 | 
				
			||||||
 | 
					            visibility: visible;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Hidden submit button.
 | 
					// Hidden submit button.
 | 
				
			||||||
.core-submit-hidden-enter {
 | 
					.core-submit-hidden-enter {
 | 
				
			||||||
    position: absolute;
 | 
					    position: absolute;
 | 
				
			||||||
 | 
				
			|||||||
@ -185,6 +185,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ion-item-divider {
 | 
					    ion-item-divider {
 | 
				
			||||||
        --background: var(--gray-lighter);
 | 
					        --background: var(--gray-lighter);
 | 
				
			||||||
 | 
					        --color: inherit;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    --core-button-select-background: var(--custom-button-select-background, var(--ion-color-primary-contrast));
 | 
					    --core-button-select-background: var(--custom-button-select-background, var(--ion-color-primary-contrast));
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user