MOBILE-3594 course: Improve course listing and add course image parallax

main
Pau Ferrer Ocaña 2020-11-19 16:37:46 +01:00
parent c3e59edf18
commit add521a0e7
8 changed files with 207 additions and 69 deletions

View File

@ -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"

View File

@ -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;
}
}

View File

@ -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 } });
}
}

View File

@ -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 {

View File

@ -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>

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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));
}