MOBILE-3659 course: Implement prefetch delegate

main
Dani Palou 2021-01-20 15:33:41 +01:00
parent d14540218a
commit 031f238117
18 changed files with 2800 additions and 244 deletions

View File

@ -201,6 +201,7 @@ const appConfig = {
'no-duplicate-imports': 'error', 'no-duplicate-imports': 'error',
'no-empty': 'error', 'no-empty': 'error',
'no-eval': 'error', 'no-eval': 'error',
'no-fallthrough': 'off',
'no-invalid-this': 'error', 'no-invalid-this': 'error',
'no-irregular-whitespace': 'error', 'no-irregular-whitespace': 'error',
'no-multiple-empty-lines': 'error', 'no-multiple-empty-lines': 'error',

View File

@ -17,7 +17,7 @@ import { CoreSites } from '@services/sites';
import { CoreCourse, CoreCourseSection } from '@features/course/services/course'; import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome'; import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
// @todo import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
/** /**
@ -63,7 +63,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
if (this.mainMenuBlock && this.mainMenuBlock.modules) { if (this.mainMenuBlock && this.mainMenuBlock.modules) {
// Invalidate modules prefetch data. // Invalidate modules prefetch data.
// @todo promises.push(this.prefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId)); promises.push(CoreCourseModulePrefetchDelegate.instance.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
} }
await Promise.all(promises); await Promise.all(promises);

View File

@ -144,7 +144,7 @@
<ng-container *ngFor="let module of section.modules"> <ng-container *ngFor="let module of section.modules">
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course?.id" <core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course?.id"
[downloadEnabled]="downloadEnabled" [section]="section" (completionChanged)="onCompletionChange($event)" [downloadEnabled]="downloadEnabled" [section]="section" (completionChanged)="onCompletionChange($event)"
(statusChanged)="onModuleStatusChange($event)"> (statusChanged)="onModuleStatusChange()">
</core-course-module> </core-course-module>
</ng-container> </ng-container>
</section> </section>

View File

@ -37,15 +37,13 @@ import {
CoreCourseModuleData, CoreCourseModuleData,
CoreCourseProvider, CoreCourseProvider,
} from '@features/course/services/course'; } from '@features/course/services/course';
// import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseHelper, CoreCourseSectionFormatted, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData, CoreEventSelectCourseTabData } from '@singletons/events'; import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData, CoreEventSelectCourseTabData } from '@singletons/events';
import { IonContent, IonRefresher } from '@ionic/angular'; import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
// import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks'; import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseSectionFormatted } from '@features/course/services/course-helper';
import { CoreCourseModuleStatusChangedData } from '../module/module';
import { ModalController } from '@singletons'; import { ModalController } from '@singletons';
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector'; import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
@ -62,13 +60,14 @@ import { CoreCourseSectionSelectorComponent } from '../section-selector/section-
@Component({ @Component({
selector: 'core-course-format', selector: 'core-course-format',
templateUrl: 'core-course-format.html', templateUrl: 'core-course-format.html',
styleUrls: ['format.scss'],
}) })
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called. static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
@Input() course?: CoreCourseAnyCourseData; // The course to render. @Input() course?: CoreCourseAnyCourseData; // The course to render.
@Input() sections?: CoreCourseSectionFormatted[]; // List of course sections. @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
@Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionId?: number; // The section to load first (by ID).
@Input() initialSectionNumber?: number; // The section to load first (by number). @Input() initialSectionNumber?: number; // The section to load first (by number).
@ -125,26 +124,26 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
return; return;
} }
// @todo Check if the affected section is being downloaded. // 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. // If so, we don't update section status because it'll already be updated when the download finishes.
// const downloadId = CoreCourseHelper.instance.getSectionDownloadId({ id: data.sectionId }); const downloadId = CoreCourseHelper.instance.getSectionDownloadId({ id: data.sectionId });
// if (prefetchDelegate.isBeingDownloaded(downloadId)) { if (CoreCourseModulePrefetchDelegate.instance.isBeingDownloaded(downloadId)) {
// return; return;
// } }
// Get the affected section. // Get the affected section.
// const section = this.sections.find(section => section.id == data.sectionId); const section = this.sections.find(section => section.id == data.sectionId);
// if (!section) { if (!section) {
// return; return;
// } }
// Recalculate the status. // Recalculate the status.
// await CoreCourseHelper.instance.calculateSectionStatus(section, this.course.id, false); await CoreCourseHelper.instance.calculateSectionStatus(section, this.course.id, false);
// if (section.isDownloading && !prefetchDelegate.isBeingDownloaded(downloadId)) { if (section.isDownloading && !CoreCourseModulePrefetchDelegate.instance.isBeingDownloaded(downloadId)) {
// // All the modules are now downloading, set a download all promise. // All the modules are now downloading, set a download all promise.
// this.prefetch(section); this.prefetch(section);
// } }
}, },
CoreSites.instance.getCurrentSiteId(), CoreSites.instance.getCurrentSiteId(),
); );
@ -392,7 +391,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
this.canLoadMore = false; this.canLoadMore = false;
this.showSectionId = 0; this.showSectionId = 0;
this.showMoreActivities(); this.showMoreActivities();
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false); if (this.downloadEnabled) {
this.calculateSectionsStatus(false);
}
} }
if (this.moduleId && typeof previousValue == 'undefined') { if (this.moduleId && typeof previousValue == 'undefined') {
@ -427,11 +428,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* *
* @param refresh If refresh or not. * @param refresh If refresh or not.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected calculateSectionsStatus(refresh?: boolean): void { protected calculateSectionsStatus(refresh?: boolean): void {
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, refresh).catch(() => { if (!this.sections || !this.course) {
// // Ignore errors (shouldn't happen). return;
// }); }
CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, refresh));
} }
/** /**
@ -440,38 +442,42 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
* @param section Section to download. * @param section Section to download.
* @param refresh Refresh clicked (not used). * @param refresh Refresh clicked (not used).
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars async prefetch(section: CoreCourseSectionWithStatus): Promise<void> {
prefetch(section: CoreCourseSectionFormatted): void { section.isCalculating = true;
// section.isCalculating = true;
// @todo CoreCourseHelper.instance.confirmDownloadSizeSection(this.course.id, section, this.sections).then(() => { try {
// this.prefetchSection(section, true); await CoreCourseHelper.instance.confirmDownloadSizeSection(this.course!.id, section, this.sections);
// }, (error) => {
// // User cancelled or there was an error calculating the size. await this.prefetchSection(section, true);
// if (error) { } catch (error) {
// CoreDomUtils.instance.showErrorModal(error); // User cancelled or there was an error calculating the size.
// } if (error) {
// }).finally(() => { CoreDomUtils.instance.showErrorModal(error);
// section.isCalculating = false; }
// }); } finally {
section.isCalculating = false;
}
} }
/** /**
* Prefetch a section. @todo * Prefetch a section.
* *
* @param section The section to download. * @param section The section to download.
* @param manual Whether the prefetch was started manually or it was automatically started because all modules * @param manual Whether the prefetch was started manually or it was automatically started because all modules
* are being downloaded. * are being downloaded.
*/ */
// protected prefetchSection(section: Section, manual?: boolean): void { protected async prefetchSection(section: CoreCourseSectionWithStatus, manual?: boolean): Promise<void> {
// CoreCourseHelper.instance.prefetchSection(section, this.course.id, this.sections).catch((error) => { try {
// // Don't show error message if it's an automatic download. await CoreCourseHelper.instance.prefetchSection(section, this.course!.id, this.sections);
// if (!manual) { } catch (error) {
// return; // Don't show error message if it's an automatic download.
// } if (!manual) {
return;
}
// CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingsection', true); CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
// }); }
// } }
/** /**
* Refresh the data. * Refresh the data.
@ -550,14 +556,16 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
component.callComponentFunction('ionViewDidEnter'); component.callComponentFunction('ionViewDidEnter');
}); });
// @todo if (this.downloadEnabled) { if (!this.downloadEnabled || !this.course || !this.sections) {
// // The download status of a section might have been changed from within a module page. return;
// if (this.selectedSection && this.selectedSection.id !== CoreCourseProvider.ALL_SECTIONS_ID) { }
// CoreCourseHelper.instance.calculateSectionStatus(this.selectedSection, this.course.id, false, false);
// } else { // The download status of a section might have been changed from within a module page.
// CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false); if (this.selectedSection && this.selectedSection.id !== CoreCourseProvider.ALL_SECTIONS_ID) {
// } CoreCourseHelper.instance.calculateSectionStatus(this.selectedSection, this.course.id, false, false);
// } } else {
CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
}
} }
/** /**
@ -609,12 +617,13 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
/** /**
* Recalculate the download status of each section, in response to a module being downloaded. * Recalculate the download status of each section, in response to a module being downloaded.
*
* @param eventData
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars onModuleStatusChange(): void {
onModuleStatusChange(eventData: CoreCourseModuleStatusChangedData): void { if (!this.downloadEnabled || !this.sections || !this.course) {
// @todo CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false); return;
}
CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
} }
} }

View File

@ -14,13 +14,20 @@
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
// import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
// import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
// import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
import { CoreCourseModuleDataFormatted, CoreCourseSectionFormatted } from '@features/course/services/course-helper'; import {
CoreCourseHelper,
CoreCourseModuleDataFormatted,
CoreCourseSectionFormatted,
} from '@features/course/services/course-helper';
import { CoreCourse, CoreCourseModuleCompletionData } from '@features/course/services/course'; import { CoreCourse, CoreCourseModuleCompletionData } from '@features/course/services/course';
import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
// import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '../../providers/module-prefetch-delegate'; import {
CoreCourseModulePrefetchDelegate,
CoreCourseModulePrefetchHandler,
} from '@features/course/services/module-prefetch-delegate';
/** /**
* Component to display a module entry in a list of modules. * Component to display a module entry in a list of modules.
@ -32,6 +39,7 @@ import { CoreCourseModuleHandlerButton } from '@features/course/services/module-
@Component({ @Component({
selector: 'core-course-module', selector: 'core-course-module',
templateUrl: 'core-course-module.html', templateUrl: 'core-course-module.html',
styleUrls: ['module.scss'],
}) })
export class CoreCourseModuleComponent implements OnInit, OnDestroy { export class CoreCourseModuleComponent implements OnInit, OnDestroy {
@ -51,7 +59,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
this.spinner = true; // Show spinner while calculating the status. this.spinner = true; // Show spinner while calculating the status.
// Get current status to decide which icon should be shown. // Get current status to decide which icon should be shown.
// @todo this.prefetchDelegate.getModuleStatus(this.module, this.courseId).then(this.showStatus.bind(this)); this.calculateAndShowStatus();
}; };
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes. @Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
@ -63,8 +71,8 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
modNameTranslated = ''; modNameTranslated = '';
// protected prefetchHandler: CoreCourseModulePrefetchHandler; protected prefetchHandler?: CoreCourseModulePrefetchHandler;
// protected statusObserver?: CoreEventObserver; protected statusObserver?: CoreEventObserver;
protected statusCalculated = false; protected statusCalculated = false;
protected isDestroyed = false; protected isDestroyed = false;
@ -86,26 +94,27 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
if (this.module.handlerData.showDownloadButton) { if (this.module.handlerData.showDownloadButton) {
// @todo Listen for changes on this module status, even if download isn't enabled. // Listen for changes on this module status, even if download isn't enabled.
// this.prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(this.module); this.prefetchHandler = CoreCourseModulePrefetchDelegate.instance.getPrefetchHandlerFor(this.module);
// this.canCheckUpdates = this.prefetchDelegate.canCheckUpdates(); this.canCheckUpdates = CoreCourseModulePrefetchDelegate.instance.canCheckUpdates();
// this.statusObserver = this.eventsProvider.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => { this.statusObserver = CoreEvents.on<CoreEventPackageStatusChanged>(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
// if (data.componentId === this.module.id && this.prefetchHandler && if (!this.module || data.componentId != this.module.id || !this.prefetchHandler ||
// data.component === this.prefetchHandler.component) { data.component != this.prefetchHandler.component) {
return;
}
// // Call determineModuleStatus to get the right status to display. // Call determineModuleStatus to get the right status to display.
// const status = this.prefetchDelegate.determineModuleStatus(this.module, data.status); const status = CoreCourseModulePrefetchDelegate.instance.determineModuleStatus(this.module, data.status);
// if (this.downloadEnabled) { if (this.downloadEnabled) {
// // Download is enabled, show the status. // Download is enabled, show the status.
// this.showStatus(status); this.showStatus(status);
// } else if (this.module.handlerData.updateStatus) { } else if (this.module.handlerData?.updateStatus) {
// // Download isn't enabled but the handler defines a updateStatus function, call it anyway. // Download isn't enabled but the handler defines a updateStatus function, call it anyway.
// this.module.handlerData.updateStatus(status); this.module.handlerData.updateStatus(status);
// } }
// } }, CoreSites.instance.getCurrentSiteId());
// }, this.sitesProvider.getCurrentSiteId());
} }
} }
@ -138,36 +147,39 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
} }
/** /**
* @todo Download the module. * Download the module.
* *
* @param refresh Whether it's refreshing. * @param refresh Whether it's refreshing.
* @return Promise resolved when done.
*/ */
// download(refresh: boolean): void { async download(refresh: boolean): Promise<void> {
// if (!this.prefetchHandler) { if (!this.prefetchHandler || !this.module) {
// return; return;
// } }
// // Show spinner since this operation might take a while. // Show spinner since this operation might take a while.
// this.spinner = true; this.spinner = true;
// // Get download size to ask for confirm if it's high. try {
// this.prefetchHandler.getDownloadSize(this.module, this.courseId, true).then((size) => { // Get download size to ask for confirm if it's high.
// return this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh); const size = await this.prefetchHandler.getDownloadSize(this.module, this.courseId!, true);
// }).then(() => {
// const eventData = { await CoreCourseHelper.instance.prefetchModule(this.prefetchHandler, this.module, size, this.courseId!, refresh);
// sectionId: this.section.id,
// moduleId: this.module.id, const eventData = {
// courseId: this.courseId sectionId: this.section?.id,
// }; moduleId: this.module.id,
// this.statusChanged.emit(eventData); courseId: this.courseId!,
// }).catch((error) => { };
// // Error, hide spinner. this.statusChanged.emit(eventData);
// this.spinner = false; } catch (error) {
// if (!this.isDestroyed) { // Error, hide spinner.
// this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); this.spinner = false;
// } if (!this.isDestroyed) {
// }); CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
// } }
}
}
/** /**
* Show download buttons according to module status. * Show download buttons according to module status.
@ -185,6 +197,21 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy {
this.module?.handlerData?.updateStatus?.(status); this.module?.handlerData?.updateStatus?.(status);
} }
/**
* Calculate and show module status.
*
* @return Promise resolved when done.
*/
protected async calculateAndShowStatus(): Promise<void> {
if (!this.module || !this.courseId) {
return;
}
const status = await CoreCourseModulePrefetchDelegate.instance.getModuleStatus(this.module, this.courseId);
this.showStatus(status);
}
/** /**
* Component destroyed. * Component destroyed.
*/ */

View File

@ -12,16 +12,19 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreCourseComponentsModule } from './components/components.module'; import { CoreCourseComponentsModule } from './components/components.module';
import { CoreCourseDirectivesModule } from './directives/directives.module';
import { CoreCourseFormatModule } from './format/formats.module'; import { CoreCourseFormatModule } from './format/formats.module';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course'; import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log'; import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch';
import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module'; import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module';
import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate';
const routes: Routes = [ const routes: Routes = [
{ {
@ -43,14 +46,23 @@ const courseIndexRoutes: Routes = [
CoreMainMenuTabRoutingModule.forChild(routes), CoreMainMenuTabRoutingModule.forChild(routes),
CoreCourseFormatModule, CoreCourseFormatModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
CoreCourseDirectivesModule,
], ],
exports: [CoreCourseIndexRoutingModule], exports: [CoreCourseIndexRoutingModule],
providers: [ providers: [
{ {
provide: CORE_SITE_SCHEMAS, provide: CORE_SITE_SCHEMAS,
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA, LOG_SITE_SCHEMA], useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA, LOG_SITE_SCHEMA, PREFETCH_SITE_SCHEMA],
multi: true, multi: true,
}, },
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModulePrefetchDelegate.instance.initialize();
},
},
], ],
}) })
export class CoreCourseModule {} export class CoreCourseModule {}

View File

@ -0,0 +1,28 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { CoreCourseDownloadModuleMainFileDirective } from './download-module-main-file';
@NgModule({
declarations: [
CoreCourseDownloadModuleMainFileDirective,
],
imports: [],
exports: [
CoreCourseDownloadModuleMainFileDirective,
],
})
export class CoreCourseDirectivesModule {}

View File

@ -0,0 +1,85 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '@features/course/services/course';
import { CoreCourseHelper } from '@features/course/services/course-helper';
/**
* Directive to allow downloading and open the main file of a module.
* When the item with this directive is clicked, the module will be downloaded (if needed) and opened.
* This is meant for modules like mod_resource.
*
* This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.
*/
@Directive({
selector: '[core-course-download-module-main-file]',
})
export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
@Input() module?: CoreCourseWSModule; // The module.
@Input() moduleId?: string | number; // The module ID. Required if module is not supplied.
@Input() courseId?: string | number; // The course ID.
@Input() component?: string; // Component to link the file to.
@Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId.
@Input() files?: CoreCourseModuleContentFile[]; // List of files of the module. If not provided, use module.contents.
protected element: HTMLElement;
constructor(element: ElementRef) {
this.element = element.nativeElement;
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.element.addEventListener('click', async (ev: Event) => {
if (!this.module && !this.moduleId) {
return;
}
ev.preventDefault();
ev.stopPropagation();
const modal = await CoreDomUtils.instance.showModalLoading();
const courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
try {
if (!this.module) {
// Try to get the module from cache.
this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
this.module = await CoreCourse.instance.getModule(this.moduleId!, courseId);
}
const componentId = this.componentId || module.id;
await CoreCourseHelper.instance.downloadModuleAndOpenFile(
this.module,
courseId ?? this.module.course!,
this.component,
componentId,
this.files,
);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
} finally {
modal.dismiss();
}
});
}
}

View File

@ -28,20 +28,19 @@ import {
} from '@features/course/services/course'; } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseSectionFormatted, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; import { CoreCourseHelper, CoreCourseSectionFormatted, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
// import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { import {
CoreCourseOptionsDelegate, CoreCourseOptionsDelegate,
CoreCourseOptionsMenuHandlerToDisplay, CoreCourseOptionsMenuHandlerToDisplay,
} from '@features/course/services/course-options-delegate'; } from '@features/course/services/course-options-delegate';
// import { CoreCourseSyncProvider } from '../../providers/sync'; // import { CoreCourseSyncProvider } from '../../providers/sync';
// import { CoreCourseFormatComponent } from '../../components/format/format'; import { CoreCourseFormatComponent } from '../../components/format/format';
import { import {
CoreEvents, CoreEvents,
CoreEventObserver, CoreEventObserver,
CoreEventCourseStatusChanged, CoreEventCourseStatusChanged,
CoreEventCompletionModuleViewedData, CoreEventCompletionModuleViewedData,
} from '@singletons/events'; } from '@singletons/events';
import { Translate } from '@singletons';
import { CoreNavHelper } from '@services/nav-helper'; import { CoreNavHelper } from '@services/nav-helper';
/** /**
@ -54,7 +53,7 @@ import { CoreNavHelper } from '@services/nav-helper';
export class CoreCourseContentsPage implements OnInit, OnDestroy { export class CoreCourseContentsPage implements OnInit, OnDestroy {
@ViewChild(IonContent) content?: IonContent; @ViewChild(IonContent) content?: IonContent;
// @ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent; @ViewChild(CoreCourseFormatComponent) formatComponent?: CoreCourseFormatComponent;
course!: CoreCourseAnyCourseData; course!: CoreCourseAnyCourseData;
sections?: CoreCourseSectionFormatted[]; sections?: CoreCourseSectionFormatted[];
@ -244,9 +243,9 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
if (refresh) { if (refresh) {
// Invalidate the recently downloaded module list. To ensure info can be prefetched. // Invalidate the recently downloaded module list. To ensure info can be prefetched.
// const modules = CoreCourse.instance.getSectionsModules(sections); const modules = CoreCourse.instance.getSectionsModules(sections);
// @todo await this.prefetchDelegate.invalidateModules(modules, this.course.id); await CoreCourseModulePrefetchDelegate.instance.invalidateModules(modules, this.course.id);
} }
let completionStatus: Record<string, CoreCourseCompletionActivityStatus> = {}; let completionStatus: Record<string, CoreCourseCompletionActivityStatus> = {};
@ -279,14 +278,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) { if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) {
// Add a fake first section (all sections). // Add a fake first section (all sections).
this.sections.unshift({ this.sections.unshift(CoreCourseHelper.instance.createAllSectionsSection());
id: CoreCourseProvider.ALL_SECTIONS_ID,
name: Translate.instance.instant('core.course.allsections'),
hasContent: true,
summary: '',
summaryformat: 1,
modules: [],
});
} }
// Get whether to show the refresher now that we have sections. // Get whether to show the refresher now that we have sections.
@ -345,8 +337,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
} finally { } finally {
// Do not call doRefresh on the format component if the refresher is defined in the format component // Do not call doRefresh on the format component if the refresher is defined in the format component
// to prevent an inifinite loop. // to prevent an inifinite loop.
if (this.displayRefresher) { if (this.displayRefresher && this.formatComponent) {
// @todo await CoreUtils.instance.ignoreErrors(this.formatComponent.doRefresh(refresher)); await CoreUtils.instance.ignoreErrors(this.formatComponent.doRefresh(refresher));
} }
refresher?.detail.complete(); refresher?.detail.complete();
@ -384,7 +376,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
promises.push(CoreCourseFormatDelegate.instance.invalidateData(this.course, this.sections || [])); promises.push(CoreCourseFormatDelegate.instance.invalidateData(this.course, this.sections || []));
if (this.sections) { if (this.sections) {
// @todo promises.push(this.prefetchDelegate.invalidateCourseUpdates(this.course.id)); promises.push(CoreCourseModulePrefetchDelegate.instance.invalidateCourseUpdates(this.course.id));
} }
await Promise.all(promises); await Promise.all(promises);
@ -408,7 +400,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
try { try {
await this.loadData(true, sync); await this.loadData(true, sync);
// @todo await this.formatComponent.doRefresh(undefined, undefined, true); await this.formatComponent?.doRefresh(undefined, undefined, true);
} finally { } finally {
this.dataLoaded = true; this.dataLoaded = true;
@ -431,15 +423,15 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
/** /**
* Prefetch the whole course. * Prefetch the whole course.
*/ */
prefetchCourse(): void { async prefetchCourse(): Promise<void> {
try { try {
// @todo await CoreCourseHelper.instance.confirmAndPrefetchCourse( await CoreCourseHelper.instance.confirmAndPrefetchCourse(
// this.prefetchCourseData, this.prefetchCourseData,
// this.course, this.course,
// this.sections, this.sections,
// this.courseHandlers, undefined,
// this.courseMenuHandlers, this.courseMenuHandlers,
// ); );
} catch (error) { } catch (error) {
if (this.isDestroyed) { if (this.isDestroyed) {
return; return;
@ -497,14 +489,14 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* User entered the page. * User entered the page.
*/ */
ionViewDidEnter(): void { ionViewDidEnter(): void {
// @todo this.formatComponent?.ionViewDidEnter(); this.formatComponent?.ionViewDidEnter();
} }
/** /**
* User left the page. * User left the page.
*/ */
ionViewDidLeave(): void { ionViewDidLeave(): void {
// @todo this.formatComponent?.ionViewDidLeave(); this.formatComponent?.ionViewDidLeave();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// 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.
// @todo test delegate
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate'; import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate';
@ -178,7 +177,7 @@ export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay
* @param course The course. * @param course The course.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void>; prefetch?(course: CoreCourseAnyCourseData): Promise<void>;
} }
/** /**
@ -206,7 +205,7 @@ export interface CoreCourseOptionsMenuHandlerToDisplay {
* @param course The course. * @param course The course.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void>; prefetch?(course: CoreCourseAnyCourseData): Promise<void>;
} }
/** /**

View File

@ -109,7 +109,6 @@ export class CoreCourseProvider {
* *
* @param courseId Course ID. * @param courseId Course ID.
* @param completion Completion status of the module. * @param completion Completion status of the module.
* @todo Add completion type.
*/ */
checkModuleCompletion(courseId: number, completion: CoreCourseModuleCompletionDataFormatted): void { checkModuleCompletion(courseId: number, completion: CoreCourseModuleCompletionDataFormatted): void {
if (completion && completion.tracking === 2 && completion.state === 0) { if (completion && completion.tracking === 2 && completion.state === 0) {
@ -830,7 +829,7 @@ export class CoreCourseProvider {
* @return Promise resolved when loaded. * @return Promise resolved when loaded.
*/ */
async loadModuleContents( async loadModuleContents(
module: CoreCourseModuleData & CoreCourseModuleBasicInfo, module: CoreCourseModuleData,
courseId?: number, courseId?: number,
sectionId?: number, sectionId?: number,
preferCache?: boolean, preferCache?: boolean,
@ -1412,14 +1411,13 @@ export type CoreCourseModuleContentFile = {
filename: string; // Filename. filename: string; // Filename.
filepath: string; // Filepath. filepath: string; // Filepath.
filesize: number; // Filesize. filesize: number; // Filesize.
fileurl?: string; // Downloadable file url. fileurl: string; // Downloadable file url.
url?: string; // @deprecated. Use fileurl instead.
content?: string; // Raw content, will be used when type is content. content?: string; // Raw content, will be used when type is content.
timecreated: number; // Time created. timecreated: number; // Time created.
timemodified: number; // Time modified. timemodified: number; // Time modified.
sortorder: number; // Content sort order. sortorder: number; // Content sort order.
mimetype?: string; // File mime type. mimetype?: string; // File mime type.
isexternalfile?: boolean; // Whether is an external file. isexternalfile?: number; // Whether is an external file.
repositorytype?: string; // The repository type for external files. repositorytype?: string; // The repository type for external files.
userid: number; // User who added this content to moodle. userid: number; // User who added this content to moodle.
author: string; // Content owner. author: string; // Content owner.

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreCourseModulePrefetchDelegate service.
*/
export const CHECK_UPDATES_TIMES_TABLE = 'check_updates_times';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreCourseModulePrefetchDelegate',
version: 1,
tables: [
{
name: CHECK_UPDATES_TIMES_TABLE,
columns: [
{
name: 'courseId',
type: 'INTEGER',
primaryKey: true,
},
{
name: 'time',
type: 'INTEGER',
notNull: true,
},
],
},
],
};
export type CoreCourseCheckUpdatesDBRecord = {
courseId: number;
time: number;
};

File diff suppressed because it is too large Load Diff

View File

@ -26,6 +26,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseHelper } from '@features/course/services/course-helper'; import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks'; import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; import { CoreCourseModuleDelegate, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
/** /**
* Page that displays site home index. * Page that displays site home index.
@ -59,7 +60,6 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
protected navCtrl: NavController, protected navCtrl: NavController,
// @todo private prefetchDelegate: CoreCourseModulePrefetchDelegate,
) {} ) {}
/** /**
@ -86,8 +86,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
const module = navParams['module']; const module = navParams['module'];
if (module) { if (module) {
// @todo const modParams = navParams.get('modParams'); const modParams = navParams['modParams'];
// CoreCourseHelper.instance.openModule(module, this.siteHomeId, undefined, modParams); CoreCourseHelper.instance.openModule(module, this.siteHomeId, undefined, modParams);
} }
this.loadContent().finally(() => { this.loadContent().finally(() => {
@ -174,7 +174,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
if (this.section && this.section.modules) { if (this.section && this.section.modules) {
// Invalidate modules prefetch data. // Invalidate modules prefetch data.
// @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId)); promises.push(CoreCourseModulePrefetchDelegate.instance.invalidateModules(this.section.modules, this.siteHomeId));
} }
if (this.courseBlocksComponent) { if (this.courseBlocksComponent) {

View File

@ -160,40 +160,39 @@ export class CoreFileHelperProvider {
onProgress({ calculating: true }); onProgress({ calculating: true });
} }
try { const shouldDownloadFirst = await CoreFilepool.instance.shouldDownloadFileBeforeOpen(fixedUrl, file.filesize || 0);
await CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize || 0); if (shouldDownloadFirst) {
} catch (error) { // Download the file first.
// Start the download if in wifi, but return the URL right away so the file is opened. if (state == CoreConstants.DOWNLOADING) {
if (isWifi) { // It's already downloading, stop.
this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
}
if (!this.isStateDownloaded(state) || isOnline) {
// Not downloaded or online, return the online URL.
return fixedUrl; return fixedUrl;
} else {
// Outdated but offline, so we return the local URL.
return CoreFilepool.instance.getUrlByUrl(
siteId,
fileUrl,
component,
componentId,
timemodified,
false,
false,
file,
);
} }
// Download and then return the local URL.
return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
} }
// Download the file first. // Start the download if in wifi, but return the URL right away so the file is opened.
if (state == CoreConstants.DOWNLOADING) { if (isWifi) {
// It's already downloading, stop. this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
}
if (!this.isStateDownloaded(state) || isOnline) {
// Not downloaded or online, return the online URL.
return fixedUrl; return fixedUrl;
} else {
// Outdated but offline, so we return the local URL.
return CoreFilepool.instance.getUrlByUrl(
siteId,
fileUrl,
component,
componentId,
timemodified,
false,
false,
file,
);
} }
// Download and then return the local URL.
return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
} }
} }

View File

@ -2763,13 +2763,7 @@ export class CoreFilepoolProvider {
* @param url File online URL. * @param url File online URL.
* @param size File size. * @param size File size.
* @return Promise resolved if should download before open, rejected otherwise. * @return Promise resolved if should download before open, rejected otherwise.
* @description * @ddeprecated since 3.9.5. Please use shouldDownloadFileBeforeOpen instead.
* Convenience function to check if a file should be downloaded before opening it.
*
* The default behaviour in the app is to download first and then open the local file in the following cases:
* - The file is small (less than DOWNLOAD_THRESHOLD).
* - The file cannot be streamed.
* If the file is big and can be streamed, the promise returned by this function will be rejected.
*/ */
async shouldDownloadBeforeOpen(url: string, size: number): Promise<void> { async shouldDownloadBeforeOpen(url: string, size: number): Promise<void> {
if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) { if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) {
@ -2784,6 +2778,32 @@ export class CoreFilepoolProvider {
} }
} }
/**
* Convenience function to check if a file should be downloaded before opening it.
*
* @param url File online URL.
* @param size File size.
* @return Promise resolved with boolean: whether file should be downloaded before opening it.
* @description
* Convenience function to check if a file should be downloaded before opening it.
*
* The default behaviour in the app is to download first and then open the local file in the following cases:
* - The file is small (less than DOWNLOAD_THRESHOLD).
* - The file cannot be streamed.
* If the file is big and can be streamed, the promise returned by this function will be rejected.
*/
async shouldDownloadFileBeforeOpen(url: string, size: number): Promise<boolean> {
if (size >= 0 && size <= CoreFilepoolProvider.DOWNLOAD_THRESHOLD) {
// The file is small, download it.
return true;
}
const mimetype = await CoreUtils.instance.getMimeTypeFromUrl(url);
// If the file is streaming (audio or video), return false.
return mimetype.indexOf('video') == -1 && mimetype.indexOf('audio') == -1;
}
/** /**
* Store package status. * Store package status.
* *

View File

@ -114,7 +114,7 @@ export class CoreUtilsProvider {
* @param result Object where to put the properties. If not defined, a new object will be created. * @param result Object where to put the properties. If not defined, a new object will be created.
* @return The object. * @return The object.
*/ */
arrayToObject<T extends Record<string, unknown> | string>( arrayToObject<T>(
array: T[], array: T[],
propertyName?: string, propertyName?: string,
result: Record<string, T> = {}, result: Record<string, T> = {},