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" <ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
[title]="course.displayname || course.fullname" detail> [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> <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> <h2>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id"> <core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text> </core-format-text>
</h2> </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> </ion-label>
<ng-container *ngIf="!isEnrolled"> <ng-container *ngIf="!isEnrolled">
<ion-icon *ngFor="let icon of icons" color="dark" size="small" <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 { Component, Input, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular'; import { NavController } from '@ionic/angular';
import { CoreCourseHelper } from '@features/course/services/course.helper'; import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
import { CoreCourses, CoreCourseSearchedData } from '@features/courses/services/courses'; import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses.helper';
/** /**
* This directive is meant to display an item for a list of courses. * 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({ @Component({
selector: 'core-courses-course-list-item', selector: 'core-courses-course-list-item',
templateUrl: 'core-courses-course-list-item.html', templateUrl: 'core-courses-course-list-item.html',
styleUrls: ['course-list-item.scss'],
}) })
export class CoreCoursesCourseListItemComponent implements OnInit { 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[] = []; icons: CoreCoursesEnrolmentIcons[] = [];
isEnrolled = false; isEnrolled = false;
@ -44,9 +48,13 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
* Component being initialized. * Component being initialized.
*/ */
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
CoreCoursesHelper.instance.loadCourseColorAndImage(this.course);
// Check if the user is enrolled in the course. // Check if the user is enrolled in the course.
try { 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; this.isEnrolled = true;
} catch { } catch {
@ -87,11 +95,13 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
* @param course The course to open. * @param course The course to open.
*/ */
openCourse(): void { openCourse(): void {
if (this.isEnrolled) { /* if (this.isEnrolled) {
CoreCourseHelper.instance.openCourse(this.course); CoreCourseHelper.instance.openCourse(this.course);
} else { } else {
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } }); 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); height: calc(100% - 20px);
&[course-color="0"] .core-course-thumb { &[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 { &[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 { &[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 { &[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 { &[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 { &[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 { &[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 { &[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 { &[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 { &[course-color="9"] .core-course-thumb {
background: var(--core-course-image-background-9); background: var(--core-course-color-9);
} }
.core-course-thumb { .core-course-thumb {

View File

@ -11,11 +11,12 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content> <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher> </ion-refresher>
<core-loading [hideUntil]="dataLoaded"> <core-loading [hideUntil]="dataLoaded">
<div class="core-course-thumb-parallax">
<ion-list *ngIf="course">
<div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb"> <div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb">
<img [src]="courseImageUrl" core-external-content alt=""/> <img [src]="courseImageUrl" core-external-content alt=""/>
</div> </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-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-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
<ion-label> <ion-label>
@ -37,7 +38,11 @@
</ion-item> </ion-item>
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length"> <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 <ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
[userId]="contact.id" [userId]="contact.id"
[courseId]="isEnrolled ? course.id : null" [courseId]="isEnrolled ? course.id : null"
@ -115,6 +120,6 @@
<ion-icon name="fas-external-link-alt" slot="start"></ion-icon> <ion-icon name="fas-external-link-alt" slot="start"></ion-icon>
<ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label> <ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label>
</ion-item> </ion-item>
</ion-list> </div>
</core-loading> </core-loading>
</ion-content> </ion-content>

View File

@ -1,20 +1,39 @@
:host { :host {
.core-course-thumb { --scroll-factor: 0.5;
height: 150px; --translate-z: calc(-2 * var(--scroll-factor))px;
width: 100%; --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; overflow: hidden;
}
.core-course-thumb {
overflow: hidden;
text-align: center;
cursor: pointer; cursor: pointer;
pointer-events: auto; pointer-events: auto;
position: relative; transform-origin: center top;
img { /**
position: absolute; * Calculated with scroll-factor: 0.5;
top: 0; * translate-z: -2 * $scroll-factor px;
bottom: 0; * scale: 1 + $scroll-factor * 2;
margin: auto; */
width: 100%; transform: translateZ(-1px) scale(2);
}
} }
.core-customfieldvalue core-format-text { .core-customfieldvalue core-format-text {
display: inline; display: inline;
} }

View File

@ -18,6 +18,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses'; import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
import { CoreWSExternalFile } from '@services/ws';
// import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion'; // import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
// import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover'; // import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
@ -51,20 +52,17 @@ export class CoreCoursesHelperProvider {
course: CoreEnrolledCourseDataWithExtraInfo, course: CoreEnrolledCourseDataWithExtraInfo,
courseByField: CoreCourseSearchedData, courseByField: CoreCourseSearchedData,
addCategoryName: boolean = false, addCategoryName: boolean = false,
colors?: (string | undefined)[],
): void { ): void {
if (courseByField) { if (courseByField) {
course.displayname = courseByField.displayname; course.displayname = courseByField.displayname;
course.categoryname = addCategoryName ? courseByField.categoryname : undefined; course.categoryname = addCategoryName ? courseByField.categoryname : undefined;
course.overviewfiles = course.overviewfiles || courseByField.overviewfiles;
if (courseByField.overviewfiles && courseByField.overviewfiles[0]) {
course.courseImage = courseByField.overviewfiles[0].fileurl;
} else {
delete course.courseImage;
}
} else { } else {
delete course.displayname; delete course.displayname;
delete course.courseImage;
} }
this.loadCourseColorAndImage(course, colors);
} }
/** /**
@ -84,21 +82,14 @@ export class CoreCoursesHelperProvider {
let coursesInfo = {}; let coursesInfo = {};
let courseInfoAvailable = false; let courseInfoAvailable = false;
const site = CoreSites.instance.getCurrentSite();
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
const colors: (string | undefined)[] = []; let colors: (string | undefined)[] = [];
if (site?.isVersionGreaterEqualThan('3.8')) { promises.push(this.loadCourseSiteColors().then((loadedColors) => {
promises.push(site.getConfig().then((configs) => { colors = loadedColors;
for (let x = 0; x < 10; x++) {
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
}
return; return;
}).catch(() => {
// Ignore errors.
})); }));
}
if (CoreCourses.instance.isGetCoursesByFieldAvailable() && (loadCategoryNames || if (CoreCourses.instance.isGetCoursesByFieldAvailable() && (loadCategoryNames ||
(typeof courses[0].overviewfiles == 'undefined' && typeof courses[0].displayname == 'undefined'))) { (typeof courses[0].overviewfiles == 'undefined' && typeof courses[0].displayname == 'undefined'))) {
@ -117,13 +108,50 @@ export class CoreCoursesHelperProvider {
await Promise.all(promises); await Promise.all(promises);
courses.forEach((course) => { courses.forEach((course) => {
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames); this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames, colors);
});
}
if (!course.courseImage) { /**
* 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.colorNumber = course.id % 10;
course.color = colors.length ? colors[course.colorNumber] : undefined; course.color = colors.length ? colors[course.colorNumber] : undefined;
} }
});
} }
/** /**
@ -157,12 +185,20 @@ export class CoreCoursesHelperProvider {
export class CoreCoursesHelper extends makeSingleton(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. colorNumber?: number; // Color index number.
color?: string; // Color RGB. color?: string; // Color RGB.
courseImage?: string; // Course thumbnail. courseImage?: string; // Course thumbnail.
};
/**
* Enrolled course data with extra rendering info.
*/
export type CoreEnrolledCourseDataWithExtraInfo = CoreCourseWithImageAndColor & CoreEnrolledCourseData & {
categoryname?: string; // Category name, categoryname?: string; // Category name,
}; };

View File

@ -154,16 +154,16 @@
--core-login-background: var(--custom-login-background, var(--white)); --core-login-background: var(--custom-login-background, var(--white));
--core-login-text-color: var(--custom-login-text-color, var(--black)); --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-color-0: var(--custom-course-color-0, #81ecec);
--core-course-image-background-1: var(--custom-course-image-background-1, #74b9ff); --core-course-color-1: var(--custom-course-color-1, #74b9ff);
--core-course-image-background-2: var(--custom-course-image-background-2, #a29bfe); --core-course-color-2: var(--custom-course-color-2, #a29bfe);
--core-course-image-background-3: var(--custom-course-image-background-3, #dfe6e9); --core-course-color-3: var(--custom-course-color-3, #dfe6e9);
--core-course-image-background-4: var(--custom-course-image-background-4, #00b894); --core-course-color-4: var(--custom-course-color-4, #00b894);
--core-course-image-background-5: var(--custom-course-image-background-5, #0984e3); --core-course-color-5: var(--custom-course-color-5, #0984e3);
--core-course-image-background-6: var(--custom-course-image-background-6, #b2bec3); --core-course-color-6: var(--custom-course-color-6, #b2bec3);
--core-course-image-background-7: var(--custom-course-image-background-7, #fdcb6e); --core-course-color-7: var(--custom-course-color-7, #fdcb6e);
--core-course-image-background-8: var(--custom-course-image-background-9, #fd79a8); --core-course-color-8: var(--custom-course-color-9, #fd79a8);
--core-course-image-background-9: var(--custom-course-image-background-90, #6c5ce7); --core-course-color-9: var(--custom-course-color-90, #6c5ce7);
--core-star-color: var(--custom-star-color, var(--core-color)); --core-star-color: var(--custom-star-color, var(--core-color));
} }