MOBILE-3915 course: Improve course summary page

main
Pau Ferrer Ocaña 2022-01-24 17:46:36 +01:00
parent 901a445408
commit 6b46f48b3c
17 changed files with 233 additions and 225 deletions

View File

@ -2257,6 +2257,7 @@
"core.strftimetime24": "langconfig",
"core.submit": "moodle",
"core.success": "moodle",
"core.summary": "moodle",
"core.tablet": "local_moodlemobileapp",
"core.tag.defautltagcoll": "tag",
"core.tag.errorareanotsupported": "local_moodlemobileapp",

View File

@ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
}
rootAnimation.addAnimation(enteringContentAnimation);
enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating');
if (backDirection) {
enteringContentAnimation
@ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp
// setup leaving view
if (leavingEl) {
const leavingContent = createAnimation();
leavingContent.beforeAddClass('animating').afterRemoveClass('animating');
const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');

View File

@ -0,0 +1,3 @@
:host ::ng-deep .collapsible-title {
display: none;
}

View File

@ -31,6 +31,7 @@ import { CoreCourse } from '@features/course/services/course';
@Component({
selector: 'core-course-format-single-activity',
templateUrl: 'core-course-format-single-activity.html',
styleUrls: ['single-activity.scss'],
})
export class CoreCourseFormatSingleActivityComponent implements OnChanges {

View File

@ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
*/
openCourseSummary(): void {
CoreNavigator.navigateToSitePath(
'/course/' + this.course.id + '/preview',
{ params: { course: this.course } },
`/course/${this.course.id}/preview`,
{ params: { course: this.course, avoidOpenCourse: true } },
);
}

View File

@ -249,7 +249,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy {
}
CoreNavigator.navigateToSitePath(
'/course/' + this.course.id + '/preview',
`/course/${this.course.id}/preview`,
{ params: { course: this.course, avoidOpenCourse: true } },
);
}

View File

@ -5,7 +5,7 @@
</ion-buttons>
<ion-title>
<h1>
<core-format-text [text]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
{{'core.course.coursesummary' | translate}}
</h1>
</ion-title>
</ion-toolbar>
@ -16,44 +16,49 @@
</ion-refresher>
<core-loading [hideUntil]="dataLoaded">
<div *ngIf="courseImageUrl" class="core-course-thumb-parallax">
<div (click)="openCourse()" class="core-course-thumb">
<div class="core-course-thumb">
<img [src]="courseImageUrl" core-external-content alt="" />
</div>
</div>
<div class="core-course-thumb-parallax-content" *ngIf="course">
<ion-item class="ion-text-wrap" (click)="openCourse()" [attr.aria-label]="course.fullname" [detail]="canAccessCourse"
[button]="canAccessCourse">
<ion-icon name="fas-graduation-cap" fixed-width slot="start" aria-hidden="true"></ion-icon>
<ion-item class="ion-text-wrap">
<ion-label>
<h2>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
<p *ngIf="course.categoryname">
<core-format-text [text]="course.categoryname" contextLevel="coursecat" [contextInstanceId]="course.categoryid">
</core-format-text>
</p>
<h2>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
<p *ngIf="course.startdate">
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
<span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span>
</p>
<div class="core-course-progress" *ngIf="progress !== undefined">
<core-progress-bar [progress]="progress" a11yText="core.course.aria:sectionprogress">
</core-progress-bar>
</div>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
<ion-label>
<p class="item-heading">
{{'core.summary' | translate}}
</p>
<core-format-text [text]="course.summary" [maxHeight]="120" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</ion-label>
</ion-item>
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length">
<ion-item-divider>
<ion-list *ngIf="course.contacts && course.contacts.length">
<ion-item-divider class="ion-text-wrap">
<ion-label>
<h2>{{ 'core.teachers' | translate }}</h2>
</ion-label>
</ion-item-divider>
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
<ion-item button class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
[courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate" detail="true">
<core-user-avatar [user]="contact" slot="start" [userId]="contact.id" [courseId]="isEnrolled ? course.id : null">
</core-user-avatar>
@ -62,7 +67,7 @@
</ion-label>
</ion-item>
<core-spacer></core-spacer>
</ng-container>
</ion-list>
<ion-item class="ion-text-wrap" *ngIf="course.customfields">
<ion-label>
@ -83,7 +88,8 @@
</ion-label>
</ion-item>
<div *ngIf="!isEnrolled" detail="false">
<!-- Enrol -->
<ng-container *ngIf="!isEnrolled">
<ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances">
<ion-label>
<p class="item-heading">{{ instance.name }}</p>
@ -92,23 +98,24 @@
</ion-button>
</ion-label>
</ion-item>
</div>
<ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled">
<ion-label>
<h2>{{ 'core.courses.paypalaccepted' | translate }}</h2>
<p>{{ 'core.paymentinstant' | translate }}</p>
<ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile">
{{ 'core.courses.sendpaymentbutton' | translate }}
</ion-button>
</ion-label>
</ion-item>
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
<ion-label>
<p>{{ 'core.courses.notenrollable' | translate }}</p>
</ion-label>
</ion-item>
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" button>
<ion-item class="ion-text-wrap" *ngIf="paypalEnabled">
<ion-label>
<p class="item-heading">{{ 'core.courses.paypalaccepted' | translate }}</p>
<p *ngIf="isMobile">{{ 'core.paymentinstant' | translate }}</p>
<ion-button *ngIf="isMobile" expand="block" class="ion-margin-top" (click)="paypalEnrol()">
{{ 'core.courses.sendpaymentbutton' | translate }}
</ion-button>
</ion-label>
</ion-item>
<ion-item *ngIf="!selfEnrolInstances.length && !paypalEnabled">
<ion-label>
<p class="item-heading">{{ 'core.courses.notenrollable' | translate }}</p>
</ion-label>
</ion-item>
</ng-container>
<ion-button class="ion-margin" *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" expand="block"
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
<ion-icon *ngIf="(prefetchCourseData.status != statusDownloaded) && !prefetchCourseData.loading"
[name]="prefetchCourseData.icon" slot="start" aria-hidden="true">
</ion-icon>
@ -116,23 +123,24 @@
[name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status">
</ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-label>
<h2 *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</h2>
<h2 *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item button (click)="openCourse()" [attr.aria-label]="course.fullname" *ngIf="canAccessCourse" detail="true">
<ion-label *ngIf="prefetchCourseData.status != statusDownloaded">{{ 'core.course.downloadcourse' | translate }}</ion-label>
<ion-label *ngIf="prefetchCourseData.status == statusDownloaded">{{ 'core.course.refreshcourse' | translate }}</ion-label>
</ion-button>
<ion-button class="ion-margin" (click)="openCourse()" *ngIf="!avoidOpenCourse && canAccessCourse" expand="block">
<ion-icon name="fas-briefcase" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.course' | translate }}</h2>
{{ 'core.course' | translate }}
</ion-label>
</ion-item>
<ion-item [href]="courseUrl" core-link [attr.aria-label]="course.fullname" button detail="false" [showBrowserWarning]="false">
</ion-button>
<ion-button class="ion-margin" [href]="courseUrl" core-link [showBrowserWarning]="false" expand="block">
<ion-icon name="fas-external-link-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<h2>{{ 'core.openinbrowser' | translate }}</h2>
{{ 'core.openinbrowser' | translate }}
</ion-label>
</ion-item>
</ion-button>
</div>
</core-loading>
</ion-content>

View File

@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import {
CoreCourseCustomField,
CoreCourseEnrolmentMethod,
CoreCourseGetCoursesData,
CoreCourses,
CoreCourseSearchedData,
CoreCoursesProvider,
@ -34,6 +34,8 @@ import { Translate } from '@singletons';
import { CoreConstants } from '@/core/constants';
import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password';
import { CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper';
/**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
@ -45,12 +47,13 @@ import { CoreNavigator } from '@services/navigator';
})
export class CoreCoursePreviewPage implements OnInit, OnDestroy {
course?: CoreCourseSearchedData;
course?: CoreCourseSummaryData;
isEnrolled = false;
canAccessCourse = true;
selfEnrolInstances: CoreCourseEnrolmentMethod[] = [];
paypalEnabled = false;
dataLoaded = false;
avoidOpenCourse = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: '',
statusTranslatable: 'core.loading',
@ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
courseUrl = '';
courseImageUrl?: string;
isMobile: boolean;
progress?: number;
protected isGuestEnabled = false;
protected useGuestAccess = false;
@ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
protected paypalReturnUrl = '';
protected pageDestroyed = false;
protected courseStatusObserver?: CoreEventObserver;
protected courseId!: number;
constructor(
protected zone: NgZone,
@ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
if (this.downloadCourseEnabled) {
// Listen for status change in course.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status);
}
}, CoreSites.getCurrentSiteId());
@ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
/**
* View loaded.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
this.course = CoreNavigator.getRouteParam('course');
if (!this.course) {
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId');
} catch (error) {
CoreDomUtils.showErrorModal(error);
CoreNavigator.back();
return;
}
const currentSite = CoreSites.getCurrentSite();
const currentSiteUrl = currentSite && currentSite.getURL();
this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse');
this.course = CoreNavigator.getRouteParam('course');
this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1;
this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id);
this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id);
this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php');
if (this.course.overviewfiles.length > 0) {
this.courseImageUrl = this.course.overviewfiles[0].fileurl;
}
const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL();
this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId);
this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId);
this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php');
try {
await this.getCourse();
@ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
if (this.downloadCourseEnabled) {
// Determine course prefetch icon.
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id);
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
if (this.prefetchCourseData.loading) {
// Course is being downloaded. Get the download promise.
const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id);
const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId);
if (promise) {
// There is a download promise. If it fails, show an error.
promise.catch((error) => {
@ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
CoreCourse.setCoursePreviousStatus(this.course!.id);
CoreCourse.setCoursePreviousStatus(this.courseId);
}
}
}
@ -177,13 +180,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
this.selfEnrolInstances = [];
try {
this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id);
this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId);
this.enrolmentMethods.forEach((method) => {
if (method.type === 'self') {
this.selfEnrolInstances.push(method);
} else if (method.type === 'guest') {
this.isGuestEnabled = true;
} else if (method.type === 'paypal') {
this.paypalEnabled = true;
}
});
} catch (error) {
@ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
try {
let course: CoreEnrolledCourseData | CoreCourseGetCoursesData;
// Check if user is enrolled in the course.
try {
course = await CoreCourses.getUserCourse(this.course!.id);
this.course = await CoreCourses.getUserCourse(this.courseId);
this.isEnrolled = true;
} catch {
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
this.isEnrolled = false;
course = await CoreCourses.getCourse(this.course!.id);
this.course = await CoreCourses.getCourse(this.courseId);
}
// Success retrieving the course, we can assume the user has permissions to view it.
this.course!.fullname = course.fullname || this.course!.fullname;
this.course!.summary = course.summary || this.course!.summary;
this.canAccessCourse = true;
this.useGuestAccess = false;
} catch {
@ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
}
if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
try {
const course = await CoreCourses.getCourseByField('id', this.course!.id);
if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) {
this.courseImageUrl = this.course.overviewfiles[0].fileurl;
}
this.course!.customfields = course.customfields;
} catch {
// Ignore errors.
try {
const courseByField = await CoreCourses.getCourseByField('id', this.courseId);
if (this.course) {
this.course.customfields = courseByField.customfields;
this.course.contacts = courseByField.contacts;
this.course.displayname = courseByField.displayname;
this.course.categoryname = courseByField.categoryname;
this.course.overviewfiles = courseByField.overviewfiles;
} else {
this.course = courseByField;
}
this.paypalEnabled = !this.isEnrolled && courseByField.enrollmentmethods?.indexOf('paypal') > -1;
} catch {
// Ignore errors.
}
if (!this.course ||
!('progress' in this.course) ||
typeof this.course.progress !== 'number' ||
this.course.progress < 0 ||
this.course.completionusertracked === false
) {
this.progress = undefined;
} else {
this.progress = this.course.progress;
}
this.dataLoaded = true;
@ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
/**
* Open the course.
*
* @param replaceCurrentPage If current place should be replaced in the navigation stack.
*/
openCourse(): void {
if (!this.canAccessCourse) {
openCourse(replaceCurrentPage = false): void {
if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) {
return;
}
CoreCourseHelper.openCourse(this.course!, { isGuest: this.useGuestAccess });
CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage });
}
/**
@ -279,7 +304,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
};
// Open the enrolment page in InAppBrowser.
const window = await CoreSites.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl);
const window = await CoreSites.getRequiredCurrentSite().openInAppWithAutoLogin(this.enrolUrl);
// Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
const inAppLoadSubscription = window.on('loadstart').subscribe((event) => {
@ -319,7 +344,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
const modal = await CoreDomUtils.showModalLoading('core.loading', true);
try {
await CoreCourses.selfEnrol(this.course!.id, password, instanceId);
await CoreCourses.selfEnrol(this.courseId, password, instanceId);
// Close modal and refresh data.
this.isEnrolled = true;
@ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
await this.refreshData().finally(() => {
// My courses have been updated, trigger event.
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
courseId: this.course!.id,
courseId: this.courseId,
course: this.course,
action: CoreCoursesProvider.ACTION_ENROL,
}, CoreSites.getCurrentSiteId());
});
this.openCourse();
this.openCourse(true);
modal?.dismiss();
} catch (error) {
@ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCourse(this.course!.id));
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id));
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id));
if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) {
promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id));
}
promises.push(CoreCourses.invalidateCourse(this.courseId));
promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId));
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId));
promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId));
if (this.guestInstanceId) {
promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId));
}
@ -419,14 +442,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
// Check if user is enrolled in the course.
try {
CoreCourses.invalidateUserCourses();
} catch {
// Ignore errors.
}
await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses());
try {
await CoreCourses.getUserCourse(this.course!.id);
await CoreCourses.getUserCourse(this.courseId);
} catch {
// Not enrolled, wait a bit and try again.
if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) {
@ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
*/
async prefetchCourse(): Promise<void> {
try {
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, {
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, {
isGuest: this.useGuestAccess,
});
} catch (error) {
@ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy {
}
/**
* Page destroyed.
* @inheritdoc
*/
ngOnDestroy(): void {
this.pageDestroyed = true;
if (this.courseStatusObserver) {
this.courseStatusObserver.off();
}
this.courseStatusObserver?.off();
}
}
type CoreCourseSummaryData = CoreCourseWithImageAndColor & (CoreEnrolledCourseData | CoreCourseSearchedData) & {
contacts?: { // Contact users.
id: number; // Contact user id.
fullname: string; // Contact user fullname.
}[];
customfields?: CoreCourseCustomField[]; // Custom fields and associated values.
categoryname?: string; // Category name.
};

View File

@ -1,41 +1,43 @@
:host {
--scroll-factor: 0.5;
--translate-z: calc(-2 * var(--scroll-factor))px;
--scale: calc(1 + var(--scroll-factor) * 2);
ion-content:not(.animating) {
&::part(scroll) {
perspective: 1px;
perspective-origin: center top;
transform-style: preserve-3d;
}
perspective: 1px;
perspective-origin: center top;
transform-style: preserve-3d;
.core-course-thumb {
transform-origin: center top;
// @todo This parallax effect caused the image to be scaled during page transitions,
// and in some devices it seems like the problem persisted even after the transition.
// We should decide whether we want to keep this parallax or not, and if we do fix
// the problem or find an alternative implementation. For now, it's disabled.
--scroll-factor: 0.5;
--translate-z: calc(-2 * var(--scroll-factor))px;
--scale: calc(1 + var(--scroll-factor) * 2);
/**
* Calculated with scroll-factor: 0.5;
* translate-z: -2 * $scroll-factor px;
* scale: 1 + $scroll-factor * 2;
*/
transform: translateZ(-1px) scale(2);
}
}
.core-course-thumb-parallax-content {
transform: translateZ(0);
-webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
}
.core-course-thumb-parallax {
height: 40vw;
max-height: 35vh;
z-index: -1;
overflow: hidden;
}
// .core-course-thumb-parallax-content {
// transform: translateZ(0);
// -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
// filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
// }
// .core-course-thumb-parallax {
// height: 40vw;
// max-height: 35vh;
// z-index: -1;
// overflow: hidden;
// }
.core-course-thumb {
overflow: hidden;
text-align: center;
cursor: pointer;
pointer-events: auto;
transform-origin: center top;
/**
* Calculated with scroll-factor: 0.5;
* translate-z: -2 * $scroll-factor px;
* scale: 1 + $scroll-factor * 2;
*/
// transform: translateZ(-1px) scale(2);
}

View File

@ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
import { CoreNavigator } from '@services/navigator';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home';
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
@ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider {
modal?.dismiss();
return this.openCourse(course, params, siteId);
return this.openCourse(course, { params , siteId });
}
/**
@ -2020,20 +2020,25 @@ export class CoreCourseHelperProvider {
* they will see the result immediately.
*
* @param course Course to open
* @param params Params to pass to the course page.
* @param siteId Site ID. If not defined, current site.
* @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise<void> {
async openCourse(
course: CoreCourseAnyCourseData | { id: number },
navOptions?: CoreNavigationOptions & { siteId?: string },
): Promise<void> {
const siteId = navOptions?.siteId;
if (!siteId || siteId == CoreSites.getCurrentSiteId()) {
// Current site, we can open the course.
return CoreCourse.openCourse(course, params);
return CoreCourse.openCourse(course, navOptions);
} else {
// We need to load the site first.
params = params || {};
Object.assign(params, { course: course });
navOptions = navOptions || {};
await CoreNavigator.navigateToSitePath(`course/${course.id}`, { siteId, params });
navOptions.params = navOptions.params || {};
Object.assign(navOptions.params, { course: course });
await CoreNavigator.navigateToSitePath(`course/${course.id}`, navOptions);
}
}

View File

@ -43,7 +43,7 @@ import { CoreCourseLogCronHandler } from './handlers/log-cron';
import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins';
import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync';
import { CoreTagItem } from '@features/tag/services/tag';
import { CoreNavigator } from '@services/navigator';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreCourseModuleDelegate } from './module-delegate';
const ROOT_CACHE_KEY = 'mmCourse:';
@ -1177,10 +1177,13 @@ export class CoreCourseProvider {
* This function must be in here instead of course helper to prevent circular dependencies.
*
* @param course Course to open
* @param params Other params to pass to the course page.
* @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> {
async openCourse(
course: CoreCourseAnyCourseData | { id: number },
navOptions?: CoreNavigationOptions,
): Promise<void> {
const loading = await CoreDomUtils.showModalLoading();
// Wait for site plugins to be fetched.
@ -1197,7 +1200,7 @@ export class CoreCourseProvider {
if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) {
// No custom format plugin. We don't need to wait for anything.
loading.dismiss();
await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
await CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
return;
}
@ -1208,7 +1211,7 @@ export class CoreCourseProvider {
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
if (CoreSitePlugins.sitePluginsFinishedLoading) {
return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params);
return CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions);
}
// Wait for plugins to be loaded.
@ -1217,7 +1220,7 @@ export class CoreCourseProvider {
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
observer?.off();
CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, params)
CoreCourseFormatDelegate.openCourse(<CoreCourseAnyCourseData> course, navOptions)
.then(deferred.resolve).catch(deferred.reject);
});

View File

@ -13,10 +13,10 @@
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { Params } from '@angular/router';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreNavigationOptions } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { CoreCourseWSSection } from './course';
import { CoreCourseSection } from './course-helper';
@ -100,10 +100,10 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
* Your page should include the course handlers using CoreCoursesDelegate.
*
* @param course The course to open. It should contain a "format" attribute.
* @param params Params to pass to the course page.
* @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise<void>;
openCourse?(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void>;
/**
* Return the Component to use to display the course format instead of using the default one.
@ -323,11 +323,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate<CoreCourseForm
* Open a course. Should not be called directly. Call CoreCourseHelper.openCourse instead.
*
* @param course The course to open. It should contain a "format" attribute.
* @param params Params to pass to the course page.
* @param navOptions Navigation options that includes params to pass to the page.
* @return Promise resolved when done.
*/
async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> {
await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]);
async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> {
await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]);
}
/**

View File

@ -13,12 +13,9 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
import { CoreNavigator } from '@services/navigator';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourseWSSection } from '../course';
import { CoreCourseSection } from '../course-helper';
import { CoreCourseFormatHandler } from '../format-delegate';
@ -32,19 +29,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
format = 'default';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Get the title to use in course page.
*
* @param course The course.
* @return Title.
* @inheritdoc
*/
getCourseTitle(course: CoreCourseAnyCourseData): string {
if (course.displayname) {
@ -57,57 +49,35 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
}
/**
* Whether it allows seeing all sections at the same time. Defaults to true.
*
* @param course The course to check.
* @return Whether it can view all sections.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
canViewAllSections(course: CoreCourseAnyCourseData): boolean {
canViewAllSections(): boolean {
return true;
}
/**
* Whether the option blocks should be displayed. Defaults to true.
*
* @param course The course to check.
* @return Whether it can display blocks.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
displayBlocks(course: CoreCourseAnyCourseData): boolean {
displayBlocks(): boolean {
return true;
}
/**
* Whether the default section selector should be displayed. Defaults to true.
*
* @param course The course to check.
* @return Whether the default section selector should be displayed.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
displaySectionSelector(): boolean {
return true;
}
/**
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
* and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true.
*
* @param course The course to check.
* @param sections List of course sections.
* @return Whether the refresher should be displayed.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
displayRefresher(): boolean {
return true;
}
/**
* Given a list of sections, get the "current" section that should be displayed first.
*
* @param course The course to get the title.
* @param sections List of sections.
* @return Current section (or promise resolved with current section).
* @inheritdoc
*/
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
let marker: number | undefined;
@ -137,48 +107,33 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
}
/**
* Invalidate the data required to load the course format.
*
* @param course The course to get the title.
* @param sections List of sections.
* @return Promise resolved when the data is invalidated.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise<void> {
async invalidateData(course: CoreCourseAnyCourseData): Promise<void> {
await CoreCourses.invalidateCoursesByField('id', course.id);
}
/**
* Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened.
* Implement it only if you want to create your own page to display the course. In general it's better to use the method
* getCourseFormatComponent because it will display the course handlers at the top.
* Your page should include the course handlers using CoreCoursesDelegate.
*
* @param course The course to open. It should contain a "format" attribute.
* @param params Params to pass to the course page.
* @return Promise resolved when done.
* @inheritdoc
*/
async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise<void> {
params = params || {};
Object.assign(params, { course: course });
async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise<void> {
navOptions = navOptions || {};
navOptions.params = navOptions.params || {};
Object.assign(navOptions.params, { course: course });
// Don't return the .push promise, we don't want to display a loading modal during the page transition.
const currentTab = CoreNavigator.getCurrentMainMenuTab();
const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
const deepPath = '/deep'.repeat(routeDepth);
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params });
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions);
}
/**
* Whether the view should be refreshed when completion changes. If your course format doesn't display
* activity completion then you should return false.
*
* @param course The course.
* @return Whether course view should be refreshed when an activity completion changes.
* @inheritdoc
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise<boolean> {
async shouldRefreshWhenCompletionChanges(): Promise<boolean> {
return true;
}

View File

@ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
CoreCourseHelper.openCourse(this.course);
} else {
CoreNavigator.navigateToSitePath(
'/course/' + this.course.id + '/preview',
`/course/${this.course.id}/preview`,
{ params: { course: this.course } },
);
}

View File

@ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler
modal.dismiss();
// Now open the course.
CoreCourseHelper.openCourse(course, pageParams);
CoreCourseHelper.openCourse(course, { params: pageParams });
}
/**

View File

@ -301,6 +301,7 @@
"strftimetime24": "%H:%M",
"submit": "Submit",
"success": "Success",
"summary": "Summary",
"tablet": "Tablet",
"teachers": "Teachers",
"thereisdatatosync": "There are offline {{$a}} to be synchronised.",

View File

@ -15,6 +15,7 @@ information provided here is intended especially for developers.
The function CoreUserDelegate.getProfileHandlersFor must now receive a context + contextId instead of a courseId.
The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId.
Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action.
- CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page.
=== 3.9.5 ===