MOBILE-2310 courses: Apply download courses

main
Dani Palou 2018-01-17 15:00:14 +01:00
parent fe2c4cce74
commit 04085d5929
9 changed files with 277 additions and 45 deletions

View File

@ -242,6 +242,10 @@ export class CoreCourseSectionPage implements OnDestroy {
// Ignore errors (shouldn't happen).
});
}
}).catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
}

View File

@ -2,11 +2,11 @@
<a ion-item text-wrap detail-none (click)="openCourse(course)" [title]="course.fullname">
<h2 float-start><core-format-text [text]="course.fullname"></core-format-text></h2>
<!-- Download course. -->
<!--<button [hidden]="!downloadButton.isDownload" ion-button icon-only clear color="dark" float-end>
<ion-icon name="cloud-download"></ion-icon>
</button>-->
<button *ngIf="prefetchCourseData.prefetchCourseIcon != 'spinner'" ion-button icon-only clear color="dark" float-end (click)="prefetchCourse($event)">
<ion-icon [name]="prefetchCourseData.prefetchCourseIcon"></ion-icon>
</button>
<!-- Download course spinner. -->
<!-- <ion-spinner *ngIf="prefetchCourseIcon == 'spinner'" class="core-course-download-spinner"></ion-spinner> -->
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" class="core-course-download-spinner" float-end></ion-spinner>
</a>
<ion-item text-wrap *ngIf="course.summary && course.summary.length">
<p>

View File

@ -12,10 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
import { CoreCourseProvider } from '../../../course/providers/course';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
/**
* This component is meant to display a course for a list of courses with progress.
@ -29,32 +34,52 @@ import { CoreCourseFormatDelegate } from '../../../course/providers/format-deleg
selector: 'core-courses-course-progress',
templateUrl: 'course-progress.html'
})
export class CoreCoursesCourseProgressComponent implements OnInit {
export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
@Input() course: any; // The course to render.
isDownloading: boolean;
protected obsStatus;
protected downloadText;
protected downloadingText;
protected downloadButton = {
isDownload: true,
className: 'core-download-course',
priority: 1000
prefetchCourseData = {
prefetchCourseIcon: 'spinner'
};
protected buttons;
constructor(private navCtrl: NavController, private translate: TranslateService,
private courseFormatDelegate: CoreCourseFormatDelegate) {
this.downloadText = this.translate.instant('core.course.downloadcourse');
this.downloadingText = this.translate.instant('core.downloading');
protected isDestroyed = false;
protected courseStatusObserver;
constructor(private navCtrl: NavController, private translate: TranslateService, private courseHelper: CoreCourseHelperProvider,
private courseFormatDelegate: CoreCourseFormatDelegate, private domUtils: CoreDomUtilsProvider,
private courseProvider: CoreCourseProvider, eventsProvider: CoreEventsProvider, sitesProvider: CoreSitesProvider) {
// Listen for status change in course.
this.courseStatusObserver = eventsProvider.on(CoreEventsProvider.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course.id) {
this.prefetchCourseData.prefetchCourseIcon = this.courseHelper.getCourseStatusIconFromStatus(data.status);
}
}, sitesProvider.getCurrentSiteId());
}
/**
* Component being initialized.
*/
ngOnInit() {
// @todo: Handle course prefetch.
// Determine course prefetch icon.
this.courseHelper.getCourseStatusIcon(this.course.id).then((icon) => {
this.prefetchCourseData.prefetchCourseIcon = icon;
if (icon == 'spinner') {
// Course is being downloaded. Get the download promise.
const promise = this.courseHelper.getCourseDownloadPromise(this.course.id);
if (promise) {
// There is a download promise. If it fails, show an error.
promise.catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
this.courseProvider.setCoursePreviousStatus(this.course.id);
}
}
});
}
/**
@ -64,4 +89,30 @@ export class CoreCoursesCourseProgressComponent implements OnInit {
this.courseFormatDelegate.openCourse(this.navCtrl, course);
}
/**
* Prefetch the course.
*
* @param {Event} e Click event.
*/
prefetchCourse(e: Event) {
e.preventDefault();
e.stopPropagation();
this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
})
}
/**
* Component destroyed.
*/
ngOnDestroy() {
this.isDestroyed = true;
if (this.courseStatusObserver) {
this.courseStatusObserver.off();
}
}
}

View File

@ -17,22 +17,22 @@
<p *ngIf="course.startdate">{{course.startdate * 1000 | coreFormatDate:"dfdaymonthyear"}} <span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"dfdaymonthyear"}}</span></p>
</a>
<ion-item text-wrap *ngIf="course.summary">
<ion-item text-wrap *ngIf="course.summary" detail-none>
<core-format-text [text]="course.summary" maxHeight="120"></core-format-text>
</ion-item>
<a ion-item text-wrap *ngIf="course.contacts && course.contacts.length" core-user-link [attr.aria-label]="'core.viewprofile' | translate">
<a ion-item text-wrap *ngIf="course.contacts && course.contacts.length" detail-none>
<p class="item-heading">{{ 'core.teachers' | translate }}</p>
<p *ngFor="let contact of course.contacts">{{contact.fullname}}</p>
</a>
<core-file *ngFor="let file of course.overviewfiles" [file]="file" [component]="component" [componentId]="course.id"></core-file>
<div *ngIf="!isEnrolled">
<div *ngIf="!isEnrolled" detail-none>
<ion-item text-wrap *ngFor="let instance of selfEnrolInstances">
<p class="item-heading">{{ instance.name }}</p>
<button ion-button block margin-top (click)="selfEnrolClicked(instance.id)">{{ 'core.courses.enrolme' | translate }}</button>
</ion-item>
</div>
<ion-item text-wrap *ngIf="!isEnrolled && paypalEnabled">
<ion-item text-wrap *ngIf="!isEnrolled && paypalEnabled" detail-none>
<p class="item-heading">{{ 'core.courses.paypalaccepted' | translate }}</p>
<p>{{ 'core.paymentinstant' | translate }}</p>
<button ion-button block margin-top (click)="paypalEnrol()">{{ 'core.courses.sendpaymentbutton' | translate }}</button>
@ -40,12 +40,11 @@
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
<p>{{ 'core.courses.notenrollable' | translate }}</p>
</ion-item>
<!-- @todo: Prefetch course.
<a class="item item-icon-left" ng-if="handlersShouldBeShown" ng-click="prefetchCourse()">
<i ng-if="prefetchCourseIcon != 'spinner'" class="icon {{prefetchCourseIcon}}"></i>
<ion-spinner ng-if="prefetchCourseIcon == 'spinner'" class="icon"></ion-spinner>
<a ion-item *ngIf="handlersShouldBeShown" (click)="prefetchCourse()" detail-none>
<ion-icon *ngIf="prefetchCourseData.prefetchCourseIcon != 'spinner'" [name]="prefetchCourseData.prefetchCourseIcon" item-start></ion-icon>
<ion-spinner *ngIf="prefetchCourseData.prefetchCourseIcon == 'spinner'" item-start></ion-spinner>
<h2>{{ 'core.course.downloadcourse' | translate }}</h2>
</a> -->
</a>
<a ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="handlersShouldBeShown">
<ion-icon name="briefcase" item-start></ion-icon>
<h2>{{ 'core.course.contents' | translate }}</h2>

View File

@ -22,6 +22,8 @@ import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCoursesDelegate } from '../../providers/delegate';
import { CoreCourseProvider } from '../../../course/providers/course';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
/**
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
@ -40,7 +42,9 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
selfEnrolInstances: any[] = [];
paypalEnabled: boolean;
dataLoaded: boolean;
prefetchCourseIcon: string;
prefetchCourseData = {
prefetchCourseIcon: 'spinner'
};
protected guestWSAvailable: boolean;
protected isGuestEnabled: boolean = false;
@ -55,15 +59,24 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
protected selfEnrolModal: Modal;
protected pageDestroyed = false;
protected currentInstanceId: number;
protected courseStatusObserver;
constructor(private navCtrl: NavController, navParams: NavParams, private sitesProvider: CoreSitesProvider,
private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider, appProvider: CoreAppProvider,
private coursesProvider: CoreCoursesProvider, private platform: Platform, private modalCtrl: ModalController,
private translate: TranslateService, private eventsProvider: CoreEventsProvider,
private coursesDelegate: CoreCoursesDelegate) {
private coursesDelegate: CoreCoursesDelegate, private courseHelper: CoreCourseHelperProvider,
private courseProvider: CoreCourseProvider) {
this.course = navParams.get('course');
this.isMobile = appProvider.isMobile();
this.isDesktop = appProvider.isDesktop();
// Listen for status change in course.
this.courseStatusObserver = eventsProvider.on(CoreEventsProvider.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course.id) {
this.prefetchCourseData.prefetchCourseIcon = this.courseHelper.getCourseStatusIconFromStatus(data.status);
}
}, sitesProvider.getCurrentSiteId());
}
/**
@ -88,7 +101,26 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
});
this.getCourse().finally(() => {
// @todo: Prefetch course.
// Determine course prefetch icon.
this.courseHelper.getCourseStatusIcon(this.course.id).then((icon) => {
this.prefetchCourseData.prefetchCourseIcon = icon;
if (icon == 'spinner') {
// Course is being downloaded. Get the download promise.
let promise = this.courseHelper.getCourseDownloadPromise(this.course.id);
if (promise) {
// There is a download promise. If it fails, show an error.
promise.catch((error) => {
if (!this.pageDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
this.courseProvider.setCoursePreviousStatus(this.course.id);
}
}
});
});
}
@ -97,6 +129,10 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
*/
ngOnDestroy() {
this.pageDestroyed = true;
if (this.courseStatusObserver) {
this.courseStatusObserver.off();
}
}
/**
@ -387,4 +423,16 @@ export class CoreCoursesCoursePreviewPage implements OnDestroy {
});
});
}
/**
* Prefetch the course.
*/
prefetchCourse() {
this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, undefined, this.course._handlers)
.catch((error) => {
if (!this.pageDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
})
}
}

View File

@ -7,6 +7,7 @@
<ion-icon name="search"></ion-icon>
</button>
<core-context-menu>
<core-context-menu-item [hidden]="!courses || courses.length < 2" [priority]="800" [content]="'core.courses.downloadcourses' | translate" (action)="prefetchCourses()" [iconAction]="prefetchCoursesData.icon" [closeOnClick]="false" [badge]="prefetchCoursesData.badge"></core-context-menu-item>
<core-context-menu-item [hidden]="!courses || courses.length <= 5" [priority]="700" [content]="'core.courses.filtermycourses' | translate" (action)="switchFilter()" [iconAction]="'funnel'"></core-context-menu-item>
</core-context-menu>
</ion-buttons>

View File

@ -18,6 +18,7 @@ import { CoreEventsProvider } from '../../../../providers/events';
import { CoreSitesProvider } from '../../../../providers/sites';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
/**
* Page that displays the list of courses the user is enrolled in.
@ -34,14 +35,16 @@ export class CoreCoursesMyCoursesPage implements OnDestroy {
filter = '';
showFilter = false;
coursesLoaded = false;
prefetchCoursesData: any = {};
protected prefetchIconInitialized = false;
protected myCoursesObserver;
protected siteUpdatedObserver;
protected isDestroyed = false;
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider) {}
private sitesProvider: CoreSitesProvider, private courseHelper: CoreCourseHelperProvider) {}
/**
* View loaded.
@ -81,7 +84,7 @@ export class CoreCoursesMyCoursesPage implements OnDestroy {
this.filteredCourses = this.courses;
this.filter = '';
// this.initPrefetchCoursesIcon();
this.initPrefetchCoursesIcon();
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
@ -139,10 +142,60 @@ export class CoreCoursesMyCoursesPage implements OnDestroy {
}
}
/**
* Prefetch all the courses.
*/
prefetchCourses() {
let initialIcon = this.prefetchCoursesData.icon;
this.prefetchCoursesData.icon = 'spinner';
this.prefetchCoursesData.badge = '';
return this.courseHelper.confirmAndPrefetchCourses(this.courses, (progress) => {
this.prefetchCoursesData.badge = progress.count + ' / ' + progress.total;
}).then((downloaded) => {
this.prefetchCoursesData.icon = downloaded ? 'ion-android-refresh' : initialIcon;
}, (error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
this.prefetchCoursesData.icon = initialIcon;
}
}).finally(() => {
this.prefetchCoursesData.badge = '';
});
}
/**
* Initialize the prefetch icon for the list of courses.
*/
protected initPrefetchCoursesIcon() {
if (this.prefetchIconInitialized) {
// Already initialized.
return;
}
this.prefetchIconInitialized = true;
if (!this.courses || this.courses.length < 2) {
// Not enough courses.
this.prefetchCoursesData.icon = '';
return;
}
this.courseHelper.determineCoursesStatus(this.courses).then((status) => {
let icon = this.courseHelper.getCourseStatusIconFromStatus(status);
if (icon == 'spinner') {
// It seems all courses are being downloaded, show a download button instead.
icon = 'cloud-download';
}
this.prefetchCoursesData.icon = icon;
});
}
/**
* Page destroyed.
*/
ngOnDestroy() {
this.isDestroyed = true;
this.myCoursesObserver && this.myCoursesObserver.off();
this.siteUpdatedObserver && this.siteUpdatedObserver.off();
}

View File

@ -46,22 +46,30 @@
<!-- Courses tab. -->
<core-tab [title]="'core.courses.courses' | translate" (ionSelect)="tabChanged('courses')">
<core-loading [hideUntil]="courses.loaded" class="core-loading-center">
<!-- "Time" selector. -->
<div no-padding class="clearfix" [hidden]="showFilter">
<ion-select [title]="'core.show' | translate" [(ngModel)]="courses.selected" float-start (ngModelChange)="selectedChanged()">
<ion-option value="inprogress">{{ 'core.courses.inprogress' | translate }}</ion-option>
<ion-option value="future">{{ 'core.courses.future' | translate }}</ion-option>
<ion-option value="past">{{ 'core.courses.past' | translate }}</ion-option>
</ion-select>
<button [hidden]="!courses[courses.selected] || !courses[courses.selected].length" ion-button icon-only clear color="dark" float-end>
<ion-icon name="cloud-download"></ion-icon>
</button>
<!-- Download all courses. -->
<div *ngIf="courses[courses.selected] && courses[courses.selected].length > 1">
<button *ngIf="prefetchCoursesData[courses.selected].icon && prefetchCoursesData[courses.selected].icon != 'spinner'" ion-button icon-only clear color="dark" float-end (click)="prefetchCourses()">
<ion-icon [name]="prefetchCoursesData[courses.selected].icon"></ion-icon>
</button>
<ion-spinner *ngIf="!prefetchCoursesData[courses.selected].icon || prefetchCoursesData[courses.selected].icon == 'spinner'" float-end></ion-spinner>
<span float-end *ngIf="prefetchCoursesData[courses.selected].badge">{{prefetchCoursesData[courses.selected].badge}}</span>
</div>
</div>
<!-- Filter courses. -->
<div no-padding padding-bottom [hidden]="!showFilter">
<ion-item>
<ion-label><ion-icon name="funnel" class="placeholder-icon"></ion-icon></ion-label>
<ion-input type="text" name="filter" clearInput [(ngModel)]="courses.filter" (ngModelChange)="filterChanged($event)" [placeholder]="'core.courses.filtermycourses' | translate"></ion-input>
</ion-item>
</div>
<!-- List of courses. -->
<div>
<ion-grid no-padding>
<ion-row no-padding>

View File

@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
import { CoreCoursesProvider } from '../../providers/courses';
import { CoreCoursesMyOverviewProvider } from '../../providers/my-overview';
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
import * as moment from 'moment';
/**
@ -27,7 +28,7 @@ import * as moment from 'moment';
selector: 'page-core-courses-my-overview',
templateUrl: 'my-overview.html',
})
export class CoreCoursesMyOverviewPage {
export class CoreCoursesMyOverviewPage implements OnDestroy {
tabShown = 'courses';
timeline = {
sort: 'sortbydates',
@ -52,21 +53,24 @@ export class CoreCoursesMyOverviewPage {
searchEnabled: boolean;
filteredCourses: any[];
tabs = [];
prefetchCoursesData = {
inprogress: {},
past: {},
future: {}
};
protected prefetchIconInitialized = false;
protected myCoursesObserver;
protected siteUpdatedObserver;
protected prefetchIconsInitialized = false;
protected isDestroyed;
constructor(private navCtrl: NavController, private coursesProvider: CoreCoursesProvider,
private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider) {}
private domUtils: CoreDomUtilsProvider, private myOverviewProvider: CoreCoursesMyOverviewProvider,
private courseHelper: CoreCourseHelperProvider) {}
/**
* View loaded.
*/
ionViewDidLoad() {
this.searchEnabled = !this.coursesProvider.isSearchCoursesDisabledInSite();
// @todo: Course download.
}
/**
@ -144,6 +148,8 @@ export class CoreCoursesMyOverviewPage {
this.courses.filter = '';
this.showFilter = false;
this.filteredCourses = this.courses[this.courses.selected];
this.initPrefetchCoursesIcons();
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting my overview data.');
});
@ -229,6 +235,7 @@ export class CoreCoursesMyOverviewPage {
}
break;
case 'courses':
this.prefetchIconsInitialized = false;
return this.fetchMyOverviewCourses();
}
}).finally(() => {
@ -315,4 +322,65 @@ export class CoreCoursesMyOverviewPage {
selectedChanged() {
this.filteredCourses = this.courses[this.courses.selected];
}
/**
* Prefetch all the shown courses.
*/
prefetchCourses() {
let selected = this.courses.selected,
selectedData = this.prefetchCoursesData[selected],
initialIcon = selectedData.icon;
selectedData.icon = 'spinner';
selectedData.badge = '';
return this.courseHelper.confirmAndPrefetchCourses(this.courses[selected], (progress) => {
selectedData.badge = progress.count + ' / ' + progress.total;
}).then((downloaded) => {
selectedData.icon = downloaded ? 'refresh' : initialIcon;
}, (error) => {
if (!this.isDestroyed) {
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
selectedData.icon = initialIcon;
}
}).finally(() => {
selectedData.badge = '';
});
}
/**
* Initialize the prefetch icon for selected courses.
*/
protected initPrefetchCoursesIcons() {
if (this.prefetchIconsInitialized) {
// Already initialized.
return;
}
this.prefetchIconsInitialized = true;
Object.keys(this.prefetchCoursesData).forEach((filter) => {
if (!this.courses[filter] || this.courses[filter].length < 2) {
// Not enough courses.
this.prefetchCoursesData[filter].icon = '';
return;
}
this.courseHelper.determineCoursesStatus(this.courses[filter]).then((status) => {
let icon = this.courseHelper.getCourseStatusIconFromStatus(status);
if (icon == 'spinner') {
// It seems all courses are being downloaded, show a download button instead.
icon = 'cloud-download';
}
this.prefetchCoursesData[filter].icon = icon;
});
});
}
/**
* Component being destroyed.
*/
ngOnDestroy() {
this.isDestroyed = true;
}
}