MOBILE-3806 course: Merge course progress and course list items components

main
Pau Ferrer Ocaña 2021-11-08 16:19:08 +01:00
parent 8925e5ccb4
commit 29f6d6cd39
11 changed files with 536 additions and 114 deletions

View File

@ -90,11 +90,11 @@
<div class="safe-area-padding"> <div class="safe-area-padding">
<ion-grid class="ion-no-padding"> <ion-grid class="ion-no-padding">
<ion-row class="ion-no-padding"> <ion-row class="ion-no-padding">
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" <ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6" size-lg="4"
size="12" size-sm="6" size-md="6" size-lg="4" size-xl="3"> size-xl="3">
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true" <core-courses-course-list-item [course]="course" class="core-courseoverview"
[showDownload]="downloadCourseEnabled && downloadEnabled"> [showDownload]="downloadCourseEnabled && downloadEnabled" [layout]="selectedLayout">
</core-courses-course-progress> </core-courses-course-list-item>
</ion-col> </ion-col>
</ion-row> </ion-row>
</ion-grid> </ion-grid>

View File

@ -8,13 +8,13 @@
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate"> (click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar"
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count"
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText"> [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
{{prefetchCoursesData.badge}} {{prefetchCoursesData.badge}}
</ion-badge> </ion-badge>
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading" <ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate">
[attr.aria-label]="'core.loading' | translate"></ion-spinner> </ion-spinner>
</div> </div>
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId"> <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
@ -25,16 +25,12 @@
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true" <core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
[message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box> [message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. --> <!-- List of courses. -->
<div <div [id]="scrollElementId" class="core-horizontal-scroll" (scroll)="scrollControls.updateScrollPosition()">
[id]="scrollElementId"
class="core-horizontal-scroll"
(scroll)="scrollControls.updateScrollPosition()"
>
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
<div class="safe-area-pseudo-padding-start"></div> <div class="safe-area-pseudo-padding-start"></div>
<ng-container *ngFor="let course of courses"> <ng-container *ngFor="let course of courses">
<core-courses-course-progress [course]="course" class="core-recentlyaccessedcourses" <core-courses-course-list-item [course]="course" class="core-recentlyaccessedcourses" layout="summarycard"
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-list-item>
</ng-container> </ng-container>
<div class="safe-area-pseudo-padding-end"></div> <div class="safe-area-pseudo-padding-end"></div>
</div> </div>

View File

@ -8,13 +8,13 @@
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate"> (click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
<ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon> <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button> </ion-button>
<ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge" role="progressbar"
role="progressbar" [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuemax]="prefetchCoursesData.total" [attr.aria-valuenow]="prefetchCoursesData.count"
[attr.aria-valuenow]="prefetchCoursesData.count" [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText"> [attr.aria-valuetext]="prefetchCoursesData.badgeA11yText">
{{prefetchCoursesData.badge}} {{prefetchCoursesData.badge}}
</ion-badge> </ion-badge>
<ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading" <ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading" [attr.aria-label]="'core.loading' | translate">
[attr.aria-label]="'core.loading' | translate"></ion-spinner> </ion-spinner>
</div> </div>
<core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId"> <core-horizontal-scroll-controls #scrollControls [aria-controls]="scrollElementId">
@ -25,17 +25,13 @@
<core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true" <core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" inline="true"
[message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box> [message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box>
<!-- List of courses. --> <!-- List of courses. -->
<div <div [hidden]="courses.length === 0" [id]="scrollElementId" class="core-horizontal-scroll"
[hidden]="courses.length === 0" (scroll)="scrollControls.updateScrollPosition()">
[id]="scrollElementId"
class="core-horizontal-scroll"
(scroll)="scrollControls.updateScrollPosition()"
>
<div (onResize)="scrollControls.updateScrollPosition()" class="flex-row"> <div (onResize)="scrollControls.updateScrollPosition()" class="flex-row">
<div class="safe-area-pseudo-padding-start"></div> <div class="safe-area-pseudo-padding-start"></div>
<ng-container *ngFor="let course of courses"> <ng-container *ngFor="let course of courses">
<core-courses-course-progress [course]="course" class="core-block_starredcourses" <core-courses-course-list-item [course]="course" class="core-block_starredcourses" layout="summarycard"
[showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-list-item>
</ng-container> </ng-container>
<div class="safe-area-pseudo-padding-end"></div> <div class="safe-area-pseudo-padding-end"></div>
</div> </div>

View File

@ -1,47 +1,124 @@
<ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0" <ion-item class="ion-text-wrap core-course-list-item" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
[attr.aria-label]="course.displayname || course.fullname" detail="true" button> [class.item-dimmed]="course.hidden" [attr.aria-label]="course.displayname || course.fullname" detail="true" button
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon" *ngIf="layout == 'list' || layout == 'listwithenrol'">
[attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color"></ion-icon> <ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon core-course-thumb"
<ion-avatar *ngIf="course.courseImage" slot="start"> [attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color">
<img [src]="course.courseImage" core-external-content alt=""/> </ion-icon>
<ion-avatar *ngIf="course.courseImage" slot="start" class="core-course-thumb">
<img [src]="course.courseImage" core-external-content alt="" />
</ion-avatar> </ion-avatar>
<ion-label> <ion-label>
<h2> <ion-row>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id"> <ion-col>
</core-format-text> <p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
</h2> class="core-course-additional-info">
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)" <span *ngIf="course.categoryname" class="core-course-category">
class="core-course-additional-info"> <core-format-text [text]="course.categoryname"></core-format-text>
<span *ngIf="course.categoryname" class="core-course-category"> </span>
<core-format-text [text]="course.categoryname"></core-format-text> <span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
</span> class="core-course-category"> | </span>
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname" <span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
class="core-course-category"> | </span> class="core-course-shortname">
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname" <core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
class="core-course-shortname"> </core-format-text>
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id"> </span>
</core-format-text> </p>
</span> <p class="item-heading">
</p> <ion-icon name="fas-star" *ngIf="course.isfavourite" [attr.aria-label]="'core.courses.favourite' | translate">
<p *ngIf="isEnrolled && course.progress! >= 0 && course.completionusertracked !== false"> </ion-icon>
<core-progress-bar [progress]="course.progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar> <span class="sr-only" *ngIf="course.isfavourite">{{ 'core.courses.aria:favourite' | translate }}</span>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</p>
</ion-col>
<ion-col size="auto">
<ng-container *ngIf="!isEnrolled">
<ion-icon *ngFor="let icon of enrolmentIcons" color="dark" size="small" [name]="icon.icon"
[title]="icon.label | translate" [attr.aria-label]="icon.label | translate">
</ion-icon>
</ng-container>
<ng-container *ngIf="isEnrolled">
<ng-container *ngTemplateOutlet="download"></ng-container>
</ng-container>
</ion-col>
</ion-row>
<p *ngIf="isEnrolled && progress! >= 0 && completionUserTracked !== false">
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
</p> </p>
</ion-label> </ion-label>
<ng-container *ngIf="!isEnrolled">
<ion-icon *ngFor="let icon of icons" color="dark" size="small" [name]="icon.icon"
[title]="icon.label | translate"
[attr.aria-label]="icon.label | translate"
slot="end">
</ion-icon>
</ng-container>
<div class="core-button-spinner" *ngIf="isEnrolled && showDownload" slot="end"> </ion-item>
<core-download-refresh
[status]="prefetchCourseData.status" <ion-card [attr.course-color]="course.color ? null : course.colorNumber" *ngIf="layout == 'card' || layout == 'summarycard'"
[statusTranslatable]="prefetchCourseData.statusTranslatable" class="core-course-list-card" [class.item-dimmed]="course.hidden">
[enabled]="true" <div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
canTrustDownload="false" [style.background-color]="course.color">
[loading]="prefetchCourseData.loading" <img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt="" />
</div>
<ion-item button lines="none" (click)="openCourse()" [attr.aria-label]="course.displayname || course.fullname"
class="core-course-header" [class.item-disabled]="course.visible == 0"
[class.core-course-only-title]="layout == 'summarycard' || progress < 0 && completionUserTracked === false" detail="false">
<ion-label class="ion-text-wrap core-course-title">
<ion-row>
<ion-col>
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
class="core-course-additional-info">
<span class="sr-only">{{ 'core.courses.aria:coursecategory' | translate }}</span>
<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>
<p class="item-heading">
<ion-icon name="fas-star" *ngIf="course.isfavourite" [attr.aria-label]="'core.courses.favourite' | translate">
</ion-icon>
<span class="sr-only" *ngIf="course.isfavourite">{{ 'core.courses.aria:favourite' | translate }}</span>
<span class="sr-only">{{ 'core.courses.aria:coursename' | translate }}</span>
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</p>
</ion-col>
<ion-col size="auto"
*ngIf="isEnrolled && ((downloadCourseEnabled && !courseOptionMenuEnabled && showDownload) || courseOptionMenuEnabled)">
<ng-container *ngTemplateOutlet="download"></ng-container>
</ion-col>
</ion-row>
<div *ngIf="layout == 'card' && progress >= 0 && completionUserTracked !== false" lines="none" class="core-course-progress">
<core-progress-bar [progress]="progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
</div>
</ion-label>
</ion-item>
</ion-card>
<ng-template #download>
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload">
<core-download-refresh [status]="prefetchCourseData.status" [enabled]="downloadCourseEnabled"
[statusTranslatable]="prefetchCourseData.statusTranslatable" canTrustDownload="false" [loading]="prefetchCourseData.loading"
(action)="prefetchCourse()"></core-download-refresh> (action)="prefetchCourse()"></core-download-refresh>
</div> </div>
</ion-item>
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled">
<!-- Download course spinner. -->
<ion-spinner *ngIf="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
<!-- Downloaded icon. -->
<ion-icon *ngIf="downloadCourseEnabled && prefetchCourseData.downloadSucceeded && !showSpinner" class="core-icon-downloaded"
name="cloud-done" color="success" role="status" [attr.aria-label]="'core.downloaded' | translate"></ion-icon>
<!-- Options menu. -->
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
[attr.aria-label]="('core.displayoptions' | translate)">
<ion-icon name="ellipsis-vertical" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</div>
</ng-template>

View File

@ -1,13 +1,12 @@
@import "~theme/globals"; @import "~theme/globals";
:host { .core-course-list-item {
.course-icon { .course-icon {
color: white; color: white;
background: var(--gray-light); background: var(--gray-light);
padding: 8px; padding: 8px;
font-size: 24px; font-size: 24px;
border-radius: 50%; border-radius: 50%;
margin-inline-end: 16px;
-webkit-transition: all 50ms ease-in-out; -webkit-transition: all 50ms ease-in-out;
transition: all 50ms ease-in-out; transition: all 50ms ease-in-out;
} }
@ -22,4 +21,177 @@
-webkit-transition: all 50ms ease-in-out; -webkit-transition: all 50ms ease-in-out;
transition: all 50ms ease-in-out; transition: all 50ms ease-in-out;
} }
.core-course-thumb {
@include margin(12px, 16px, 12px, null);
align-self: flex-start;
}
.core-course-summary {
margin-top: 12px;
}
}
.item-heading ion-icon {
margin-right: 4px;
color: var(--core-star-color);
}
ion-card {
--vertical-margin: 12px;
display: flex;
flex-direction: column;
align-self: stretch;
height: calc(100% - var(--vertical-margin) - var(--vertical-margin));
margin-top: var(--vertical-margin);
margin-bottom: var(--vertical-margin);
@for $i from 0 to length($core-course-image-background) {
&[course-color="#{$i}"] .core-course-thumb {
background: var(--core-course-color-#{$i});
}
}
ion-row {
min-height: var(--a11y-min-target-size);
ion-col .core-button-spinner {
min-width: calc(var(--a11y-min-target-size) + 16px);
}
}
.core-course-thumb {
padding-top: 40%;
width: 100%;
overflow: hidden;
cursor: pointer;
pointer-events: auto;
position: relative;
background-position: center;
background-size: cover;
-webkit-transition: all 50ms ease-in-out;
transition: all 50ms ease-in-out;
&.core-course-color-img {
background: var(--ion-item-background);
}
img {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin: auto;
}
}
@if ($core-course-hide-thumb-on-cards) {
.core-course-thumb {
display: none;
}
}
@if ($core-course-thumb-on-cards-background) {
.core-course-thumb {
background: $core-course-thumb-on-cards-background !important;
}
}
.core-course-additional-info {
margin-bottom: 8px;
}
.core-course-header {
flex-grow: 1;
display: flex;
flex-direction: column;
--inner-padding-end: 0px;
&::part(native) {
flex-grow: 1;
align-items: self-start;
}
&.core-course-only-title {
&::part(native) {
flex-grow: 1;
}
}
.core-course-title {
margin: 12px 0;
flex-grow: 1;
width: 100%;
max-width: 100%;
}
.core-button-spinner {
margin: 0;
}
.core-button-spinner ion-spinner {
vertical-align: top; // the better option for most scenarios
vertical-align: -webkit-baseline-middle; // the best for those that support it
}
.core-button-spinner .core-icon-downloaded {
font-size: 28.8px;
margin-top: 8px;
vertical-align: top;
}
.item-button[icon-only] {
min-width: 50px;
width: 50px;
}
}
@if ($core-course-hide-progress-on-cards) {
.core-course-progress {
display: none;
}
}
}
button {
z-index: 1;
}
:host-context(.core-horizontal-scroll) {
@include horizontal_scroll_item(80%, 250px, 300px);
ion-card {
.core-course-thumb {
padding-top: 30%;
}
ion-item.core-course-header {
--padding-start: 4px;
.core-course-title {
margin: 7px 0;
.item-heading ion-icon {
margin-right: 2px;
}
}
.core-button-spinner {
min-height: 40px;
min-width: 40px;
ion-spinner {
width: 20px;
height: 20px;
}
}
.item-button[icon-only] {
min-width: 40px;
width: 40px;
padding: 8px;
}
}
}
} }

View File

@ -12,15 +12,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { CoreConstants } from '@/core/constants';
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { CoreCourseProvider, CoreCourse } from '@features/course/services/course'; import { CoreCourseProvider, CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { CoreUser } from '@features/user/services/user';
import { CoreNavigator } from '@services/navigator'; import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseListItem, CoreCourses } from '../../services/courses'; import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses';
import { CoreCoursesHelper } from '../../services/courses-helper'; import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
/** /**
* 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.
@ -37,10 +41,10 @@ import { CoreCoursesHelper } from '../../services/courses-helper';
export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges { export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges {
@Input() course!: CoreCourseListItem; // The course to render. @Input() course!: CoreCourseListItem; // The course to render.
@Input() showDownload = false; // If true, will show download button. @Input() showDownload = false; // If true, will show download button.
@Input() layout: 'listwithenrol'|'summarycard'|'list'|'card' = 'listwithenrol';
icons: CoreCoursesEnrolmentIcons[] = []; enrolmentIcons: CoreCoursesEnrolmentIcons[] = [];
isEnrolled = false; isEnrolled = false;
prefetchCourseData: CorePrefetchStatusInfo = { prefetchCourseData: CorePrefetchStatusInfo = {
icon: '', icon: '',
@ -49,8 +53,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
loading: true, loading: true,
}; };
protected courseStatusObserver?: CoreEventObserver; showSpinner = false;
downloadCourseEnabled = false;
courseOptionMenuEnabled = false;
progress = -1;
completionUserTracked: boolean | undefined = false;
protected courseStatus = CoreConstants.NOT_DOWNLOADED;
protected isDestroyed = false; protected isDestroyed = false;
protected courseStatusObserver?: CoreEventObserver;
protected siteUpdatedObserver?: CoreEventObserver;
/** /**
* @inheritdoc * @inheritdoc
@ -58,48 +70,71 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
CoreCoursesHelper.loadCourseColorAndImage(this.course); CoreCoursesHelper.loadCourseColorAndImage(this.course);
this.isEnrolled = this.course.progress !== undefined; // Assume is enroled if mode is not listwithenrol.
this.isEnrolled = this.layout != 'listwithenrol' || this.course.progress !== undefined;
if (!this.isEnrolled) { if (!this.isEnrolled) {
try { try {
const course = await CoreCourses.getUserCourse(this.course.id); const course = await CoreCourses.getUserCourse(this.course.id);
this.course.progress = course.progress; this.course = Object.assign(this.course, course);
this.course.completionusertracked = course.completionusertracked; this.updateCourseFields();
this.isEnrolled = true; this.isEnrolled = true;
if (this.showDownload) {
this.initPrefetchCourse();
}
} catch { } catch {
this.isEnrolled = false; this.isEnrolled = false;
} }
} }
if (!this.isEnrolled) { if (this.isEnrolled) {
this.icons = []; if (this.showDownload) {
this.initPrefetchCourse();
}
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
if (this.downloadCourseEnabled) {
this.initPrefetchCourse();
}
// This field is only available from 3.6 onwards.
this.courseOptionMenuEnabled = (this.layout != 'listwithenrol' && this.layout != 'summarycard') &&
this.course.isfavourite !== undefined;
// Refresh the enabled flag if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
const wasEnabled = this.downloadCourseEnabled;
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
if (!wasEnabled && this.downloadCourseEnabled) {
// Download course is enabled now, initialize it.
this.initPrefetchCourse();
}
}, CoreSites.getCurrentSiteId());
} else if ('enrollmentmethods' in this.course) {
this.enrolmentIcons = [];
this.course.enrollmentmethods.forEach((instance) => { this.course.enrollmentmethods.forEach((instance) => {
if (instance === 'self') { if (instance === 'self') {
this.icons.push({ this.enrolmentIcons.push({
label: 'core.courses.selfenrolment', label: 'core.courses.selfenrolment',
icon: 'fas-key', icon: 'fas-key',
}); });
} else if (instance === 'guest') { } else if (instance === 'guest') {
this.icons.push({ this.enrolmentIcons.push({
label: 'core.courses.allowguests', label: 'core.courses.allowguests',
icon: 'fas-unlock', icon: 'fas-unlock',
}); });
} else if (instance === 'paypal') { } else if (instance === 'paypal') {
this.icons.push({ this.enrolmentIcons.push({
label: 'core.courses.paypalaccepted', label: 'core.courses.paypalaccepted',
icon: 'fab-paypal', icon: 'fab-paypal',
}); });
} }
}); });
if (this.icons.length == 0) { if (this.enrolmentIcons.length == 0) {
this.icons.push({ this.enrolmentIcons.push({
label: 'core.courses.notenrollable', label: 'core.courses.notenrollable',
icon: 'fas-lock', icon: 'fas-lock',
}); });
@ -114,6 +149,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
if (this.showDownload && this.isEnrolled) { if (this.showDownload && this.isEnrolled) {
this.initPrefetchCourse(); this.initPrefetchCourse();
} }
this.updateCourseFields();
}
/**
* Helper function to update course fields.
*/
protected updateCourseFields(): void {
this.progress = 'progress' in this.course ? this.course.progress || -1 : -1;
this.completionUserTracked = 'completionusertracked' in this.course && this.course.completionusertracked;
} }
/** /**
@ -179,6 +224,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
protected updateCourseStatus(status: string): void { protected updateCourseStatus(status: string): void {
const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status); const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);
this.courseStatus = status;
this.prefetchCourseData.status = statusData.status; this.prefetchCourseData.status = statusData.status;
this.prefetchCourseData.icon = statusData.icon; this.prefetchCourseData.icon = statusData.icon;
this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable; this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
@ -188,11 +234,11 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
/** /**
* Prefetch the course. * Prefetch the course.
* *
* @param e Click event. * @param event Click event.
*/ */
async prefetchCourse(e?: Event): Promise<void> { async prefetchCourse(event?: Event): Promise<void> {
e?.preventDefault(); event?.preventDefault();
e?.stopPropagation(); event?.stopPropagation();
try { try {
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course); await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
@ -203,12 +249,149 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On
} }
} }
/**
* Delete the course.
*/
async deleteCourse(): Promise<void> {
try {
await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata');
} catch (error) {
if (CoreDomUtils.isCanceledError(error)) {
throw error;
}
return;
}
const modal = await CoreDomUtils.showModalLoading();
try {
await CoreCourseHelper.deleteCourseFiles(this.course.id);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile'));
} finally {
modal.dismiss();
}
}
/**
* Show the context menu.
*
* @param event Click Event.
*/
async showCourseOptionsMenu(event: Event): Promise<void> {
event.preventDefault();
event.stopPropagation();
const popoverData = await CoreDomUtils.openPopover<string>({
component: CoreCoursesCourseOptionsMenuComponent,
componentProps: {
course: this.course,
prefetch: this.prefetchCourseData,
},
event: event,
});
switch (popoverData) {
case 'download':
if (!this.prefetchCourseData.loading) {
this.prefetchCourse(event);
}
break;
case 'delete':
if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') {
this.deleteCourse();
}
break;
case 'hide':
this.setCourseHidden(true);
break;
case 'show':
this.setCourseHidden(false);
break;
case 'favourite':
this.setCourseFavourite(true);
break;
case 'unfavourite':
this.setCourseFavourite(false);
break;
default:
break;
}
}
/**
* Hide/Unhide the course from the course list.
*
* @param hide True to hide and false to show.
*/
protected async setCourseHidden(hide: boolean): Promise<void> {
this.showSpinner = true;
// We should use null to unset the preference.
try {
await CoreUser.updateUserPreference(
'block_myoverview_hidden_course_' + this.course.id,
hide ? '1' : undefined,
);
this.course.hidden = hide;
(<CoreEnrolledCourseDataWithExtraInfoAndOptions> this.course).hidden = hide;
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
courseId: this.course.id,
course: this.course,
action: CoreCoursesProvider.ACTION_STATE_CHANGED,
state: CoreCoursesProvider.STATE_HIDDEN,
value: hide,
}, CoreSites.getCurrentSiteId());
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.');
}
} finally {
this.showSpinner = false;
}
}
/**
* Favourite/Unfavourite the course from the course list.
*
* @param favourite True to favourite and false to unfavourite.
*/
protected async setCourseFavourite(favourite: boolean): Promise<void> {
this.showSpinner = true;
try {
await CoreCourses.setFavouriteCourse(this.course.id, favourite);
this.course.isfavourite = favourite;
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
courseId: this.course.id,
course: this.course,
action: CoreCoursesProvider.ACTION_STATE_CHANGED,
state: CoreCoursesProvider.STATE_FAVOURITE,
value: favourite,
}, CoreSites.getCurrentSiteId());
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.');
}
} finally {
this.showSpinner = false;
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.isDestroyed = true; this.isDestroyed = true;
this.courseStatusObserver?.off(); this.courseStatusObserver?.off();
this.siteUpdatedObserver?.off();
} }
} }

View File

@ -151,9 +151,3 @@
} }
} }
} }
:host-context(body.version-3-1) {
.core-course-thumb{
display: none;
}
}

View File

@ -221,7 +221,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
component: CoreCoursesCourseOptionsMenuComponent, component: CoreCoursesCourseOptionsMenuComponent,
componentProps: { componentProps: {
course: this.course, course: this.course,
courseStatus: this.courseStatus,
prefetch: this.prefetchCourseData, prefetch: this.prefetchCourseData,
}, },
event: e, event: e,

View File

@ -10,11 +10,10 @@
<ion-buttons slot="end"> <ion-buttons slot="end">
<core-context-menu> <core-context-menu>
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000" <core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" iconAction="toggle"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item> [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="900" <core-context-menu-item [priority]="900" [content]="'core.courses.showonlyenrolled' | translate" (action)="filterEnrolled()"
[content]="'core.courses.showonlyenrolled' | translate" (action)="filterEnrolled()" iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
</core-context-menu> </core-context-menu>
</ion-buttons> </ion-buttons>
</ion-toolbar> </ion-toolbar>
@ -28,12 +27,12 @@
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon> <ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
<ion-label> <ion-label>
<p class="item-heading"> <p class="item-heading">
<core-format-text [text]="currentCategory.name" contextLevel="coursecat" <core-format-text [text]="currentCategory.name" contextLevel="coursecat" [contextInstanceId]="currentCategory.id">
[contextInstanceId]="currentCategory.id"></core-format-text> </core-format-text>
</p> </p>
<p *ngIf="currentCategory.description"> <p *ngIf="currentCategory.description">
<core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat" <core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat"
[contextInstanceId]="currentCategory.id"></core-format-text> [contextInstanceId]="currentCategory.id"></core-format-text>
</p> </p>
</ion-label> </ion-label>
</ion-item> </ion-item>
@ -45,8 +44,7 @@
</ion-label> </ion-label>
</ion-item-divider> </ion-item-divider>
<section *ngFor="let category of categories"> <section *ngFor="let category of categories">
<ion-item button class="ion-text-wrap" (click)="openCategory(category.id)" [attr.aria-label]="category.name" <ion-item button class="ion-text-wrap" (click)="openCategory(category.id)" [attr.aria-label]="category.name" detail="true">
detail="true">
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon> <ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
<ion-label> <ion-label>
<h2> <h2>

View File

@ -19,7 +19,7 @@ import { makeSingleton } from '@singletons';
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreWSError } from '@classes/errors/wserror'; import { CoreWSError } from '@classes/errors/wserror';
import { CoreCourseWithImageAndColor } from './courses-helper'; import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper';
const ROOT_CACHE_KEY = 'mmCourses:'; const ROOT_CACHE_KEY = 'mmCourses:';
@ -1384,7 +1384,10 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
/** /**
* Course to render as list item. * Course to render as list item.
*/ */
export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & { export type CoreCourseListItem = ((CoreCourseSearchedData & CoreCourseWithImageAndColor) |
CoreCourseAnyCourseDataWithExtraInfoAndOptions) & {
isfavourite?: boolean; // If the user marked this course a favourite.
hidden?: boolean; // If the user hide the course from the dashboard.
completionusertracked?: boolean; // If the user is completion tracked. completionusertracked?: boolean; // If the user is completion tracked.
progress?: number | null; // Progress percentage. progress?: number | null; // Progress percentage.
}; };

View File

@ -1163,3 +1163,7 @@ iframe {
display: none !important; display: none !important;
} }
} }
ion-grid.core-no-grid > ion-row {
display: block;
}