MOBILE-2310 course: Apply prefetch to section page
parent
64f1551d56
commit
fe2c4cce74
|
@ -38,7 +38,7 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange
|
||||||
@Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
|
@Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
|
||||||
@Input() iconAction?: string; // Name of the icon to be shown on the right side of the item. It represents the action to do on
|
@Input() iconAction?: string; // Name of the icon to be shown on the right side of the item. It represents the action to do on
|
||||||
// click. If is "spinner" an spinner will be shown. If no icon or spinner is selected, no action
|
// click. If is "spinner" an spinner will be shown. If no icon or spinner is selected, no action
|
||||||
// or link will work. If href but no iconAction is provided ion-arrow-right-c will be used.
|
// or link will work. If href but no iconAction is provided arrow-right will be used.
|
||||||
@Input() ariaDescription?: string; // Aria label to add to iconDescription.
|
@Input() ariaDescription?: string; // Aria label to add to iconDescription.
|
||||||
@Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
|
@Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
|
||||||
@Input() href?: string; // Link to go if no action provided.
|
@Input() href?: string; // Link to go if no action provided.
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { NavParams, ViewController } from 'ionic-angular';
|
import { NavParams, ViewController } from 'ionic-angular';
|
||||||
import { CoreContextMenuItemComponent } from './context-menu-item';
|
import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||||
|
import { CoreLoggerProvider } from '../../providers/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display a list of items received by param in a popover.
|
* Component to display a list of items received by param in a popover.
|
||||||
|
@ -26,10 +27,12 @@ import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||||
export class CoreContextMenuPopoverComponent {
|
export class CoreContextMenuPopoverComponent {
|
||||||
title: string;
|
title: string;
|
||||||
items: CoreContextMenuItemComponent[];
|
items: CoreContextMenuItemComponent[];
|
||||||
|
protected logger: any;
|
||||||
|
|
||||||
constructor(navParams: NavParams, private viewCtrl: ViewController) {
|
constructor(navParams: NavParams, private viewCtrl: ViewController, logger: CoreLoggerProvider) {
|
||||||
this.title = navParams.get('title');
|
this.title = navParams.get('title');
|
||||||
this.items = navParams.get('items') || [];
|
this.items = navParams.get('items') || [];
|
||||||
|
this.logger = logger.getInstance('CoreContextMenuPopoverComponent');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -51,7 +54,10 @@ export class CoreContextMenuPopoverComponent {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
if (!item.iconAction || item.iconAction == 'spinner') {
|
if (!item.iconAction) {
|
||||||
|
this.logger.warn('Items with action must have an icon action to work', item);
|
||||||
|
return false;
|
||||||
|
} else if (item.iconAction == 'spinner') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,18 +10,14 @@
|
||||||
|
|
||||||
<core-loading [hideUntil]="loaded">
|
<core-loading [hideUntil]="loaded">
|
||||||
<!-- Section selector. -->
|
<!-- Section selector. -->
|
||||||
<ion-row *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length">
|
<div *ngIf="!componentInstances.sectionSelector && displaySectionSelector && sections && sections.length" no-padding class="clearfix">
|
||||||
<ion-col col-11>
|
<!-- @todo: How to display availabilityinfo and not visible messages? -->
|
||||||
<ion-item>
|
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions" float-start>
|
||||||
<ion-select [ngModel]="selectedSection" (ngModelChange)="sectionChanged($event)" [compareWith]="compareSections" [selectOptions]="selectOptions">
|
|
||||||
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
<ion-option *ngFor="let section of sections" [value]="section">{{section.formattedName || section.name}}</ion-option>
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
<!-- Section download. -->
|
||||||
</ion-col>
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
<ion-col col-1 class="text-right">
|
</div>
|
||||||
<!-- @todo Download button. -->
|
|
||||||
</ion-col>
|
|
||||||
</ion-row>
|
|
||||||
<ng-template #sectionSelector></ng-template>
|
<ng-template #sectionSelector></ng-template>
|
||||||
|
|
||||||
<!-- Single section. -->
|
<!-- Single section. -->
|
||||||
|
@ -45,11 +41,14 @@
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Template to render a section. -->
|
||||||
<ng-template #sectionTemplate let-section="section">
|
<ng-template #sectionTemplate let-section="section">
|
||||||
<section ion-list *ngIf="section.hasContent">
|
<section ion-list *ngIf="section.hasContent">
|
||||||
<!-- Title is only displayed when viewing all sections. -->
|
<!-- Title is only displayed when viewing all sections. -->
|
||||||
<ion-item-divider text-wrap color="light" *ngIf="selectedSection.id == allSectionsId && section.name">
|
<ion-item-divider text-wrap color="light" *ngIf="selectedSection.id == allSectionsId && section.name">
|
||||||
<core-format-text [text]="section.name"></core-format-text>
|
<core-format-text [text]="section.name"></core-format-text>
|
||||||
|
<!-- Section download. -->
|
||||||
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: section}"></ng-container>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
|
|
||||||
<ion-item text-wrap *ngIf="section.summary">
|
<ion-item text-wrap *ngIf="section.summary">
|
||||||
|
@ -62,5 +61,23 @@
|
||||||
</section>
|
</section>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Template to render a section download button/progress. -->
|
||||||
|
<ng-template #sectionDownloadTemplate let-section="section">
|
||||||
|
<div *ngIf="section && downloadEnabled" float-end>
|
||||||
|
<!-- Download button. -->
|
||||||
|
<button *ngIf="section.showDownload && !section.isDownloading && !section.isCalculating" (click)="prefetch($event, section)" ion-button icon-only clear color="dark" [attr.aria-label]="'core.download' | translate">
|
||||||
|
<ion-icon name="cloud-download"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<!-- Refresh button. -->
|
||||||
|
<button *ngIf="section.showRefresh && !section.isDownloading && !section.isCalculating" (click)="prefetch($event, section)" ion-button icon-only clear color="dark" [attr.aria-label]="'core.refresh' | translate">
|
||||||
|
<ion-icon name="refresh"></ion-icon>
|
||||||
|
</button>
|
||||||
|
<!-- Spinner (downloading or calculating status). -->
|
||||||
|
<ion-spinner *ngIf="(section.isDownloading && section.total > 0) || section.isCalculating"></ion-spinner>
|
||||||
|
<!-- Download progress. -->
|
||||||
|
<ion-badge class="core-course-download-section-progress" *ngIf="section.isDownloading && section.total > 0 && section.count < section.total">{{section.count}} / {{section.total}}</ion-badge>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
<!-- Custom course format that overrides the default one. -->
|
<!-- Custom course format that overrides the default one. -->
|
||||||
<ng-template #courseFormat></ng-template>
|
<ng-template #courseFormat></ng-template>
|
|
@ -12,12 +12,17 @@
|
||||||
// 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 { Component, Input, OnInit, OnChanges, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
|
import { Component, Input, OnInit, OnChanges, OnDestroy, ViewContainerRef, ComponentFactoryResolver, ViewChild, ChangeDetectorRef,
|
||||||
SimpleChange, Output, EventEmitter } from '@angular/core';
|
SimpleChange, Output, EventEmitter } from '@angular/core';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
import { CoreLoggerProvider } from '../../../../providers/logger';
|
import { CoreLoggerProvider } from '../../../../providers/logger';
|
||||||
|
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||||
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
import { CoreCourseProvider } from '../../../course/providers/course';
|
import { CoreCourseProvider } from '../../../course/providers/course';
|
||||||
|
import { CoreCourseHelperProvider } from '../../../course/providers/helper';
|
||||||
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
|
import { CoreCourseFormatDelegate } from '../../../course/providers/format-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '../../../course/providers/module-prefetch-delegate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||||
|
@ -33,9 +38,10 @@ import { CoreCourseFormatDelegate } from '../../../course/providers/format-deleg
|
||||||
selector: 'core-course-format',
|
selector: 'core-course-format',
|
||||||
templateUrl: 'format.html'
|
templateUrl: 'format.html'
|
||||||
})
|
})
|
||||||
export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
@Input() course: any; // The course to render.
|
@Input() course: any; // The course to render.
|
||||||
@Input() sections: any[]; // List of course sections.
|
@Input() sections: any[]; // List of course sections.
|
||||||
|
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
@Output() completionChanged?: EventEmitter<void>; // Will emit an event when any module completion changes.
|
||||||
|
|
||||||
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
// Get the containers where to inject dynamic components. We use a setter because they might be inside a *ngIf.
|
||||||
|
@ -71,12 +77,53 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
|
|
||||||
protected logger;
|
protected logger;
|
||||||
|
protected sectionStatusObserver;
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
constructor(logger: CoreLoggerProvider, private cfDelegate: CoreCourseFormatDelegate, translate: TranslateService,
|
||||||
private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef) {
|
private factoryResolver: ComponentFactoryResolver, private cdr: ChangeDetectorRef,
|
||||||
|
private courseHelper: CoreCourseHelperProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
|
eventsProvider: CoreEventsProvider, private sitesProvider: CoreSitesProvider,
|
||||||
|
prefetchDelegate: CoreCourseModulePrefetchDelegate) {
|
||||||
|
|
||||||
this.logger = logger.getInstance('CoreCourseFormatComponent');
|
this.logger = logger.getInstance('CoreCourseFormatComponent');
|
||||||
this.selectOptions.title = translate.instant('core.course.sections');
|
this.selectOptions.title = translate.instant('core.course.sections');
|
||||||
this.completionChanged = new EventEmitter();
|
this.completionChanged = new EventEmitter();
|
||||||
|
|
||||||
|
// Listen for section status changes.
|
||||||
|
this.sectionStatusObserver = eventsProvider.on(CoreEventsProvider.SECTION_STATUS_CHANGED, (data) => {
|
||||||
|
if (this.downloadEnabled && this.sections && this.sections.length && this.course && data.sectionId &&
|
||||||
|
data.courseId == this.course.id) {
|
||||||
|
// Check if the affected section is being downloaded. If so, we don't update section status
|
||||||
|
// because it'll already be updated when the download finishes.
|
||||||
|
let downloadId = this.courseHelper.getSectionDownloadId({id: data.sectionId});
|
||||||
|
if (prefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the affected section.
|
||||||
|
let section;
|
||||||
|
for (let i = 0; i < this.sections.length; i++) {
|
||||||
|
let s = this.sections[i];
|
||||||
|
if (s.id === data.sectionId) {
|
||||||
|
section = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!section) {
|
||||||
|
// Section not found, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate the status.
|
||||||
|
this.courseHelper.calculateSectionStatus(section, this.course.id, false).then(() => {
|
||||||
|
if (section.isDownloading && !prefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||||
|
// All the modules are now downloading, set a download all promise.
|
||||||
|
this.prefetch(section, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, this.sitesProvider.getCurrentSiteId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,6 +166,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changes.downloadEnabled && this.downloadEnabled) {
|
||||||
|
this.calculateSectionsStatus(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the changes to the components and call ngOnChanges if it exists.
|
// Apply the changes to the components and call ngOnChanges if it exists.
|
||||||
for (let type in this.componentInstances) {
|
for (let type in this.componentInstances) {
|
||||||
let instance = this.componentInstances[type];
|
let instance = this.componentInstances[type];
|
||||||
|
@ -164,6 +215,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
||||||
// Set the Input data.
|
// Set the Input data.
|
||||||
this.componentInstances[type].course = this.course;
|
this.componentInstances[type].course = this.course;
|
||||||
this.componentInstances[type].sections = this.sections;
|
this.componentInstances[type].sections = this.sections;
|
||||||
|
this.componentInstances[type].downloadEnabled = this.downloadEnabled;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch(ex) {
|
} catch(ex) {
|
||||||
|
@ -202,4 +254,65 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges {
|
||||||
compareSections(s1: any, s2: any) : boolean {
|
compareSections(s1: any, s2: any) : boolean {
|
||||||
return s1 && s2 ? s1.id === s2.id : s1 === s2;
|
return s1 && s2 ? s1.id === s2.id : s1 === s2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the status of sections.
|
||||||
|
*
|
||||||
|
* @param {boolean} refresh [description]
|
||||||
|
*/
|
||||||
|
protected calculateSectionsStatus(refresh?: boolean) : void {
|
||||||
|
this.courseHelper.calculateSectionsStatus(this.sections, this.course.id, refresh).catch(() => {
|
||||||
|
// Ignore errors (shouldn't happen).
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
|
||||||
|
*
|
||||||
|
* @param {Event} e Click event.
|
||||||
|
* @param {any} section Section to download.
|
||||||
|
*/
|
||||||
|
prefetch(e: Event, section: any) : void {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
section.isCalculating = true;
|
||||||
|
this.courseHelper.confirmDownloadSizeSection(this.course.id, section, this.sections).then(() => {
|
||||||
|
this.prefetchSection(section, true);
|
||||||
|
}, (error) => {
|
||||||
|
// User cancelled or there was an error calculating the size.
|
||||||
|
if (error) {
|
||||||
|
this.domUtils.showErrorModal(error);
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
section.isCalculating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a section.
|
||||||
|
*
|
||||||
|
* @param {any} section The section to download.
|
||||||
|
* @param {boolean} [manual] Whether the prefetch was started manually or it was automatically started because all modules
|
||||||
|
* are being downloaded.
|
||||||
|
*/
|
||||||
|
protected prefetchSection(section: any, manual?: boolean) {
|
||||||
|
this.courseHelper.prefetchSection(section, this.course.id, this.sections).catch((error) => {
|
||||||
|
// Don't show error message if it's an automatic download.
|
||||||
|
if (!manual) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.domUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.sectionStatusObserver) {
|
||||||
|
this.sectionStatusObserver.off();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
<ion-buttons end>
|
<ion-buttons end>
|
||||||
<core-context-menu>
|
<core-context-menu>
|
||||||
<core-context-menu-item [priority]="900" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="'downloadSectionsIcon'"></core-context-menu-item>
|
<core-context-menu-item [priority]="900" [content]="'core.settings.enabledownloadsection' | translate" (action)="toggleDownload()" [iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="850" [content]="'core.course.downloadcourse' | translate" (action)="prefetchCourse()" [iconAction]="prefetchCourseData.prefetchCourseIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||||
</core-context-menu>
|
</core-context-menu>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
</ion-navbar>
|
</ion-navbar>
|
||||||
|
@ -19,6 +20,6 @@
|
||||||
<a ion-button class="tab-item">{{ 'core.course.contents' || translate }}</a>
|
<a ion-button class="tab-item">{{ 'core.course.contents' || translate }}</a>
|
||||||
<a *ngFor="let handler of courseHandlers" ion-button class="tab-item">{{ handler.data.title || translate }}</a>
|
<a *ngFor="let handler of courseHandlers" ion-button class="tab-item">{{ handler.data.title || translate }}</a>
|
||||||
</div>
|
</div>
|
||||||
<core-course-format [course]="course" [sections]="sections" (completionChanged)="onCompletionChange()"></core-course-format>
|
<core-course-format [course]="course" [sections]="sections" [downloadEnabled]="downloadEnabled" (completionChanged)="onCompletionChange()"></core-course-format>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|
|
@ -16,12 +16,13 @@ import { Component, ViewChild, OnDestroy } from '@angular/core';
|
||||||
import { IonicPage, NavParams, Content } from 'ionic-angular';
|
import { IonicPage, NavParams, Content } from 'ionic-angular';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CoreEventsProvider } from '../../../../providers/events';
|
import { CoreEventsProvider } from '../../../../providers/events';
|
||||||
|
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||||
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
|
import { CoreTextUtilsProvider } from '../../../../providers/utils/text';
|
||||||
import { CoreCourseProvider } from '../../providers/course';
|
import { CoreCourseProvider } from '../../providers/course';
|
||||||
import { CoreCourseHelperProvider } from '../../providers/helper';
|
import { CoreCourseHelperProvider } from '../../providers/helper';
|
||||||
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
|
||||||
import { CoreCoursesDelegate } from '../../../courses/providers/delegate';
|
import { CoreCoursesDelegate, CoreCoursesHandlerToDisplay } from '../../../courses/providers/delegate';
|
||||||
import { CoreCoursesProvider } from '../../../courses/providers/courses';
|
import { CoreCoursesProvider } from '../../../courses/providers/courses';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,16 +39,24 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
title: string;
|
title: string;
|
||||||
course: any;
|
course: any;
|
||||||
sections: any[];
|
sections: any[];
|
||||||
courseHandlers: any[];
|
courseHandlers: CoreCoursesHandlerToDisplay[];
|
||||||
dataLoaded: boolean;
|
dataLoaded: boolean;
|
||||||
|
downloadEnabled: boolean;
|
||||||
|
downloadEnabledIcon: string = 'square-outline'; // Disabled by default.
|
||||||
|
prefetchCourseData = {
|
||||||
|
prefetchCourseIcon: 'spinner'
|
||||||
|
};
|
||||||
|
|
||||||
protected moduleId;
|
protected moduleId;
|
||||||
protected completionObserver;
|
protected completionObserver;
|
||||||
|
protected courseStatusObserver;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
|
constructor(navParams: NavParams, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate,
|
private courseFormatDelegate: CoreCourseFormatDelegate, private coursesDelegate: CoreCoursesDelegate,
|
||||||
private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider,
|
private translate: TranslateService, private courseHelper: CoreCourseHelperProvider, eventsProvider: CoreEventsProvider,
|
||||||
private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider) {
|
private textUtils: CoreTextUtilsProvider, private coursesProvider: CoreCoursesProvider,
|
||||||
|
sitesProvider: CoreSitesProvider) {
|
||||||
this.course = navParams.get('course');
|
this.course = navParams.get('course');
|
||||||
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
this.title = courseFormatDelegate.getCourseTitle(this.course);
|
||||||
this.moduleId = navParams.get('moduleId');
|
this.moduleId = navParams.get('moduleId');
|
||||||
|
@ -57,6 +66,13 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
this.refreshAfterCompletionChange();
|
this.refreshAfterCompletionChange();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for changes in course status.
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,6 +82,27 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
this.loadData().finally(() => {
|
this.loadData().finally(() => {
|
||||||
this.dataLoaded = true;
|
this.dataLoaded = true;
|
||||||
delete this.moduleId; // Only load module automatically the first time.
|
delete this.moduleId; // Only load module automatically the first time.
|
||||||
|
|
||||||
|
// Determine the course prefetch status.
|
||||||
|
this.determineCoursePrefetchIcon().then(() => {
|
||||||
|
if (this.prefetchCourseData.prefetchCourseIcon == 'spinner') {
|
||||||
|
// Course is being downloaded. Get the download promise.
|
||||||
|
const promise = this.courseHelper.getCourseDownloadPromise(this.course.id);
|
||||||
|
if (promise) {
|
||||||
|
// There is a download promise. Show an error if it fails.
|
||||||
|
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).then((status) => {
|
||||||
|
this.prefetchCourseData.prefetchCourseIcon = this.courseHelper.getCourseStatusIconFromStatus(status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,10 +221,43 @@ export class CoreCourseSectionPage implements OnDestroy {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the prefetch icon of the course.
|
||||||
|
*/
|
||||||
|
protected determineCoursePrefetchIcon() {
|
||||||
|
return this.courseHelper.getCourseStatusIcon(this.course.id).then((icon) => {
|
||||||
|
this.prefetchCourseData.prefetchCourseIcon = icon;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the whole course.
|
||||||
|
*/
|
||||||
|
prefetchCourse() {
|
||||||
|
this.courseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course, this.sections, this.courseHandlers)
|
||||||
|
.then((downloaded) => {
|
||||||
|
if (downloaded && this.downloadEnabled) {
|
||||||
|
// Recalculate the status.
|
||||||
|
this.courseHelper.calculateSectionsStatus(this.sections, this.course.id).catch(() => {
|
||||||
|
// Ignore errors (shouldn't happen).
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle download enabled.
|
||||||
|
*/
|
||||||
|
toggleDownload() {
|
||||||
|
this.downloadEnabled = !this.downloadEnabled;
|
||||||
|
this.downloadEnabledIcon = this.downloadEnabled ? 'checkbox-outline' : 'square-outline';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page destroyed.
|
* Page destroyed.
|
||||||
*/
|
*/
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
this.isDestroyed = true;
|
||||||
if (this.completionObserver) {
|
if (this.completionObserver) {
|
||||||
this.completionObserver.off();
|
this.completionObserver.off();
|
||||||
}
|
}
|
||||||
|
|
|
@ -712,7 +712,7 @@ export class CoreCourseProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger mmCoreEventCourseStatusChanged with the right data.
|
* Trigger COURSE_STATUS_CHANGED with the right data.
|
||||||
*
|
*
|
||||||
* @param {number} courseId Course ID.
|
* @param {number} courseId Course ID.
|
||||||
* @param {string} status New course status.
|
* @param {string} status New course status.
|
||||||
|
@ -720,9 +720,8 @@ export class CoreCourseProvider {
|
||||||
*/
|
*/
|
||||||
protected triggerCourseStatusChanged(courseId: number, status: string, siteId?: string) : void {
|
protected triggerCourseStatusChanged(courseId: number, status: string, siteId?: string) : void {
|
||||||
this.eventsProvider.trigger(CoreEventsProvider.COURSE_STATUS_CHANGED, {
|
this.eventsProvider.trigger(CoreEventsProvider.COURSE_STATUS_CHANGED, {
|
||||||
siteId: siteId,
|
|
||||||
courseId: courseId,
|
courseId: courseId,
|
||||||
status: status
|
status: status
|
||||||
});
|
}, siteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,89 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CoreFilepoolProvider } from '../../../providers/filepool';
|
||||||
|
import { CoreSitesProvider } from '../../../providers/sites';
|
||||||
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
|
import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
|
||||||
|
import { CoreTextUtilsProvider } from '../../../providers/utils/text';
|
||||||
|
import { CoreTimeUtilsProvider } from '../../../providers/utils/time';
|
||||||
|
import { CoreUtilsProvider } from '../../../providers/utils/utils';
|
||||||
|
import { CoreCoursesDelegate, CoreCoursesHandlerToDisplay } from '../../courses/providers/delegate';
|
||||||
import { CoreCourseProvider } from './course';
|
import { CoreCourseProvider } from './course';
|
||||||
import { CoreCourseModuleDelegate } from './module-delegate';
|
import { CoreCourseModuleDelegate } from './module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from './module-prefetch-delegate';
|
||||||
|
import { CoreConstants } from '../../constants';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch info of a module.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModulePrefetchInfo = {
|
||||||
|
/**
|
||||||
|
* Downloaded size.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
size?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloadable size in a readable format.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
sizeReadable?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module status.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
status?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon's name of the module status.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
statusIcon?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time when the module was last downloaded.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
downloadTime?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download time in a readable format.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
downloadTimeReadable?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress of downloading a list of courses.
|
||||||
|
*/
|
||||||
|
export type CoreCourseCoursesProgress = {
|
||||||
|
/**
|
||||||
|
* Number of courses downloaded so far.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toal of courses to download.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the download has been successful so far.
|
||||||
|
* @type {boolean}
|
||||||
|
*/
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last downloaded course.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
courseId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to gather some common course functions.
|
* Helper to gather some common course functions.
|
||||||
|
@ -23,8 +103,13 @@ import { CoreCourseModuleDelegate } from './module-delegate';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CoreCourseHelperProvider {
|
export class CoreCourseHelperProvider {
|
||||||
|
|
||||||
|
protected courseDwnPromises: {[s: string]: {[id: number]: Promise<any>}} = {};
|
||||||
|
|
||||||
constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
|
constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider,
|
||||||
private moduleDelegate: CoreCourseModuleDelegate) {}
|
private moduleDelegate: CoreCourseModuleDelegate, private prefetchDelegate: CoreCourseModulePrefetchDelegate,
|
||||||
|
private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider,
|
||||||
|
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
|
||||||
|
private utils: CoreUtilsProvider, private translate: TranslateService, private coursesDelegate: CoreCoursesDelegate) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function treats every module on the sections provided to load the handler data, treat completion
|
* This function treats every module on the sections provided to load the handler data, treat completion
|
||||||
|
@ -65,6 +150,329 @@ export class CoreCourseHelperProvider {
|
||||||
return hasContent;
|
return hasContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the status of a section.
|
||||||
|
*
|
||||||
|
* @param {any} section Section to calculate its status. It can't be "All sections".
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @param {boolean} [refresh] True if it shouldn't use module status cache (slower).
|
||||||
|
* @return {Promise<any>} Promise resolved when the status is calculated.
|
||||||
|
*/
|
||||||
|
calculateSectionStatus(section: any, courseId: number, refresh?: boolean) : Promise<any> {
|
||||||
|
|
||||||
|
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
return Promise.reject(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the status of this section.
|
||||||
|
return this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id, refresh).then((result) => {
|
||||||
|
// Check if it's being downloaded.
|
||||||
|
const downloadId = this.getSectionDownloadId(section);
|
||||||
|
if (this.prefetchDelegate.isBeingDownloaded(downloadId)) {
|
||||||
|
result.status = CoreConstants.DOWNLOADING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set this section data.
|
||||||
|
section.showDownload = result.status === CoreConstants.NOT_DOWNLOADED;
|
||||||
|
section.showRefresh = result.status === CoreConstants.OUTDATED;
|
||||||
|
|
||||||
|
if (result.status !== CoreConstants.DOWNLOADING || !this.prefetchDelegate.isBeingDownloaded(section.id)) {
|
||||||
|
section.isDownloading = false;
|
||||||
|
section.total = 0;
|
||||||
|
} else {
|
||||||
|
// Section is being downloaded.
|
||||||
|
section.isDownloading = true;
|
||||||
|
this.prefetchDelegate.setOnProgress(downloadId, (data) => {
|
||||||
|
section.count = data.count;
|
||||||
|
section.total = data.total;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown.
|
||||||
|
*
|
||||||
|
* @param {any[]} sections Sections to calculate their status.
|
||||||
|
* @param {number} courseId Course ID the sections belong to.
|
||||||
|
* @param {boolean} [refresh] True if it shouldn't use module status cache (slower).
|
||||||
|
* @return {Promise<void>} Promise resolved when the states are calculated.
|
||||||
|
*/
|
||||||
|
calculateSectionsStatus(sections: any[], courseId: number, refresh?: boolean) : Promise<void> {
|
||||||
|
let allSectionsSection,
|
||||||
|
allSectionsStatus,
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
sections.forEach((section) => {
|
||||||
|
if (section.id === CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
// "All sections" section status is calculated using the status of the rest of sections.
|
||||||
|
allSectionsSection = section;
|
||||||
|
section.isCalculating = true;
|
||||||
|
} else {
|
||||||
|
section.isCalculating = true;
|
||||||
|
promises.push(this.calculateSectionStatus(section, courseId, refresh).then((result) => {
|
||||||
|
// Calculate "All sections" status.
|
||||||
|
allSectionsStatus = this.filepoolProvider.determinePackagesStatus(allSectionsStatus, result.status);
|
||||||
|
}).finally(() => {
|
||||||
|
section.isCalculating = false;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
if (allSectionsSection) {
|
||||||
|
// Set "All sections" data.
|
||||||
|
allSectionsSection.showDownload = allSectionsStatus === CoreConstants.NOT_DOWNLOADED;
|
||||||
|
allSectionsSection.showRefresh = allSectionsStatus === CoreConstants.OUTDATED;
|
||||||
|
allSectionsSection.isDownloading = allSectionsStatus === CoreConstants.DOWNLOADING;
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
if (allSectionsSection) {
|
||||||
|
allSectionsSection.isCalculating = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a confirm and prefetch a course. It will retrieve the sections and the course options if not provided.
|
||||||
|
* This function will set the icon to "spinner" when starting and it will also set it back to the initial icon if the
|
||||||
|
* user cancels. All the other updates of the icon should be made when CoreEventsProvider.COURSE_STATUS_CHANGED is received.
|
||||||
|
*
|
||||||
|
* @param {any} iconData An object where to store the course icon. It will be stored with the name "prefetchCourseIcon".
|
||||||
|
* @param {any} course Course to prefetch.
|
||||||
|
* @param {any[]} [sections] List of course sections.
|
||||||
|
* @param {CoreCoursesHandlerToDisplay[]} courseHandlers List of course handlers.
|
||||||
|
* @return {Promise<boolean>} Promise resolved with true when the download finishes, resolved with false if user doesn't
|
||||||
|
* confirm, rejected if an error occurs.
|
||||||
|
*/
|
||||||
|
confirmAndPrefetchCourse(iconData: any, course: any, sections?: any[], courseHandlers?: CoreCoursesHandlerToDisplay[])
|
||||||
|
: Promise<boolean> {
|
||||||
|
let initialIcon = iconData.prefetchCourseIcon,
|
||||||
|
promise,
|
||||||
|
siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
iconData.prefetchCourseIcon = 'spinner';
|
||||||
|
|
||||||
|
// Get the sections first if needed.
|
||||||
|
if (sections) {
|
||||||
|
promise = Promise.resolve(sections);
|
||||||
|
} else {
|
||||||
|
promise = this.courseProvider.getSections(course.id, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then((sections) => {
|
||||||
|
// Confirm the download.
|
||||||
|
return this.confirmDownloadSizeSection(course.id, undefined, sections, true).then(() => {
|
||||||
|
// User confirmed, get the course handlers if needed.
|
||||||
|
if (courseHandlers) {
|
||||||
|
promise = Promise.resolve(courseHandlers);
|
||||||
|
} else {
|
||||||
|
promise = this.coursesDelegate.getHandlersToDisplay(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then((handlers: CoreCoursesHandlerToDisplay[]) => {
|
||||||
|
// Now we have all the data, download the course.
|
||||||
|
return this.prefetchCourse(course, sections, handlers, siteId);
|
||||||
|
}).then(() => {
|
||||||
|
// Download successful.
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, (error) : any => {
|
||||||
|
// User cancelled or there was an error calculating the size.
|
||||||
|
iconData.prefetchCourseIcon = initialIcon;
|
||||||
|
if (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm and prefetches a list of courses.
|
||||||
|
*
|
||||||
|
* @param {Object[]} courses List of courses to download.
|
||||||
|
* @return {Promise} Promise resolved with true when downloaded, resolved with false if user cancels, rejected if error.
|
||||||
|
* It will send a "progress" everytime a course is downloaded or fails to download.
|
||||||
|
*/
|
||||||
|
confirmAndPrefetchCourses(courses: any[], onProgress?: (data: CoreCourseCoursesProgress) => void) : Promise<boolean> {
|
||||||
|
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
// Confirm the download without checking size because it could take a while.
|
||||||
|
return this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
|
||||||
|
let promises = [],
|
||||||
|
total = courses.length,
|
||||||
|
count = 0;
|
||||||
|
|
||||||
|
courses.forEach((course) => {
|
||||||
|
let subPromises = [],
|
||||||
|
sections,
|
||||||
|
handlers,
|
||||||
|
success = true;
|
||||||
|
|
||||||
|
// Get the sections and the handlers.
|
||||||
|
subPromises.push(this.courseProvider.getSections(course.id, false, true).then((courseSections) => {
|
||||||
|
sections = courseSections;
|
||||||
|
}));
|
||||||
|
subPromises.push(this.coursesDelegate.getHandlersToDisplay(course).then((cHandlers) => {
|
||||||
|
handlers = cHandlers;
|
||||||
|
}));
|
||||||
|
|
||||||
|
promises.push(Promise.all(subPromises).then(() => {
|
||||||
|
return this.prefetchCourse(course, sections, handlers, siteId);
|
||||||
|
}).catch((error) => {
|
||||||
|
success = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
}).finally(() => {
|
||||||
|
// Course downloaded or failed, notify the progress.
|
||||||
|
count++;
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({count: count, total: total, courseId: course.id, success: success});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
// Notify the start of the download.
|
||||||
|
onProgress({count: 0, total: total, success: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises).then(() => {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
// User cancelled.
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show confirmation dialog and then remove a module files.
|
||||||
|
*
|
||||||
|
* @param {any} module Module to remove the files.
|
||||||
|
* @param {number} courseId Course ID the module belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when done.
|
||||||
|
*/
|
||||||
|
confirmAndRemoveFiles(module: any, courseId: number) : Promise<any> {
|
||||||
|
return this.domUtils.showConfirm(this.translate.instant('course.confirmdeletemodulefiles')).then(() => {
|
||||||
|
return this.prefetchDelegate.removeModuleFiles(module, courseId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the size to download a section and show a confirm modal if needed.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @param {any} [section] Section. If not provided, all sections.
|
||||||
|
* @param {any[]} [sections] List of sections. Used when downloading all the sections.
|
||||||
|
* @param {boolean} [alwaysConfirm] True to show a confirm even if the size isn't high, false otherwise.
|
||||||
|
* @return {Promise<any>} Promise resolved if the user confirms or there's no need to confirm.
|
||||||
|
*/
|
||||||
|
confirmDownloadSizeSection(courseId: number, section?: any, sections?: any[], alwaysConfirm?: boolean) : Promise<any> {
|
||||||
|
let sizePromise;
|
||||||
|
|
||||||
|
// Calculate the size of the download.
|
||||||
|
if (section && section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
sizePromise = this.prefetchDelegate.getDownloadSize(section.modules, courseId);
|
||||||
|
} else {
|
||||||
|
let promises = [],
|
||||||
|
results = {
|
||||||
|
size: 0,
|
||||||
|
total: true
|
||||||
|
};
|
||||||
|
|
||||||
|
sections.forEach((s) => {
|
||||||
|
if (s.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
promises.push(this.prefetchDelegate.getDownloadSize(s.modules, courseId).then((sectionSize) => {
|
||||||
|
results.total = results.total && sectionSize.total;
|
||||||
|
results.size += sectionSize.size;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sizePromise = Promise.all(promises).then(() => {
|
||||||
|
return results;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizePromise.then((size) => {
|
||||||
|
// Show confirm modal if needed.
|
||||||
|
return this.domUtils.confirmDownloadSize(size, undefined, undefined, undefined, undefined, alwaysConfirm);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the status of a list of courses.
|
||||||
|
*
|
||||||
|
* @param {any[]} courses Courses
|
||||||
|
* @return {Promise<string>} Promise resolved with the status.
|
||||||
|
*/
|
||||||
|
determineCoursesStatus(courses: any[]) : Promise<string> {
|
||||||
|
// Get the status of each course.
|
||||||
|
const promises = [],
|
||||||
|
siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
courses.forEach((course) => {
|
||||||
|
promises.push(this.courseProvider.getCourseStatus(course.id, siteId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.all(promises).then((statuses) => {
|
||||||
|
// Now determine the status of the whole list.
|
||||||
|
let status = statuses[0];
|
||||||
|
for (let i = 1; i < statuses.length; i++) {
|
||||||
|
status = this.filepoolProvider.determinePackagesStatus(status, statuses[i]);
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a course download promise (if any).
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<any>} Download promise, undefined if not found.
|
||||||
|
*/
|
||||||
|
getCourseDownloadPromise(courseId: number, siteId?: string) : Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
return this.courseDwnPromises[siteId] && this.courseDwnPromises[siteId][courseId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a course status icon.
|
||||||
|
*
|
||||||
|
* @param {number} courseId Course ID.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise<string>} Promise resolved with the icon name.
|
||||||
|
*/
|
||||||
|
getCourseStatusIcon(courseId: number, siteId?: string) : Promise<string> {
|
||||||
|
return this.courseProvider.getCourseStatus(courseId, siteId).then((status) => {
|
||||||
|
return this.getCourseStatusIconFromStatus(status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a course status icon from status.
|
||||||
|
*
|
||||||
|
* @module mm.core.course
|
||||||
|
* @ngdoc method
|
||||||
|
* @name $mmCourseHelper#getCourseStatusIconFromStatus
|
||||||
|
* @param {String} status Course status.
|
||||||
|
* @return {String} Icon name.
|
||||||
|
*/
|
||||||
|
getCourseStatusIconFromStatus(status: string) : string {
|
||||||
|
if (status == CoreConstants.DOWNLOADED) {
|
||||||
|
// Always show refresh icon, we cannot knew if there's anything new in course options.
|
||||||
|
return 'refresh';
|
||||||
|
} else if (status == CoreConstants.DOWNLOADING) {
|
||||||
|
return 'spinner';
|
||||||
|
} else {
|
||||||
|
return 'cloud-download';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the course ID from a module instance ID, showing an error message if it can't be retrieved.
|
* Get the course ID from a module instance ID, showing an error message if it can't be retrieved.
|
||||||
*
|
*
|
||||||
|
@ -82,6 +490,279 @@ export class CoreCourseHelperProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get prefetch info for a module.
|
||||||
|
*
|
||||||
|
* @param {any} module Module to get the info from.
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @param {boolean} [invalidateCache] Invalidates the cache first.
|
||||||
|
* @param {string} [component] Component of the module.
|
||||||
|
* @return {Promise<CoreCourseModulePrefetchInfo>} Promise resolved with the info.
|
||||||
|
*/
|
||||||
|
getModulePrefetchInfo(module: any, courseId: number, invalidateCache?: boolean, component?: string)
|
||||||
|
: Promise<CoreCourseModulePrefetchInfo> {
|
||||||
|
let moduleInfo: CoreCourseModulePrefetchInfo = {},
|
||||||
|
siteId = this.sitesProvider.getCurrentSiteId(),
|
||||||
|
promises = [];
|
||||||
|
|
||||||
|
if (invalidateCache) {
|
||||||
|
this.prefetchDelegate.invalidateModuleStatusCache(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(this.prefetchDelegate.getModuleDownloadedSize(module, courseId).then((moduleSize) => {
|
||||||
|
moduleInfo.size = moduleSize;
|
||||||
|
moduleInfo.sizeReadable = this.textUtils.bytesToSize(moduleSize, 2);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// @todo: Decide what to display instead of timemodified. Last check_updates?
|
||||||
|
// promises.push(this.prefetchDelegate.getModuleTimemodified(module, courseId).then(function(moduleModified) {
|
||||||
|
// moduleInfo.timemodified = moduleModified;
|
||||||
|
// if (moduleModified > 0) {
|
||||||
|
// var now = $mmUtil.timestamp();
|
||||||
|
// if (now - moduleModified < 7 * 86400) {
|
||||||
|
// moduleInfo.timemodifiedReadable = moment(moduleModified * 1000).fromNow();
|
||||||
|
// } else {
|
||||||
|
// moduleInfo.timemodifiedReadable = moment(moduleModified * 1000).calendar();
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// moduleInfo.timemodifiedReadable = "";
|
||||||
|
// }
|
||||||
|
// }));
|
||||||
|
|
||||||
|
promises.push(this.prefetchDelegate.getModuleStatus(module, courseId).then((moduleStatus) => {
|
||||||
|
moduleInfo.status = moduleStatus;
|
||||||
|
switch (moduleStatus) {
|
||||||
|
case CoreConstants.NOT_DOWNLOADED:
|
||||||
|
moduleInfo.statusIcon = 'cloud-download';
|
||||||
|
break;
|
||||||
|
case CoreConstants.DOWNLOADING:
|
||||||
|
moduleInfo.statusIcon = 'spinner';
|
||||||
|
break;
|
||||||
|
case CoreConstants.OUTDATED:
|
||||||
|
moduleInfo.statusIcon = 'ion-android-refresh';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
moduleInfo.statusIcon = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Get the time it was downloaded (if it was downloaded).
|
||||||
|
promises.push(this.filepoolProvider.getPackageData(siteId, component, module.id).then((data) => {
|
||||||
|
if (data && data.downloadTime && (data.status == CoreConstants.OUTDATED || data.status == CoreConstants.DOWNLOADED)) {
|
||||||
|
const now = this.timeUtils.timestamp();
|
||||||
|
moduleInfo.downloadTime = data.downloadTime;
|
||||||
|
if (now - data.downloadTime < 7 * 86400) {
|
||||||
|
moduleInfo.downloadTimeReadable = moment(data.downloadTime * 1000).fromNow();
|
||||||
|
} else {
|
||||||
|
moduleInfo.downloadTimeReadable = moment(data.downloadTime * 1000).calendar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// Not downloaded.
|
||||||
|
moduleInfo.downloadTime = 0;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
return moduleInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the download ID of a section. It's used to interact with CoreCourseModulePrefetchDelegate.
|
||||||
|
*
|
||||||
|
* @param {any} section Section.
|
||||||
|
* @return {string} Section download ID.
|
||||||
|
*/
|
||||||
|
getSectionDownloadId(section: any) : string {
|
||||||
|
return 'Section-' + section.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch all the activities in a course and also the course addons.
|
||||||
|
*
|
||||||
|
* @param {any} course The course to prefetch.
|
||||||
|
* @param {any[]} sections List of course sections.
|
||||||
|
* @param {CoreCoursesHandlerToDisplay[]} courseHandlers List of course handlers.
|
||||||
|
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||||
|
* @return {Promise} Promise resolved when the download finishes.
|
||||||
|
*/
|
||||||
|
prefetchCourse(course: any, sections: any[], courseHandlers: CoreCoursesHandlerToDisplay[], siteId?: string) : Promise<any> {
|
||||||
|
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.courseDwnPromises[siteId] && this.courseDwnPromises[siteId][course.id]) {
|
||||||
|
// There's already a download ongoing for this course, return the promise.
|
||||||
|
return this.courseDwnPromises[siteId][course.id];
|
||||||
|
} else if (!this.courseDwnPromises[siteId]) {
|
||||||
|
this.courseDwnPromises[siteId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// First of all, mark the course as being downloaded.
|
||||||
|
this.courseDwnPromises[siteId][course.id] = this.courseProvider.setCourseStatus(course.id, CoreConstants.DOWNLOADING,
|
||||||
|
siteId).then(() => {
|
||||||
|
let promises = [],
|
||||||
|
allSectionsSection = sections[0];
|
||||||
|
|
||||||
|
// Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections".
|
||||||
|
if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
allSectionsSection = {id: CoreCourseProvider.ALL_SECTIONS_ID};
|
||||||
|
}
|
||||||
|
promises.push(this.prefetchSection(allSectionsSection, course.id, sections));
|
||||||
|
|
||||||
|
// Prefetch course options.
|
||||||
|
courseHandlers.forEach((handler) => {
|
||||||
|
if (handler.prefetch) {
|
||||||
|
promises.push(handler.prefetch(course));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises);
|
||||||
|
}).then(() => {
|
||||||
|
// Download success, mark the course as downloaded.
|
||||||
|
return this.courseProvider.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId);
|
||||||
|
}).catch((error) => {
|
||||||
|
// Error, restore previous status.
|
||||||
|
return this.courseProvider.setCoursePreviousStatus(course.id, siteId).then(() => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
delete this.courseDwnPromises[siteId][course.id];
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.courseDwnPromises[siteId][course.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to prefetch a module, showing a confirmation modal if the size is big
|
||||||
|
* and invalidating contents if refreshing.
|
||||||
|
*
|
||||||
|
* @param {handler} handler Prefetch handler to use. Must implement 'prefetch' and 'invalidateContent'.
|
||||||
|
* @param {any} module Module to download.
|
||||||
|
* @param {any} size Object containing size to download (in bytes) and a boolean to indicate if its totally calculated.
|
||||||
|
* @param {number} courseId Course ID of the module.
|
||||||
|
* @param {boolean} [refresh] True if refreshing, false otherwise.
|
||||||
|
* @return {Promise<any>} Promise resolved when downloaded.
|
||||||
|
*/
|
||||||
|
prefetchModule(handler: any, module: any, size: any, courseId: number, refresh?: boolean) : Promise<any> {
|
||||||
|
// Show confirmation if needed.
|
||||||
|
return this.domUtils.confirmDownloadSize(size).then(() => {
|
||||||
|
// Invalidate content if refreshing and download the data.
|
||||||
|
let promise = refresh ? handler.invalidateContent(module.id, courseId) : Promise.resolve();
|
||||||
|
return promise.catch(() => {
|
||||||
|
// Ignore errors.
|
||||||
|
}).then(() => {
|
||||||
|
return handler.prefetch(module, courseId, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch one section or all the sections.
|
||||||
|
* If the section is "All sections" it will prefetch all the sections.
|
||||||
|
*
|
||||||
|
* @param {any} section Section.
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @param {any[]} [sections] List of sections. Used when downloading all the sections.
|
||||||
|
* @return {Promise<any>} Promise resolved when the prefetch is finished.
|
||||||
|
*/
|
||||||
|
prefetchSection(section: any, courseId: number, sections?: any[]) : Promise<any> {
|
||||||
|
if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
// Download only this section.
|
||||||
|
return this.prefetchSingleSectionIfNeeded(section, courseId).then(() => {
|
||||||
|
// Calculate the status of the section that finished.
|
||||||
|
return this.calculateSectionStatus(section, courseId);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Download all the sections except "All sections".
|
||||||
|
let promises = [],
|
||||||
|
allSectionsStatus;
|
||||||
|
|
||||||
|
section.isDownloading = true;
|
||||||
|
sections.forEach((section) => {
|
||||||
|
if (section.id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
promises.push(this.prefetchSingleSectionIfNeeded(section, courseId).then(() => {
|
||||||
|
// Calculate the status of the section that finished.
|
||||||
|
return this.calculateSectionStatus(section, courseId).then((result) => {
|
||||||
|
// Calculate "All sections" status.
|
||||||
|
allSectionsStatus = this.filepoolProvider.determinePackagesStatus(allSectionsStatus, result.status);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.utils.allPromises(promises).then(() => {
|
||||||
|
// Set "All sections" data.
|
||||||
|
section.showDownload = allSectionsStatus === CoreConstants.NOT_DOWNLOADED;
|
||||||
|
section.showRefresh = allSectionsStatus === CoreConstants.OUTDATED;
|
||||||
|
section.isDownloading = allSectionsStatus === CoreConstants.DOWNLOADING;
|
||||||
|
}).finally(() => {
|
||||||
|
section.isDownloading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a certain section if it needs to be prefetched.
|
||||||
|
* If the section is "All sections" it will be ignored.
|
||||||
|
*
|
||||||
|
* @param {any} section Section to prefetch.
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the section is prefetched.
|
||||||
|
*/
|
||||||
|
protected prefetchSingleSectionIfNeeded(section: any, courseId: number) : Promise<any> {
|
||||||
|
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
section.isDownloading = true;
|
||||||
|
|
||||||
|
// Validate the section needs to be downloaded and calculate amount of modules that need to be downloaded.
|
||||||
|
return this.prefetchDelegate.getModulesStatus(section.modules, courseId, section.id).then((result) => {
|
||||||
|
if (result.status == CoreConstants.DOWNLOADED || result.status == CoreConstants.NOT_DOWNLOADABLE) {
|
||||||
|
// Section is downloaded or not downloadable, nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return this.prefetchSingleSection(section, result, courseId);
|
||||||
|
}, (error) => {
|
||||||
|
section.isDownloading = false;
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start or restore the prefetch of a section.
|
||||||
|
* If the section is "All sections" it will be ignored.
|
||||||
|
*
|
||||||
|
* @param {any} section Section to download.
|
||||||
|
* @param {any} result Result of CoreCourseModulePrefetchDelegate.getModulesStatus for this section.
|
||||||
|
* @param {number} courseId Course ID the section belongs to.
|
||||||
|
* @return {Promise<any>} Promise resolved when the section has been prefetched.
|
||||||
|
*/
|
||||||
|
protected prefetchSingleSection(section: any, result: any, courseId: number) {
|
||||||
|
if (section.id == CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.total > 0) {
|
||||||
|
// Already being downloaded.
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only download modules with status notdownloaded, downloading or outdated.
|
||||||
|
let modules = result[CoreConstants.OUTDATED].concat(result[CoreConstants.NOT_DOWNLOADED])
|
||||||
|
.concat(result[CoreConstants.DOWNLOADING]),
|
||||||
|
downloadId = this.getSectionDownloadId(section);
|
||||||
|
|
||||||
|
section.isDownloading = true;
|
||||||
|
|
||||||
|
// We prefetch all the modules to prevent incoeherences in the download count
|
||||||
|
// and also to download stale data that might not be marked as outdated.
|
||||||
|
return this.prefetchDelegate.prefetchModules(downloadId, modules, courseId, (data) => {
|
||||||
|
section.count = data.count;
|
||||||
|
section.total = data.total;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a section has content.
|
* Check if a section has content.
|
||||||
*
|
*
|
||||||
|
|
|
@ -26,6 +26,31 @@ import { CoreCache } from '../../../classes/cache';
|
||||||
import { CoreSiteWSPreSets } from '../../../classes/site';
|
import { CoreSiteWSPreSets } from '../../../classes/site';
|
||||||
import { CoreConstants } from '../../constants';
|
import { CoreConstants } from '../../constants';
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
|
import { Subject, BehaviorSubject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress of downloading a list of modules.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModulesProgress = {
|
||||||
|
/**
|
||||||
|
* Number of modules downloaded so far.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
count: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toal of modules to download.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress function for downloading a list of modules.
|
||||||
|
*
|
||||||
|
* @param {CoreCourseModulesProgress} data Progress data.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModulesProgressFunction = (data: CoreCourseModulesProgress) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface that all course prefetch handlers must implement.
|
* Interface that all course prefetch handlers must implement.
|
||||||
|
@ -203,9 +228,16 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
protected statusCache = new CoreCache();
|
protected statusCache = new CoreCache();
|
||||||
protected lastUpdateHandlersStart: number;
|
protected lastUpdateHandlersStart: number;
|
||||||
|
|
||||||
// Promises for check updates and for prefetch.
|
// Promises for check updates, to prevent performing the same request twice at the same time.
|
||||||
protected courseUpdatesPromises: {[s: string]: {[s: string]: Promise<any>}} = {};
|
protected courseUpdatesPromises: {[s: string]: {[s: string]: Promise<any>}} = {};
|
||||||
protected prefetchPromises: {[s: string]: {[s: string]: Promise<any>}} = {};
|
|
||||||
|
// Promises and observables for prefetching, to prevent downloading the same section twice at the same time
|
||||||
|
// and notify the progress of the download.
|
||||||
|
protected prefetchData: {[s: string]: {[s: string]: {
|
||||||
|
promise: Promise<any>,
|
||||||
|
observable: Subject<CoreCourseModulesProgress>,
|
||||||
|
subscriptions: Subscription[]
|
||||||
|
}}} = {};
|
||||||
|
|
||||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider,
|
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider,
|
||||||
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider,
|
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider,
|
||||||
|
@ -306,16 +338,15 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
*
|
*
|
||||||
* @param {any} module Module.
|
* @param {any} module Module.
|
||||||
* @param {string} status Current status.
|
* @param {string} status Current status.
|
||||||
* @param {boolean} [restoreDownloads] True if it should restore downloads if needed.
|
|
||||||
* @param {boolean} [canCheck] True if updates can be checked using core_course_check_updates.
|
* @param {boolean} [canCheck] True if updates can be checked using core_course_check_updates.
|
||||||
* @return {string} Module status.
|
* @return {string} Module status.
|
||||||
*/
|
*/
|
||||||
determineModuleStatus(module: any, status: string, restoreDownloads?: boolean, canCheck?: boolean) : string {
|
determineModuleStatus(module: any, status: string, canCheck?: boolean) : string {
|
||||||
const handler = this.getPrefetchHandlerFor(module),
|
const handler = this.getPrefetchHandlerFor(module),
|
||||||
siteId = this.sitesProvider.getCurrentSiteId();
|
siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
|
|
||||||
if (handler) {
|
if (handler) {
|
||||||
if (status == CoreConstants.DOWNLOADING && restoreDownloads) {
|
if (status == CoreConstants.DOWNLOADING) {
|
||||||
// Check if the download is being handled.
|
// Check if the download is being handled.
|
||||||
if (!this.filepoolProvider.getPackageDownloadPromise(siteId, handler.component, module.id)) {
|
if (!this.filepoolProvider.getPackageDownloadPromise(siteId, handler.component, module.id)) {
|
||||||
// Not handled, the app was probably restarted or something weird happened.
|
// Not handled, the app was probably restarted or something weird happened.
|
||||||
|
@ -624,13 +655,10 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
* @param {any} [updates] Result of getCourseUpdates for all modules in the course. If not provided, it will be
|
* @param {any} [updates] Result of getCourseUpdates for all modules in the course. If not provided, it will be
|
||||||
* calculated (slower). If it's false it means the site doesn't support check updates.
|
* calculated (slower). If it's false it means the site doesn't support check updates.
|
||||||
* @param {boolean} [refresh] True if it should ignore the cache.
|
* @param {boolean} [refresh] True if it should ignore the cache.
|
||||||
* @param {Boolean} [restoreDownloads] True if it should restore downloads. It's only used if refresh=false,
|
|
||||||
* if refresh=true then it always tries to restore downloads.
|
|
||||||
* @param {number} [sectionId] ID of the section the module belongs to.
|
* @param {number} [sectionId] ID of the section the module belongs to.
|
||||||
* @return {Promise<string>} Promise resolved with the status.
|
* @return {Promise<string>} Promise resolved with the status.
|
||||||
*/
|
*/
|
||||||
getModuleStatus(module: any, courseId: number, updates?: any, refresh?: boolean, restoreDownloads?: boolean, sectionId?: number)
|
getModuleStatus(module: any, courseId: number, updates?: any, refresh?: boolean, sectionId?: number) : Promise<string> {
|
||||||
: Promise<string> {
|
|
||||||
let handler = this.getPrefetchHandlerFor(module),
|
let handler = this.getPrefetchHandlerFor(module),
|
||||||
siteId = this.sitesProvider.getCurrentSiteId(),
|
siteId = this.sitesProvider.getCurrentSiteId(),
|
||||||
canCheck = this.canCheckUpdates();
|
canCheck = this.canCheckUpdates();
|
||||||
|
@ -644,7 +672,7 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
promise;
|
promise;
|
||||||
|
|
||||||
if (!refresh && typeof status != 'undefined') {
|
if (!refresh && typeof status != 'undefined') {
|
||||||
return Promise.resolve(this.determineModuleStatus(module, status, restoreDownloads, canCheck));
|
return Promise.resolve(this.determineModuleStatus(module, status, canCheck));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the module is downloadable.
|
// Check if the module is downloadable.
|
||||||
|
@ -694,7 +722,7 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Error checking if module has updates.
|
// Error checking if module has updates.
|
||||||
const status = this.statusCache.getValue(packageId, 'status', true);
|
const status = this.statusCache.getValue(packageId, 'status', true);
|
||||||
return this.determineModuleStatus(module, status, restoreDownloads, canCheck);
|
return this.determineModuleStatus(module, status, canCheck);
|
||||||
});
|
});
|
||||||
}, () => {
|
}, () => {
|
||||||
// Error getting updates, show the stored status.
|
// Error getting updates, show the stored status.
|
||||||
|
@ -704,9 +732,9 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
});
|
});
|
||||||
}).then((status) => {
|
}).then((status) => {
|
||||||
if (updateStatus) {
|
if (updateStatus) {
|
||||||
this.updateStatusCache(component, module.id, status, sectionId);
|
this.updateStatusCache(status, courseId, component, module.id, sectionId);
|
||||||
}
|
}
|
||||||
return this.determineModuleStatus(module, status, restoreDownloads, canCheck);
|
return this.determineModuleStatus(module, status, canCheck);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -722,8 +750,6 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
* @param {number} courseId Course ID the modules belong to.
|
* @param {number} courseId Course ID the modules belong to.
|
||||||
* @param {number} [sectionId] ID of the section the modules belong to.
|
* @param {number} [sectionId] ID of the section the modules belong to.
|
||||||
* @param {boolean} [refresh] True if it should always check the DB (slower).
|
* @param {boolean} [refresh] True if it should always check the DB (slower).
|
||||||
* @param {Boolean} [restoreDownloads] True if it should restore downloads. It's only used if refresh=false,
|
|
||||||
* if refresh=true then it always tries to restore downloads.
|
|
||||||
* @return {Promise<any>} Promise resolved with an object with the following properties:
|
* @return {Promise<any>} Promise resolved with an object with the following properties:
|
||||||
* - status (string) Status of the module.
|
* - status (string) Status of the module.
|
||||||
* - total (number) Number of modules.
|
* - total (number) Number of modules.
|
||||||
|
@ -732,7 +758,7 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
* - CoreConstants.DOWNLOADING (any[]) Modules with state DOWNLOADING.
|
* - CoreConstants.DOWNLOADING (any[]) Modules with state DOWNLOADING.
|
||||||
* - CoreConstants.OUTDATED (any[]) Modules with state OUTDATED.
|
* - CoreConstants.OUTDATED (any[]) Modules with state OUTDATED.
|
||||||
*/
|
*/
|
||||||
getModulesStatus(modules: any[], courseId: number, sectionId?: number, refresh?: boolean, restoreDownloads?: boolean) : any {
|
getModulesStatus(modules: any[], courseId: number, sectionId?: number, refresh?: boolean) : any {
|
||||||
let promises = [],
|
let promises = [],
|
||||||
status = CoreConstants.NOT_DOWNLOADABLE,
|
status = CoreConstants.NOT_DOWNLOADABLE,
|
||||||
result: any = {
|
result: any = {
|
||||||
|
@ -757,7 +783,7 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
if (handler) {
|
if (handler) {
|
||||||
let packageId = this.filepoolProvider.getPackageId(handler.component, module.id);
|
let packageId = this.filepoolProvider.getPackageId(handler.component, module.id);
|
||||||
|
|
||||||
promises.push(this.getModuleStatus(module, courseId, updates, refresh, restoreDownloads).then((modStatus) => {
|
promises.push(this.getModuleStatus(module, courseId, updates, refresh).then((modStatus) => {
|
||||||
if (modStatus != CoreConstants.NOT_DOWNLOADABLE) {
|
if (modStatus != CoreConstants.NOT_DOWNLOADABLE) {
|
||||||
if (sectionId && sectionId > 0) {
|
if (sectionId && sectionId > 0) {
|
||||||
// Store the section ID.
|
// Store the section ID.
|
||||||
|
@ -906,7 +932,7 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
*/
|
*/
|
||||||
isBeingDownloaded(id: string) : boolean {
|
isBeingDownloaded(id: string) : boolean {
|
||||||
const siteId = this.sitesProvider.getCurrentSiteId();
|
const siteId = this.sitesProvider.getCurrentSiteId();
|
||||||
return !!(this.prefetchPromises[siteId] && this.prefetchPromises[siteId][id]);
|
return !!(this.prefetchData[siteId] && this.prefetchData[siteId][id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1017,19 +1043,39 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
* @param {string} id An ID to identify the download. It can be used to retrieve the download promise.
|
* @param {string} id An ID to identify the download. It can be used to retrieve the download promise.
|
||||||
* @param {any[]} modules List of modules to prefetch.
|
* @param {any[]} modules List of modules to prefetch.
|
||||||
* @param {number} courseId Course ID the modules belong to.
|
* @param {number} courseId Course ID the modules belong to.
|
||||||
* @param {Function} [onProgress] Function to call everytime a module is downloaded.
|
* @param {CoreCourseModulesProgressFunction} [onProgress] Function to call everytime a module is downloaded.
|
||||||
* @return {Promise<any>} Promise resolved when all modules have been prefetched.
|
* @return {Promise<any>} Promise resolved when all modules have been prefetched.
|
||||||
*/
|
*/
|
||||||
prefetchModules(id: string, modules: any[], courseId: number, onProgress?: (moduleId: number) => any) : Promise<any> {
|
prefetchModules(id: string, modules: any[], courseId: number, onProgress?: CoreCourseModulesProgressFunction) : Promise<any> {
|
||||||
|
|
||||||
const siteId = this.sitesProvider.getCurrentSiteId();
|
const siteId = this.sitesProvider.getCurrentSiteId(),
|
||||||
|
currentData = this.prefetchData[siteId] && this.prefetchData[siteId][id];
|
||||||
|
|
||||||
if (this.prefetchPromises[siteId] && this.prefetchPromises[siteId][id]) {
|
if (currentData) {
|
||||||
// There's a prefetch ongoing, return the current promise.
|
// There's a prefetch ongoing, return the current promise.
|
||||||
return this.prefetchPromises[siteId][id];
|
if (onProgress) {
|
||||||
|
currentData.subscriptions.push(currentData.observable.subscribe(onProgress));
|
||||||
|
}
|
||||||
|
return currentData.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
let promises = [];
|
let promises = [],
|
||||||
|
count = 0,
|
||||||
|
total = modules.length,
|
||||||
|
moduleIds = modules.map((module) => {
|
||||||
|
return module.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the prefetch data.
|
||||||
|
const prefetchData = {
|
||||||
|
observable: new BehaviorSubject<CoreCourseModulesProgress>({count: count, total: total}),
|
||||||
|
promise: undefined,
|
||||||
|
subscriptions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (onProgress) {
|
||||||
|
prefetchData.observable.subscribe(onProgress);
|
||||||
|
}
|
||||||
|
|
||||||
modules.forEach((module) => {
|
modules.forEach((module) => {
|
||||||
// Check if the module has a prefetch handler.
|
// Check if the module has a prefetch handler.
|
||||||
|
@ -1041,23 +1087,34 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.prefetch(module, courseId).then(() => {
|
return handler.prefetch(module, courseId).then(() => {
|
||||||
if (onProgress) {
|
let index = moduleIds.indexOf(id);
|
||||||
onProgress(module.id);
|
if (index > -1) {
|
||||||
|
// It's one of the modules we were expecting to download.
|
||||||
|
moduleIds.splice(index, 1);
|
||||||
|
count++;
|
||||||
|
prefetchData.observable.next({count: count, total: total});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.prefetchPromises[siteId]) {
|
// Set the promise.
|
||||||
this.prefetchPromises[siteId] = {};
|
prefetchData.promise = Promise.all(promises).finally(() => {
|
||||||
}
|
// Unsubscribe all observers.
|
||||||
|
prefetchData.subscriptions.forEach((subscription: Subscription) => {
|
||||||
this.prefetchPromises[siteId][id] = Promise.all(promises).finally(() => {
|
subscription.unsubscribe();
|
||||||
delete this.prefetchPromises[siteId][id];
|
});
|
||||||
|
delete this.prefetchData[siteId][id];
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.prefetchPromises[siteId][id];
|
// Store the prefetch data in the list.
|
||||||
|
if (!this.prefetchData[siteId]) {
|
||||||
|
this.prefetchData[siteId] = {};
|
||||||
|
}
|
||||||
|
this.prefetchData[siteId][id] = prefetchData;
|
||||||
|
|
||||||
|
return prefetchData.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1115,6 +1172,22 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an on progress function for the download of a list of modules.
|
||||||
|
*
|
||||||
|
* @param {string} id An ID to identify the download.
|
||||||
|
* @param {CoreCourseModulesProgressFunction} onProgress Function to call everytime a module is downloaded.
|
||||||
|
*/
|
||||||
|
setOnProgress(id: string, onProgress: CoreCourseModulesProgressFunction) : void {
|
||||||
|
const siteId = this.sitesProvider.getCurrentSiteId(),
|
||||||
|
currentData = this.prefetchData[siteId] && this.prefetchData[siteId][id];
|
||||||
|
|
||||||
|
if (currentData) {
|
||||||
|
// There's a prefetch ongoing, return the current promise.
|
||||||
|
currentData.subscriptions.push(currentData.observable.subscribe(onProgress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Treat the result of the check updates WS call.
|
* Treat the result of the check updates WS call.
|
||||||
*
|
*
|
||||||
|
@ -1206,10 +1279,13 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
/**
|
/**
|
||||||
* Update the status of a module in the "cache".
|
* Update the status of a module in the "cache".
|
||||||
*
|
*
|
||||||
|
* @param {string} status New status.
|
||||||
|
* @param {number} courseId Course ID of the module.
|
||||||
* @param {string} component Package's component.
|
* @param {string} component Package's component.
|
||||||
* @param {string|number} [componentId] An ID to use in conjunction with the component.
|
* @param {string|number} [componentId] An ID to use in conjunction with the component.
|
||||||
|
* @param {number} [sectionId] Section ID of the module.
|
||||||
*/
|
*/
|
||||||
updateStatusCache(component: string, componentId: string|number, status: string, sectionId?: number) : void {
|
updateStatusCache(status: string, courseId: number, component: string, componentId?: string|number, sectionId?: number) : void {
|
||||||
let notify,
|
let notify,
|
||||||
packageId = this.filepoolProvider.getPackageId(component, componentId),
|
packageId = this.filepoolProvider.getPackageId(component, componentId),
|
||||||
cachedStatus = this.statusCache.getValue(packageId, 'status', true);
|
cachedStatus = this.statusCache.getValue(packageId, 'status', true);
|
||||||
|
@ -1230,7 +1306,8 @@ export class CoreCourseModulePrefetchDelegate {
|
||||||
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
this.statusCache.setValue(packageId, 'sectionId', sectionId);
|
||||||
|
|
||||||
this.eventsProvider.trigger(CoreEventsProvider.SECTION_STATUS_CHANGED, {
|
this.eventsProvider.trigger(CoreEventsProvider.SECTION_STATUS_CHANGED, {
|
||||||
sectionId: sectionId
|
sectionId: sectionId,
|
||||||
|
courseId: courseId
|
||||||
}, this.sitesProvider.getCurrentSiteId());
|
}, this.sitesProvider.getCurrentSiteId());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -101,10 +101,11 @@ export class CoreDomUtilsProvider {
|
||||||
* @param {string} [unknownMessage] ID of the message to show if size is unknown.
|
* @param {string} [unknownMessage] ID of the message to show if size is unknown.
|
||||||
* @param {number} [wifiThreshold] Threshold to show confirm in WiFi connection. Default: CoreWifiDownloadThreshold.
|
* @param {number} [wifiThreshold] Threshold to show confirm in WiFi connection. Default: CoreWifiDownloadThreshold.
|
||||||
* @param {number} [limitedThreshold] Threshold to show confirm in limited connection. Default: CoreDownloadThreshold.
|
* @param {number} [limitedThreshold] Threshold to show confirm in limited connection. Default: CoreDownloadThreshold.
|
||||||
|
* @param {boolean} [alwaysConfirm] True to show a confirm even if the size isn't high, false otherwise.
|
||||||
* @return {Promise<void>} Promise resolved when the user confirms or if no confirm needed.
|
* @return {Promise<void>} Promise resolved when the user confirms or if no confirm needed.
|
||||||
*/
|
*/
|
||||||
confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number)
|
confirmDownloadSize(size: any, message?: string, unknownMessage?: string, wifiThreshold?: number, limitedThreshold?: number,
|
||||||
: Promise<void> {
|
alwaysConfirm?: boolean) : Promise<void> {
|
||||||
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
|
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
|
||||||
limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
|
limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
|
||||||
|
|
||||||
|
@ -120,6 +121,8 @@ export class CoreDomUtilsProvider {
|
||||||
message = message || 'core.course.confirmdownload';
|
message = message || 'core.course.confirmdownload';
|
||||||
let readableSize = this.textUtils.bytesToSize(size.size, 2);
|
let readableSize = this.textUtils.bytesToSize(size.size, 2);
|
||||||
return this.showConfirm(this.translate.instant(message, {size: readableSize}));
|
return this.showConfirm(this.translate.instant(message, {size: readableSize}));
|
||||||
|
} else if (alwaysConfirm) {
|
||||||
|
return this.showConfirm(this.translate.instant('core.areyousure'));
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue