MOBILE-3594 course: Improve course listing and add course image parallax
parent
c3e59edf18
commit
add521a0e7
|
@ -1,11 +1,31 @@
|
|||
<ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
|
||||
[title]="course.displayname || course.fullname" detail>
|
||||
<ion-icon name="fas-graduation-cap" slot="start"></ion-icon>
|
||||
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon"
|
||||
[attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color"></ion-icon>
|
||||
<ion-avatar *ngIf="course.courseImage" slot="start">
|
||||
<img [src]="course.courseImage" core-external-content alt=""/>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||
class="core-course-additional-info">
|
||||
<span *ngIf="course.categoryname" class="core-course-category">
|
||||
<core-format-text [text]="course.categoryname"></core-format-text>
|
||||
</span>
|
||||
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-category"> | </span>
|
||||
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-shortname">
|
||||
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
</p>
|
||||
<h2>
|
||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="isEnrolled && course.progress != null && course.progress! >= 0 && course.completionusertracked !== false">
|
||||
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="!isEnrolled">
|
||||
<ion-icon *ngFor="let icon of icons" color="dark" size="small"
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
:host {
|
||||
.course-icon {
|
||||
color: white;
|
||||
background: var(--gray-light);
|
||||
padding: 8px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 16px;
|
||||
-webkit-transition: all 50ms ease-in-out;
|
||||
transition: all 50ms ease-in-out;
|
||||
}
|
||||
|
||||
ion-icon[course-color="0"] {
|
||||
color: var(--core-course-color-0);
|
||||
}
|
||||
ion-icon[course-color="1"] {
|
||||
color: var(--core-course-color-1);
|
||||
}
|
||||
ion-icon[course-color="2"] {
|
||||
color: var(--core-course-color-2);
|
||||
}
|
||||
ion-icon[course-color="3"] {
|
||||
color: var(--core-course-color-3);
|
||||
}
|
||||
ion-icon[course-color="4"] {
|
||||
color: var(--core-course-color-4);
|
||||
}
|
||||
ion-icon[course-color="5"] {
|
||||
color: var(--core-course-color-5);
|
||||
}
|
||||
ion-icon[course-color="6"] {
|
||||
color: var(--core-course-color-6);
|
||||
}
|
||||
ion-icon[course-color="7"] {
|
||||
color: var(--core-course-color-7);
|
||||
}
|
||||
ion-icon[course-color="8"] {
|
||||
color: var(--core-course-color-8);
|
||||
}
|
||||
ion-icon[course-color="9"] {
|
||||
color: var(--core-course-color-9);
|
||||
}
|
||||
|
||||
ion-avatar {
|
||||
-webkit-transition: all 50ms ease-in-out;
|
||||
transition: all 50ms ease-in-out;
|
||||
}
|
||||
}
|
|
@ -14,8 +14,8 @@
|
|||
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { CoreCourseHelper } from '@features/course/services/course.helper';
|
||||
import { CoreCourses, CoreCourseSearchedData } from '@features/courses/services/courses';
|
||||
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses.helper';
|
||||
|
||||
/**
|
||||
* This directive is meant to display an item for a list of courses.
|
||||
|
@ -27,10 +27,14 @@ import { CoreCourses, CoreCourseSearchedData } from '@features/courses/services/
|
|||
@Component({
|
||||
selector: 'core-courses-course-list-item',
|
||||
templateUrl: 'core-courses-course-list-item.html',
|
||||
styleUrls: ['course-list-item.scss'],
|
||||
})
|
||||
export class CoreCoursesCourseListItemComponent implements OnInit {
|
||||
|
||||
@Input() course!: CoreCourseSearchedData; // The course to render.
|
||||
@Input() course!: CoreCourseSearchedData & CoreCourseWithImageAndColor & {
|
||||
completionusertracked?: boolean; // If the user is completion tracked.
|
||||
progress?: number; // Progress percentage.
|
||||
}; // The course to render.
|
||||
|
||||
icons: CoreCoursesEnrolmentIcons[] = [];
|
||||
isEnrolled = false;
|
||||
|
@ -44,9 +48,13 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
|
|||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
CoreCoursesHelper.instance.loadCourseColorAndImage(this.course);
|
||||
|
||||
// Check if the user is enrolled in the course.
|
||||
try {
|
||||
await CoreCourses.instance.getUserCourse(this.course.id);
|
||||
const course = await CoreCourses.instance.getUserCourse(this.course.id);
|
||||
this.course.progress = course.progress;
|
||||
this.course.completionusertracked = course.completionusertracked;
|
||||
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
|
@ -87,11 +95,13 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
|
|||
* @param course The course to open.
|
||||
*/
|
||||
openCourse(): void {
|
||||
if (this.isEnrolled) {
|
||||
/* if (this.isEnrolled) {
|
||||
CoreCourseHelper.instance.openCourse(this.course);
|
||||
} else {
|
||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
||||
}
|
||||
} */
|
||||
// @todo while opencourse function is not completed, open preview page.
|
||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,34 +6,34 @@
|
|||
height: calc(100% - 20px);
|
||||
|
||||
&[course-color="0"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-0);
|
||||
background: var(--core-course-color-0);
|
||||
}
|
||||
&[course-color="1"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-1);
|
||||
background: var(--core-course-color-1);
|
||||
}
|
||||
&[course-color="2"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-2);
|
||||
background: var(--core-course-color-2);
|
||||
}
|
||||
&[course-color="3"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-3);
|
||||
background: var(--core-course-color-3);
|
||||
}
|
||||
&[course-color="4"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-4);
|
||||
background: var(--core-course-color-4);
|
||||
}
|
||||
&[course-color="5"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-5);
|
||||
background: var(--core-course-color-5);
|
||||
}
|
||||
&[course-color="6"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-6);
|
||||
background: var(--core-course-color-6);
|
||||
}
|
||||
&[course-color="7"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-7);
|
||||
background: var(--core-course-color-7);
|
||||
}
|
||||
&[course-color="8"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-8);
|
||||
background: var(--core-course-color-8);
|
||||
}
|
||||
&[course-color="9"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-9);
|
||||
background: var(--core-course-color-9);
|
||||
}
|
||||
|
||||
.core-course-thumb {
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
|
||||
<ion-list *ngIf="course">
|
||||
<div class="core-course-thumb-parallax">
|
||||
<div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb">
|
||||
<img [src]="courseImageUrl" core-external-content alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="core-course-thumb-parallax-content">
|
||||
<ion-item class="ion-text-wrap" (click)="openCourse()" [title]="course.fullname" [attr.details]="!avoidOpenCourse && canAccessCourse">
|
||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
|
@ -37,7 +38,11 @@
|
|||
</ion-item>
|
||||
|
||||
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length">
|
||||
<ion-item-divider>{{ 'core.teachers' | translate }}</ion-item-divider>
|
||||
<ion-item-divider>
|
||||
<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"
|
||||
[courseId]="isEnrolled ? course.id : null"
|
||||
|
@ -115,6 +120,6 @@
|
|||
<ion-icon name="fas-external-link-alt" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
||||
|
|
|
@ -1,20 +1,39 @@
|
|||
:host {
|
||||
.core-course-thumb {
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
--scroll-factor: 0.5;
|
||||
--translate-z: calc(-2 * var(--scroll-factor))px;
|
||||
--scale: calc(1 + var(--scroll-factor) * 2);
|
||||
|
||||
perspective: 1px;
|
||||
perspective-origin: center top;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
.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;
|
||||
position: relative;
|
||||
transform-origin: center top;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
}
|
||||
/**
|
||||
* Calculated with scroll-factor: 0.5;
|
||||
* translate-z: -2 * $scroll-factor px;
|
||||
* scale: 1 + $scroll-factor * 2;
|
||||
*/
|
||||
transform: translateZ(-1px) scale(2);
|
||||
}
|
||||
|
||||
|
||||
.core-customfieldvalue core-format-text {
|
||||
display: inline;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { CoreUtils } from '@services/utils/utils';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
// import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
|
||||
// import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
|
||||
|
||||
|
@ -51,20 +52,17 @@ export class CoreCoursesHelperProvider {
|
|||
course: CoreEnrolledCourseDataWithExtraInfo,
|
||||
courseByField: CoreCourseSearchedData,
|
||||
addCategoryName: boolean = false,
|
||||
colors?: (string | undefined)[],
|
||||
): void {
|
||||
if (courseByField) {
|
||||
course.displayname = courseByField.displayname;
|
||||
course.categoryname = addCategoryName ? courseByField.categoryname : undefined;
|
||||
|
||||
if (courseByField.overviewfiles && courseByField.overviewfiles[0]) {
|
||||
course.courseImage = courseByField.overviewfiles[0].fileurl;
|
||||
} else {
|
||||
delete course.courseImage;
|
||||
}
|
||||
course.overviewfiles = course.overviewfiles || courseByField.overviewfiles;
|
||||
} else {
|
||||
delete course.displayname;
|
||||
delete course.courseImage;
|
||||
}
|
||||
|
||||
this.loadCourseColorAndImage(course, colors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,21 +82,14 @@ export class CoreCoursesHelperProvider {
|
|||
let coursesInfo = {};
|
||||
let courseInfoAvailable = false;
|
||||
|
||||
const site = CoreSites.instance.getCurrentSite();
|
||||
const promises: Promise<void>[] = [];
|
||||
const colors: (string | undefined)[] = [];
|
||||
let colors: (string | undefined)[] = [];
|
||||
|
||||
if (site?.isVersionGreaterEqualThan('3.8')) {
|
||||
promises.push(site.getConfig().then((configs) => {
|
||||
for (let x = 0; x < 10; x++) {
|
||||
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
||||
}
|
||||
promises.push(this.loadCourseSiteColors().then((loadedColors) => {
|
||||
colors = loadedColors;
|
||||
|
||||
return;
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}));
|
||||
|
||||
if (CoreCourses.instance.isGetCoursesByFieldAvailable() && (loadCategoryNames ||
|
||||
(typeof courses[0].overviewfiles == 'undefined' && typeof courses[0].displayname == 'undefined'))) {
|
||||
|
@ -117,15 +108,52 @@ export class CoreCoursesHelperProvider {
|
|||
await Promise.all(promises);
|
||||
|
||||
courses.forEach((course) => {
|
||||
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames);
|
||||
|
||||
if (!course.courseImage) {
|
||||
course.colorNumber = course.id % 10;
|
||||
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
||||
}
|
||||
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames, colors);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load course colors from site config.
|
||||
*
|
||||
* @return course colors RGB.
|
||||
*/
|
||||
protected async loadCourseSiteColors(): Promise<(string | undefined)[]> {
|
||||
const site = CoreSites.instance.getCurrentSite();
|
||||
const colors: (string | undefined)[] = [];
|
||||
|
||||
if (site?.isVersionGreaterEqualThan('3.8')) {
|
||||
try {
|
||||
const configs = await site.getConfig();
|
||||
for (let x = 0; x < 10; x++) {
|
||||
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the color of the course or the thumb image.
|
||||
*
|
||||
* @param course Course data.
|
||||
* @param colors Colors loaded.
|
||||
*/
|
||||
async loadCourseColorAndImage(course: CoreCourseWithImageAndColor, colors?: (string | undefined)[]): Promise<void> {
|
||||
if (!colors) {
|
||||
colors = await this.loadCourseSiteColors();
|
||||
}
|
||||
|
||||
if (course.overviewfiles && course.overviewfiles[0]) {
|
||||
course.courseImage = course.overviewfiles[0].fileurl;
|
||||
} else {
|
||||
course.colorNumber = course.id % 10;
|
||||
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user courses with admin and nav options.
|
||||
*
|
||||
|
@ -157,12 +185,20 @@ export class CoreCoursesHelperProvider {
|
|||
export class CoreCoursesHelper extends makeSingleton(CoreCoursesHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Enrolled course data with extra rendering info.
|
||||
* Course with colors info and course image.
|
||||
*/
|
||||
export type CoreEnrolledCourseDataWithExtraInfo = CoreEnrolledCourseData & {
|
||||
export type CoreCourseWithImageAndColor = {
|
||||
id: number; // Course id.
|
||||
overviewfiles?: CoreWSExternalFile[];
|
||||
colorNumber?: number; // Color index number.
|
||||
color?: string; // Color RGB.
|
||||
courseImage?: string; // Course thumbnail.
|
||||
};
|
||||
|
||||
/**
|
||||
* Enrolled course data with extra rendering info.
|
||||
*/
|
||||
export type CoreEnrolledCourseDataWithExtraInfo = CoreCourseWithImageAndColor & CoreEnrolledCourseData & {
|
||||
categoryname?: string; // Category name,
|
||||
};
|
||||
|
||||
|
|
|
@ -154,16 +154,16 @@
|
|||
--core-login-background: var(--custom-login-background, var(--white));
|
||||
--core-login-text-color: var(--custom-login-text-color, var(--black));
|
||||
|
||||
--core-course-image-background-0: var(--custom-course-image-background-0, #81ecec);
|
||||
--core-course-image-background-1: var(--custom-course-image-background-1, #74b9ff);
|
||||
--core-course-image-background-2: var(--custom-course-image-background-2, #a29bfe);
|
||||
--core-course-image-background-3: var(--custom-course-image-background-3, #dfe6e9);
|
||||
--core-course-image-background-4: var(--custom-course-image-background-4, #00b894);
|
||||
--core-course-image-background-5: var(--custom-course-image-background-5, #0984e3);
|
||||
--core-course-image-background-6: var(--custom-course-image-background-6, #b2bec3);
|
||||
--core-course-image-background-7: var(--custom-course-image-background-7, #fdcb6e);
|
||||
--core-course-image-background-8: var(--custom-course-image-background-9, #fd79a8);
|
||||
--core-course-image-background-9: var(--custom-course-image-background-90, #6c5ce7);
|
||||
--core-course-color-0: var(--custom-course-color-0, #81ecec);
|
||||
--core-course-color-1: var(--custom-course-color-1, #74b9ff);
|
||||
--core-course-color-2: var(--custom-course-color-2, #a29bfe);
|
||||
--core-course-color-3: var(--custom-course-color-3, #dfe6e9);
|
||||
--core-course-color-4: var(--custom-course-color-4, #00b894);
|
||||
--core-course-color-5: var(--custom-course-color-5, #0984e3);
|
||||
--core-course-color-6: var(--custom-course-color-6, #b2bec3);
|
||||
--core-course-color-7: var(--custom-course-color-7, #fdcb6e);
|
||||
--core-course-color-8: var(--custom-course-color-9, #fd79a8);
|
||||
--core-course-color-9: var(--custom-course-color-90, #6c5ce7);
|
||||
--core-star-color: var(--custom-star-color, var(--core-color));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue