MOBILE-3594 sitehome: Add my courses page
parent
38b11281a9
commit
cc6e87ea5c
|
@ -1,20 +1,25 @@
|
|||
<ng-container *ngIf="enabled && !(loading || status === statusDownloading)">
|
||||
<ng-container *ngIf="enabled && !loading">
|
||||
<!-- Download button. -->
|
||||
<ion-button *ngIf="status == statusNotDownloaded" fill="clear" (click)="download($event, false)" color="dark"
|
||||
class="core-animate-show-hide" [attr.aria-label]="'core.download' | translate">
|
||||
class="core-animate-show-hide" [attr.aria-label]="(statusTranslatable || 'core.download') | translate">
|
||||
<ion-icon slot="icon-only" name="cloud-download"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Refresh button. -->
|
||||
<ion-button *ngIf="status == statusOutdated || (status == statusDownloaded && !canTrustDownload)" fill="clear"
|
||||
(click)="download($event, true)" color="dark" class="core-animate-show-hide" [attr.aria-label]="'core.refresh' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-sync"></ion-icon>
|
||||
(click)="download($event, true)" color="dark" class="core-animate-show-hide"
|
||||
attr.aria-label]="(statusTranslatable || 'core.refresh') | translate">
|
||||
<ion-icon slot="icon-only" name="fas-redo-alt"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Downloaded status icon. -->
|
||||
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
|
||||
name="cloud-done" [attr.aria-label]="'core.downloaded' | translate" role="status"></ion-icon>
|
||||
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal"
|
||||
color="success" name="cloud-done" [attr.aria-label]="(statusTranslatable || 'core.downloaded') | translate"
|
||||
role="status"></ion-icon>
|
||||
|
||||
<ion-spinner *ngIf="status === statusDownloading" class="core-animate-show-hide"
|
||||
[attr.aria-label]="(statusTranslatable || 'core.downloading') | translate"></ion-spinner>
|
||||
</ng-container>
|
||||
|
||||
<!-- Spinner. -->
|
||||
<ion-spinner *ngIf="loading || status === statusDownloading" class="core-animate-show-hide"></ion-spinner>
|
||||
<ion-spinner *ngIf="loading" class="core-animate-show-hide" [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
|
|
|
@ -19,7 +19,7 @@ import { CoreConstants } from '@/core/constants';
|
|||
* Component to show a download button with refresh option, the spinner and the status of it.
|
||||
*
|
||||
* Usage:
|
||||
* <core-download-refresh [status]="status" enabled="true" canCheckUpdates="true" action="download()"></core-download-refresh>
|
||||
* <core-download-refresh [status]="status" enabled="true" canTrustDownload="true" action="download()"></core-download-refresh>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-download-refresh',
|
||||
|
@ -29,6 +29,7 @@ import { CoreConstants } from '@/core/constants';
|
|||
export class CoreDownloadRefreshComponent {
|
||||
|
||||
@Input() status?: string; // Download status.
|
||||
@Input() statusTranslatable?: string; // Download status translatable string.
|
||||
@Input() enabled = false; // Whether the download is enabled.
|
||||
@Input() loading = true; // Force loading status when is not downloading.
|
||||
@Input() canTrustDownload = false; // If false, refresh will be shown if downloaded.
|
||||
|
|
|
@ -22,10 +22,14 @@ import { CoreDirectivesModule } from '@directives/directives.module';
|
|||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
||||
import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
|
||||
import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress';
|
||||
import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -35,10 +39,13 @@ import { CoreCoursesCourseListItemComponent } from './course-list-item/course-li
|
|||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
],
|
||||
})
|
||||
export class CoreCoursesComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<ion-item button class="ion-text-wrap" (click)="action('download')" *ngIf="downloadCourseEnabled">
|
||||
<ion-icon *ngIf="!prefetch.loading" [name]="prefetch.icon" slot="start"></ion-icon>
|
||||
<ion-spinner *ngIf="prefetch.loading" slot="start"></ion-spinner>
|
||||
<ion-label><h2>{{ prefetch.statusTranslatable | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('delete')" *ngIf="prefetch.status == 'downloaded' || prefetch.status == 'outdated'">
|
||||
<ion-icon name="fas-trash" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'addon.storagemanager.deletecourse' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('hide')" *ngIf="!course.hidden">
|
||||
<ion-icon name="fas-eye" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.hidecourse' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('show')" *ngIf="course.hidden">
|
||||
<ion-icon name="fas-eye-slash" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.show' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('favourite')" *ngIf="!course.isfavourite">
|
||||
<ion-icon name="fas-star" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.addtofavourites' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('unfavourite')" *ngIf="course.isfavourite">
|
||||
<ion-icon name="far-star" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.removefromfavourites' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavParams, PopoverController } from '@ionic/angular';
|
||||
import { CoreCourses } from '../../services/courses';
|
||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CorePrefetchStatusInfo } from '@features/course/services/course.helper';
|
||||
|
||||
/**
|
||||
* This component is meant to display a popover with the course options.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-options-menu',
|
||||
templateUrl: 'core-courses-course-options-menu.html',
|
||||
})
|
||||
export class CoreCoursesCourseOptionsMenuComponent implements OnInit {
|
||||
|
||||
course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course.
|
||||
prefetch!: CorePrefetchStatusInfo; // The prefecth info.
|
||||
|
||||
downloadCourseEnabled = false;
|
||||
|
||||
constructor(
|
||||
navParams: NavParams,
|
||||
protected popoverController: PopoverController,
|
||||
) {
|
||||
this.course = navParams.get('course') || {};
|
||||
this.prefetch = navParams.get('prefetch') || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an action over the course.
|
||||
*
|
||||
* @param action Action name to take.
|
||||
*/
|
||||
action(action: string): void {
|
||||
this.popoverController.dismiss(action);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<ion-card [attr.course-color]="course.color ? null : course.colorNumber">
|
||||
<div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
|
||||
[style.background-color]="course.color">
|
||||
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt=""/>
|
||||
</div>
|
||||
<ion-item button lines="none" (click)="openCourse()" [title]="course.displayname || course.fullname"
|
||||
class="core-course-header" [class.item-disabled]="course.visible == 0"
|
||||
[class.core-course-more-than-title]="(course.progress != null && course.progress! >= 0)">
|
||||
<ion-label
|
||||
class="ion-text-wrap core-course-title"
|
||||
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
|
||||
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
|
||||
<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>
|
||||
<ion-icon name="fas-star" *ngIf="course.isfavourite"></ion-icon>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
|
||||
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end">
|
||||
<core-download-refresh
|
||||
[status]="prefetchCourseData.status"
|
||||
[enabled]="downloadCourseEnabled"
|
||||
[statusTranslatable]="prefetchCourseData.statusTranslatable"
|
||||
canTrustDownload="false"
|
||||
[loading]="prefetchCourseData.loading"
|
||||
action="prefetchCourse()"></core-download-refresh>
|
||||
</div>
|
||||
|
||||
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled" slot="end">
|
||||
<!-- Download course spinner. -->
|
||||
<ion-spinner *ngIf="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"></ion-spinner>
|
||||
|
||||
<!-- 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"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showAll && course.progress != null && course.progress! >= 0 && course.completionusertracked !== false" lines="none">
|
||||
<ion-label><core-progress-bar [progress]="course.progress"></core-progress-bar></ion-label>
|
||||
</ion-item>
|
||||
<ng-content></ng-content>
|
||||
</ion-card>
|
|
@ -0,0 +1,163 @@
|
|||
:host {
|
||||
ion-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
height: calc(100% - 20px);
|
||||
|
||||
&[course-color="0"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-0);
|
||||
}
|
||||
&[course-color="1"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-1);
|
||||
}
|
||||
&[course-color="2"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-2);
|
||||
}
|
||||
&[course-color="3"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-3);
|
||||
}
|
||||
&[course-color="4"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-4);
|
||||
}
|
||||
&[course-color="5"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-5);
|
||||
}
|
||||
&[course-color="6"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-6);
|
||||
}
|
||||
&[course-color="7"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-7);
|
||||
}
|
||||
&[course-color="8"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-8);
|
||||
}
|
||||
&[course-color="9"] .core-course-thumb {
|
||||
background: var(--core-course-image-background-9);
|
||||
}
|
||||
|
||||
.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: white;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-additional-info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.core-course-header {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
.core-course-title {
|
||||
margin: 5px 0;
|
||||
flex-grow: 1;
|
||||
|
||||
h2 ion-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--core-star-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.core-course-more-than-title {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo
|
||||
:host-context(.core-horizontal-scroll) {
|
||||
/*@include horizontal_scroll_item(80%, 250px, 300px);*/
|
||||
|
||||
ion-card {
|
||||
.core-course-thumb {
|
||||
padding-top: 30%;
|
||||
}
|
||||
|
||||
.core-course-link {
|
||||
/*@include padding(4px, 0px, 4px, 8px);*/
|
||||
.core-course-additional-info {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.core-course-title {
|
||||
margin: 3px 0;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
ion-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.core-course-with-buttons {
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
.core-button-spinner {
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
|
||||
ion-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.item-button[icon-only] {
|
||||
min-width: 40px;
|
||||
width: 40px;
|
||||
font-size: 1.5rem;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.version-3-1) {
|
||||
.core-course-thumb{
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { PopoverController } from '@ionic/angular';
|
||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course.helper';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
|
||||
|
||||
/**
|
||||
* This component is meant to display a course for a list of courses with progress.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-courses-course-progress [course]="course">
|
||||
* </core-courses-course-progress>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-progress',
|
||||
templateUrl: 'core-courses-course-progress.html',
|
||||
styleUrls: ['course-progress.scss'],
|
||||
})
|
||||
export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course to render.
|
||||
@Input() showAll = false; // If true, will show all actions, options, star and progress.
|
||||
@Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown.
|
||||
|
||||
courseStatus = CoreConstants.NOT_DOWNLOADED;
|
||||
isDownloading = false;
|
||||
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
status: '',
|
||||
loading: true,
|
||||
};
|
||||
|
||||
showSpinner = false;
|
||||
downloadCourseEnabled = false;
|
||||
courseOptionMenuEnabled = false;
|
||||
|
||||
protected isDestroyed = false;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
protected siteUpdatedObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected popoverCtrl: PopoverController,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
|
||||
if (this.downloadCourseEnabled) {
|
||||
this.initPrefetchCourse();
|
||||
}
|
||||
|
||||
// This field is only available from 3.6 onwards.
|
||||
this.courseOptionMenuEnabled = this.showAll && typeof 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.instance.isDownloadCourseDisabledInSite();
|
||||
|
||||
if (!wasEnabled && this.downloadCourseEnabled) {
|
||||
// Download course is enabled now, initialize it.
|
||||
this.initPrefetchCourse();
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize prefetch course.
|
||||
*/
|
||||
async initPrefetchCourse(): Promise<void> {
|
||||
if (typeof this.courseStatusObserver != 'undefined') {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for status change in course.
|
||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => {
|
||||
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
this.updateCourseStatus(data.status);
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
// Determine course prefetch icon.
|
||||
const status = await CoreCourse.instance.getCourseStatus(this.course.id);
|
||||
|
||||
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||
this.courseStatus = status;
|
||||
|
||||
if (this.prefetchCourseData.loading) {
|
||||
// Course is being downloaded. Get the download promise.
|
||||
const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id);
|
||||
if (promise) {
|
||||
// There is a download promise. If it fails, show an error.
|
||||
promise.catch((error) => {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No download, this probably means that the app was closed while downloading. Set previous status.
|
||||
CoreCourse.instance.setCoursePreviousStatus(this.course.id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a course.
|
||||
*/
|
||||
openCourse(): void {
|
||||
CoreCourseHelper.instance.openCourse(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch the course.
|
||||
*
|
||||
* @param e Click event.
|
||||
*/
|
||||
prefetchCourse(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
/* @ todo try {
|
||||
CoreCourseHelper.instance.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the course.
|
||||
*/
|
||||
async deleteCourse(): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
||||
} catch (error) {
|
||||
if (CoreDomUtils.instance.isCanceledError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.instance.deleteCourseFiles(this.course.id);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile'));
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course status icon and title.
|
||||
*
|
||||
* @param status Status to show.
|
||||
*/
|
||||
protected updateCourseStatus(status: string): void {
|
||||
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||
|
||||
this.courseStatus = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu.
|
||||
*
|
||||
* @param e Click Event.
|
||||
* @todo
|
||||
*/
|
||||
async showCourseOptionsMenu(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const popover = await this.popoverCtrl.create({
|
||||
component: CoreCoursesCourseOptionsMenuComponent,
|
||||
componentProps: {
|
||||
course: this.course,
|
||||
courseStatus: this.courseStatus,
|
||||
prefetch: this.prefetchCourseData,
|
||||
},
|
||||
event: e,
|
||||
});
|
||||
popover.present();
|
||||
|
||||
const action = await popover.onDidDismiss<string>();
|
||||
|
||||
if (action.data) {
|
||||
switch (action.data) {
|
||||
case 'download':
|
||||
if (!this.prefetchCourseData.loading) {
|
||||
this.prefetchCourse(e);
|
||||
}
|
||||
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.
|
||||
* @todo
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected setCourseHidden(hide: boolean): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Favourite/Unfavourite the course from the course list.
|
||||
*
|
||||
* @param favourite True to favourite and false to unfavourite.
|
||||
* @todo
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected setCourseFavourite(favourite: boolean): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
this.siteUpdatedObserver?.off();
|
||||
this.courseStatusObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -32,7 +32,7 @@ const routes: Routes = [
|
|||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'all',
|
||||
redirectTo: 'my',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
|
@ -57,6 +57,12 @@ const routes: Routes = [
|
|||
import('@features/courses/pages/search/search.page.module')
|
||||
.then(m => m.CoreCoursesSearchPageModule),
|
||||
},
|
||||
{
|
||||
path: 'my',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/my-courses/my-courses.page.module')
|
||||
.then(m => m.CoreCoursesMyCoursesPageModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.mycourses' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ng-container *ngIf="downloadAllCoursesEnabled && courses && courses.length >= 2">
|
||||
<ion-button *ngIf="!downloadAllCoursesLoading" (click)="prefetchCourses()"
|
||||
[attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="downloadAllCoursesIcon" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-spinner *ngIf="downloadAllCoursesBadge == '' && downloadAllCoursesLoading"
|
||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
<ion-badge *ngIf="downloadAllCoursesBadge != '' && downloadAllCoursesLoading"
|
||||
[attr.aria-label]="'core.downloading' | translate">{{downloadAllCoursesBadge}}</ion-badge>
|
||||
</ng-container>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="coursesLoaded">
|
||||
<ion-searchbar #searchbar *ngIf="courses && courses.length > 5" [(ngModel)]="filter" (ionInput)="filterChanged($event)"
|
||||
(ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
<ion-grid class="ion-no-padding safe-area-page">
|
||||
<ion-row 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-xl="4" align-self-stretch>
|
||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true">
|
||||
</core-courses-course-progress>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<core-empty-box *ngIf="!courses || !courses.length" icon="fas-graduation-cap"
|
||||
[message]="'core.courses.nocourses' | translate">
|
||||
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,51 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { CoreCoursesMyCoursesPage } from './my-courses.page';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesMyCoursesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesMyCoursesPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesMyCoursesPageModule { }
|
|
@ -0,0 +1,215 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { NavController, IonSearchbar, IonRefresher } from '@ionic/angular';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import {
|
||||
CoreCoursesProvider,
|
||||
CoreCoursesMyCoursesUpdatedEventData,
|
||||
CoreCourses,
|
||||
} from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CoreCourseHelper } from '@features/course/services/course.helper';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
// import { CoreCourseOptionsDelegate } from '@core/course/services/options-delegate';
|
||||
|
||||
/**
|
||||
* Page that displays the list of courses the user is enrolled in.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-my-courses',
|
||||
templateUrl: 'my-courses.html',
|
||||
})
|
||||
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(IonSearchbar) searchbar!: IonSearchbar;
|
||||
|
||||
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
searchEnabled = false;
|
||||
filter = '';
|
||||
showFilter = false;
|
||||
coursesLoaded = false;
|
||||
downloadAllCoursesIcon = CoreConstants.NOT_DOWNLOADED_ICON;
|
||||
downloadAllCoursesLoading = false;
|
||||
downloadAllCoursesBadge = '';
|
||||
downloadAllCoursesEnabled = false;
|
||||
|
||||
protected myCoursesObserver: CoreEventObserver;
|
||||
protected siteUpdatedObserver: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
protected courseIds = '';
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
// Update list if user enrols in a course.
|
||||
this.myCoursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data: CoreCoursesMyCoursesUpdatedEventData) => {
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
this.fetchCourses();
|
||||
}
|
||||
},
|
||||
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.fetchCourses().finally(() => {
|
||||
this.coursesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchCourses(): Promise<void> {
|
||||
try {
|
||||
const courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = await CoreCourses.instance.getUserCourses();
|
||||
const courseIds = courses.map((course) => course.id);
|
||||
|
||||
this.courseIds = courseIds.join(',');
|
||||
|
||||
await CoreCoursesHelper.instance.loadCoursesExtraInfo(courses);
|
||||
|
||||
if (CoreCourses.instance.canGetAdminAndNavOptions()) {
|
||||
const options = await CoreCourses.instance.getCoursesAdminAndNavOptions(courseIds);
|
||||
courses.forEach((course) => {
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
});
|
||||
}
|
||||
|
||||
this.courses = courses;
|
||||
this.filteredCourses = this.courses;
|
||||
this.filter = '';
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the courses.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshCourses(refresher: CustomEvent<IonRefresher>): void {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||
// @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds) {
|
||||
promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchCourses().finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the filter.
|
||||
*/
|
||||
switchFilter(): void {
|
||||
this.filter = '';
|
||||
this.showFilter = !this.showFilter;
|
||||
this.filteredCourses = this.courses;
|
||||
if (this.showFilter) {
|
||||
setTimeout(() => {
|
||||
this.searchbar.setFocus();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter has changed.
|
||||
*
|
||||
* @param Received Event.
|
||||
*/
|
||||
filterChanged(event?: Event): void {
|
||||
const target = <HTMLInputElement>event?.target || null;
|
||||
const newValue = target ? String(target.value).trim().toLowerCase() : null;
|
||||
if (!newValue || !this.courses) {
|
||||
this.filteredCourses = this.courses;
|
||||
} else {
|
||||
// Use displayname if avalaible, or fullname if not.
|
||||
if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') {
|
||||
this.filteredCourses = this.courses.filter((course) => course.displayname!.toLowerCase().indexOf(newValue) > -1);
|
||||
} else {
|
||||
this.filteredCourses = this.courses.filter((course) => course.fullname.toLowerCase().indexOf(newValue) > -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(): Promise<void> {
|
||||
this.downloadAllCoursesLoading = true;
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.instance.confirmAndPrefetchCourses(this.courses, (progress) => {
|
||||
this.downloadAllCoursesBadge = progress.count + ' / ' + progress.total;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadAllCoursesBadge = '';
|
||||
this.downloadAllCoursesLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch(): void {
|
||||
this.navCtrl.navigateForward(['/courses/search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.myCoursesObserver?.off();
|
||||
this.siteUpdatedObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -64,6 +64,11 @@ ion-alert.core-alert-network-error .alert-head {
|
|||
right: unset;
|
||||
left: -15%;
|
||||
}
|
||||
ion-alert.core-nohead {
|
||||
.alert-head {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Ionic item divider.
|
||||
ion-item-divider {
|
||||
|
@ -76,6 +81,16 @@ ion-list.list-md {
|
|||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
// Header.
|
||||
ion-tabs.hide-header ion-header {
|
||||
display: none;
|
||||
}
|
||||
ion-toolbar {
|
||||
ion-spinner {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// Modals.
|
||||
.core-modal-fullscreen .modal-wrapper {
|
||||
position: absolute;
|
||||
|
|
|
@ -103,6 +103,10 @@
|
|||
ion-toolbar {
|
||||
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
||||
--background: var(--custom-toolbar-background, var(--ion-color-primary));
|
||||
|
||||
ion-spinner {
|
||||
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
|
||||
}
|
||||
}
|
||||
|
||||
ion-action-sheet {
|
||||
|
@ -149,6 +153,18 @@
|
|||
|
||||
--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-star-color: var(--custom-star-color, var(--core-color));
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
Loading…
Reference in New Issue