MOBILE-3659 course: Add utility classes
parent
310ee19d26
commit
8e8793fff2
|
@ -0,0 +1,193 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreCourse, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. It is useful to minimize the amount of
|
||||||
|
* functions that handlers need to implement. It also provides some helper features like preventing a module to be
|
||||||
|
* downloaded twice at the same time.
|
||||||
|
*
|
||||||
|
* If your handler inherits from this service, you just need to override the functions that you want to change.
|
||||||
|
*
|
||||||
|
* This class should be used for ACTIVITIES. You must override the prefetch function, and it's recommended to call
|
||||||
|
* prefetchPackage in there since it handles the package status.
|
||||||
|
*/
|
||||||
|
export class CoreCourseActivityPrefetchHandlerBase extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
// Same implementation for download and prefetch.
|
||||||
|
return this.prefetch(module, courseId, false, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden. It should call prefetchPackage
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the module, setting package status at start and finish.
|
||||||
|
*
|
||||||
|
* Example usage from a child instance:
|
||||||
|
* return this.prefetchPackage(module, courseId, single, this.prefetchModule.bind(this, otherParam), siteId);
|
||||||
|
*
|
||||||
|
* Then the function "prefetchModule" will receive params:
|
||||||
|
* prefetchModule(module, courseId, single, siteId, someParam, anotherParam)
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param downloadFn Function to perform the prefetch. Please check the documentation of prefetchFunction.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the module has been downloaded. Data returned is not reliable.
|
||||||
|
*/
|
||||||
|
async prefetchPackage(
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
downloadFunction: () => Promise<string>,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isOnline()) {
|
||||||
|
// Cannot prefetch in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDownloading(module.id, siteId)) {
|
||||||
|
// There's already a download ongoing for this module, return the promise.
|
||||||
|
return this.getOngoingDownload(module.id, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefetchPromise = this.changeStatusAndPrefetch(module, courseId, downloadFunction, siteId);
|
||||||
|
|
||||||
|
return this.addOngoingDownload(module.id, prefetchPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async changeStatusAndPrefetch(
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
downloadFunction: () => Promise<string>,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.setDownloading(module.id, siteId);
|
||||||
|
|
||||||
|
// Package marked as downloading, get module info to be able to handle links. Get module filters too.
|
||||||
|
await Promise.all([
|
||||||
|
CoreCourse.instance.getModuleBasicInfo(module.id, siteId),
|
||||||
|
CoreCourse.instance.getModule(module.id, courseId, undefined, false, true, siteId),
|
||||||
|
CoreFilterHelper.instance.getFilters('module', module.id, { courseId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Call the download function.
|
||||||
|
let extra = await downloadFunction();
|
||||||
|
|
||||||
|
// Only accept string types.
|
||||||
|
if (typeof extra != 'string') {
|
||||||
|
extra = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefetch finished, mark as downloaded.
|
||||||
|
await this.setDownloaded(module.id, siteId, extra);
|
||||||
|
} catch (error) {
|
||||||
|
// Error prefetching, go back to previous status and reject the promise.
|
||||||
|
return this.setPreviousStatus(module.id, siteId);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the module as downloaded.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param extra Extra data to store.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
setDownloaded(id: number, siteId?: string, extra?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.storePackageStatus(siteId, CoreConstants.DOWNLOADED, this.component, id, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the module as downloading.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
setDownloading(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.storePackageStatus(siteId, CoreConstants.DOWNLOADING, this.component, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set previous status and return a rejected promise.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Rejected promise.
|
||||||
|
*/
|
||||||
|
async setPreviousStatus(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await CoreFilepool.instance.setPackagePreviousStatus(siteId, this.component, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set previous status and return a rejected promise.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param error Error to throw.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Rejected promise.
|
||||||
|
* @deprecated since 3.9.5. Use setPreviousStatus instead.
|
||||||
|
*/
|
||||||
|
async setPreviousStatusAndReject(id: number, error?: Error, siteId?: string): Promise<never> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await CoreFilepool.instance.setPackagePreviousStatus(siteId, this.component, id);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// (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 { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
import { CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class to create activity sync providers. It provides some common functions.
|
||||||
|
*/
|
||||||
|
export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conveniece function to prefetch data after an update.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param preventDownloadRegex If regex matches, don't download the data. Defaults to check files.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prefetchAfterUpdate(
|
||||||
|
prefetchHandler: CoreCourseModulePrefetchHandlerBase,
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
preventDownloadRegex?: RegExp,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Get the module updates to check if the data was updated or not.
|
||||||
|
const result = await CoreCourseModulePrefetchDelegate.instance.getModuleUpdates(module, courseId, true, siteId);
|
||||||
|
|
||||||
|
if (!result?.updates.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only prefetch if files haven't changed, to prevent downloading too much data automatically.
|
||||||
|
const regex = preventDownloadRegex || /^.*files$/;
|
||||||
|
const shouldDownload = !result.updates.find((entry) => entry.name.match(regex));
|
||||||
|
|
||||||
|
if (shouldDownload) {
|
||||||
|
return prefetchHandler.download(module, courseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Component, Inject, Input, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
|
import { IonContent } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreCourseModuleMainResourceComponent } from './main-resource-component';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { Network, NgZone } from '@singletons';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreCourse } from '../services/course';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { CoreCourseContentsPage } from '../pages/contents/contents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template class to easily create CoreCourseModuleMainComponent of activities.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() group?: number; // Group ID the component belongs to.
|
||||||
|
|
||||||
|
moduleName?: string; // Raw module name to be translated. It will be translated on init.
|
||||||
|
|
||||||
|
// Data for context menu.
|
||||||
|
syncIcon?: string; // Sync icon.
|
||||||
|
hasOffline?: boolean; // If it has offline data to be synced.
|
||||||
|
isOnline?: boolean; // If the app is online or not.
|
||||||
|
|
||||||
|
protected syncObserver?: CoreEventObserver; // It will observe the sync auto event.
|
||||||
|
protected onlineSubscription: Subscription; // It will observe the status of the network connection.
|
||||||
|
protected syncEventName?: string; // Auto sync event name.
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||||
|
protected content?: IonContent,
|
||||||
|
courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
super(loggerName, courseContentsPage);
|
||||||
|
|
||||||
|
// Refresh online status when changes.
|
||||||
|
this.onlineSubscription = Network.instance.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.instance.run(() => {
|
||||||
|
this.isOnline = CoreApp.instance.isOnline();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.hasOffline = false;
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.moduleName = CoreCourse.instance.translateModuleName(this.moduleName || '');
|
||||||
|
|
||||||
|
if (this.syncEventName) {
|
||||||
|
// Refresh data if this discussion is synchronized automatically.
|
||||||
|
this.syncObserver = CoreEvents.on(this.syncEventName, (data) => {
|
||||||
|
this.autoSyncEventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param syncEventData Data received on sync observer.
|
||||||
|
* @return True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: unknown): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autosync event has been received, check if refresh is needed and update the view.
|
||||||
|
*
|
||||||
|
* @param syncEventData Data receiven on sync observer.
|
||||||
|
*/
|
||||||
|
protected autoSyncEventReceived(syncEventData: unknown): void {
|
||||||
|
if (this.isRefreshSyncNeeded(syncEventData)) {
|
||||||
|
// Refresh the data.
|
||||||
|
this.showLoadingAndRefresh(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateContent());
|
||||||
|
|
||||||
|
await this.loadContent(true, sync, showErrors);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show loading and perform the load content function.
|
||||||
|
*
|
||||||
|
* @param sync If the fetch needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async showLoadingAndFetch(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.loaded = false;
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loadContent(false, sync, showErrors);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show loading and perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.loaded = false;
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
return this.refreshContent(sync, showErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the component contents.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the component contents and shows the corresponding error.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.isOnline = CoreApp.instance.isOnline();
|
||||||
|
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchContent(refresh, sync, showErrors);
|
||||||
|
} catch (error) {
|
||||||
|
if (!refresh) {
|
||||||
|
// Some call failed, retry without using cache since it might be a new activity.
|
||||||
|
return await this.refreshContent(sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async sync(): Promise<unknown> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param result Data returned on the sync function.
|
||||||
|
* @return If suceed or not.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected hasSyncSucceed(result: unknown): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to synchronize the activity.
|
||||||
|
*
|
||||||
|
* @param showErrors If show errors to the user of hide them.
|
||||||
|
* @return Promise resolved with true if sync succeed, or false if failed.
|
||||||
|
*/
|
||||||
|
protected async syncActivity(showErrors: boolean = false): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = <{warnings?: CoreWSExternalWarning[]}> await this.sync();
|
||||||
|
|
||||||
|
if (result?.warnings?.length) {
|
||||||
|
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasSyncSucceed(result);
|
||||||
|
} catch (error) {
|
||||||
|
if (showErrors) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
this.onlineSubscription?.unsubscribe();
|
||||||
|
this.syncObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,412 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { OnInit, OnDestroy, Input, Output, EventEmitter, Component, Optional, Inject } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
|
import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CoreCourseContentsPage } from '../pages/contents/contents';
|
||||||
|
import { CoreCourse } from '../services/course';
|
||||||
|
import { CoreCourseHelper, CoreCourseModule } from '../services/course-helper';
|
||||||
|
import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a resource download.
|
||||||
|
*/
|
||||||
|
export type CoreCourseResourceDownloadResult = {
|
||||||
|
failed?: boolean; // Whether the download has failed.
|
||||||
|
error?: string | CoreTextErrorObject; // The error in case it failed.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template class to easily create CoreCourseModuleMainComponent of resources (or activities without syncing).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent {
|
||||||
|
|
||||||
|
@Input() module?: CoreCourseModule; // The module of the component.
|
||||||
|
@Input() courseId?: number; // Course ID the component belongs to.
|
||||||
|
@Output() dataRetrieved = new EventEmitter<unknown>(); // Called to notify changes the index page from the main component.
|
||||||
|
|
||||||
|
loaded = false; // If the component has been loaded.
|
||||||
|
component?: string; // Component name.
|
||||||
|
componentId?: number; // Component ID.
|
||||||
|
blog?: boolean; // If blog is avalaible.
|
||||||
|
|
||||||
|
// Data for context menu.
|
||||||
|
externalUrl?: string; // External URL to open in browser.
|
||||||
|
description?: string; // Module description.
|
||||||
|
refreshIcon = 'spinner'; // Refresh icon, normally spinner or refresh.
|
||||||
|
prefetchStatusIcon?: string; // Used when calling fillContextMenu.
|
||||||
|
prefetchStatus?: string; // Used when calling fillContextMenu.
|
||||||
|
prefetchText?: string; // Used when calling fillContextMenu.
|
||||||
|
size?: string; // Used when calling fillContextMenu.
|
||||||
|
isDestroyed?: boolean; // Whether the component is destroyed, used when calling fillContextMenu.
|
||||||
|
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
|
||||||
|
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
|
||||||
|
|
||||||
|
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
|
||||||
|
protected isCurrentView?: boolean; // Whether the component is in the current view.
|
||||||
|
protected siteId?: string; // Current Site ID.
|
||||||
|
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
|
||||||
|
protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||||
|
protected courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
this.logger = CoreLogger.getInstance(loggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
this.description = this.module?.description;
|
||||||
|
this.componentId = this.module?.id;
|
||||||
|
this.externalUrl = this.module?.url;
|
||||||
|
this.courseId = this.courseId || this.module?.course;
|
||||||
|
// @todo this.blog = await this.blogProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param done Function to call when done.
|
||||||
|
* @param showErrors If show errors to the user of hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.loaded || !this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a single activity course and the refresher is displayed within the component,
|
||||||
|
// call doRefresh on the section page to refresh the course data.
|
||||||
|
if (this.courseContentsPage && !CoreCourseModuleDelegate.instance.displayRefresherInSingleActivity(this.module.modname)) {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.courseContentsPage.doRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.refreshContent(true, showErrors));
|
||||||
|
|
||||||
|
refresher?.detail.complete();
|
||||||
|
done && done();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateContent());
|
||||||
|
|
||||||
|
await this.loadContent(true);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async invalidateContent(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the component contents.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async fetchContent(refresh?: boolean): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the component contents and shows the corresponding error.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadContent(refresh?: boolean): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchContent(refresh);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the context menu options
|
||||||
|
*/
|
||||||
|
protected fillContextMenu(refresh: boolean = false): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
CoreCourseHelper.instance.fillContextMenu(this, this.module, this.courseId!, refresh, this.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu.
|
||||||
|
* This means that you need to call fillContextMenu to make this work.
|
||||||
|
*/
|
||||||
|
protected isPrefetched(): boolean {
|
||||||
|
return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the description.
|
||||||
|
*/
|
||||||
|
expandDescription(): void {
|
||||||
|
CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.description!, {
|
||||||
|
component: this.component,
|
||||||
|
componentId: this.module?.id,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module?.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to blog posts.
|
||||||
|
*/
|
||||||
|
async gotoBlog(): Promise<void> {
|
||||||
|
// @todo return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the module.
|
||||||
|
*
|
||||||
|
* @param done Function to call when done.
|
||||||
|
*/
|
||||||
|
prefetch(done?: () => void): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.instance.contextMenuPrefetch(this, this.module, this.courseId!, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm and remove downloaded files.
|
||||||
|
*
|
||||||
|
* @param done Function to call when done.
|
||||||
|
*/
|
||||||
|
removeFiles(done?: () => void): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.prefetchStatus == CoreConstants.DOWNLOADING) {
|
||||||
|
CoreDomUtils.instance.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.instance.confirmAndRemoveFiles(this.module, this.courseId!, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get message about an error occurred while downloading files.
|
||||||
|
*
|
||||||
|
* @param error The specific error.
|
||||||
|
* @param multiLine Whether to put each message in a different paragraph or in a single line.
|
||||||
|
*/
|
||||||
|
protected getErrorDownloadingSomeFilesMessage(error: string | CoreTextErrorObject, multiLine?: boolean): string {
|
||||||
|
if (multiLine) {
|
||||||
|
return CoreTextUtils.instance.buildSeveralParagraphsMessage([
|
||||||
|
Translate.instance.instant('core.errordownloadingsomefiles'),
|
||||||
|
error,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
error = CoreTextUtils.instance.getErrorMessageFromError(error) || error;
|
||||||
|
|
||||||
|
return Translate.instance.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an error occurred while downloading files.
|
||||||
|
*
|
||||||
|
* @param error The specific error.
|
||||||
|
*/
|
||||||
|
protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void {
|
||||||
|
CoreDomUtils.instance.showErrorModal(this.getErrorDownloadingSomeFilesMessage(error, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays some data based on the current status.
|
||||||
|
*
|
||||||
|
* @param status The current status.
|
||||||
|
* @param previousStatus The previous status. If not defined, there is no previous status.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected showStatus(status: string, previousStatus?: string): void {
|
||||||
|
// To be overridden.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch for changes on the status.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async setStatusListener(): Promise<void> {
|
||||||
|
if (typeof this.statusObserver != 'undefined' || !this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for changes on this module status.
|
||||||
|
this.statusObserver = CoreEvents.on<CoreEventPackageStatusChanged>(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||||
|
if (!this.module || data.componentId != this.module.id || data.component != this.component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The status has changed, update it.
|
||||||
|
const previousStatus = this.currentStatus;
|
||||||
|
this.currentStatus = data.status;
|
||||||
|
|
||||||
|
this.showStatus(this.currentStatus, previousStatus);
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Also, get the current status.
|
||||||
|
const status = await CoreCourseModulePrefetchDelegate.instance.getModuleStatus(this.module, this.courseId!);
|
||||||
|
|
||||||
|
this.currentStatus = status;
|
||||||
|
this.showStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a resource if needed.
|
||||||
|
* If the download call fails the promise won't be rejected, but the error will be included in the returned object.
|
||||||
|
* If module.contents cannot be loaded then the Promise will be rejected.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async downloadResourceIfNeeded(
|
||||||
|
refresh?: boolean,
|
||||||
|
contentsAlreadyLoaded?: boolean,
|
||||||
|
): Promise<CoreCourseResourceDownloadResult> {
|
||||||
|
|
||||||
|
const result: CoreCourseResourceDownloadResult = {
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.module) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get module status to determine if it needs to be downloaded.
|
||||||
|
await this.setStatusListener();
|
||||||
|
|
||||||
|
if (this.currentStatus != CoreConstants.DOWNLOADED) {
|
||||||
|
// Download content. This function also loads module contents if needed.
|
||||||
|
try {
|
||||||
|
await CoreCourseModulePrefetchDelegate.instance.downloadModule(this.module, this.courseId!);
|
||||||
|
|
||||||
|
// If we reach here it means the download process already loaded the contents, no need to do it again.
|
||||||
|
contentsAlreadyLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
// Mark download as failed but go on since the main files could have been downloaded.
|
||||||
|
result.failed = true;
|
||||||
|
result.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.module.contents.length || (refresh && !contentsAlreadyLoaded)) {
|
||||||
|
// Try to load the contents.
|
||||||
|
const ignoreCache = refresh && CoreApp.instance.isOnline();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourse.instance.loadModuleContents(this.module, this.courseId, undefined, false, ignoreCache);
|
||||||
|
} catch (error) {
|
||||||
|
// Error loading contents. If we ignored cache, try to get the cached value.
|
||||||
|
if (ignoreCache && !this.module.contents) {
|
||||||
|
await CoreCourse.instance.loadModuleContents(this.module, this.courseId);
|
||||||
|
} else if (!this.module.contents) {
|
||||||
|
// Not able to load contents, throw the error.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.contextMenuStatusObserver?.off();
|
||||||
|
this.contextFileStatusObserver?.off();
|
||||||
|
this.statusObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page that contains the component. This function should be called by the page that contains this component.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.isCurrentView = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page that contains the component. This function should be called by the page that contains this component.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.isCurrentView = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,344 @@
|
||||||
|
// (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 { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandler } from '../services/module-prefetch-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. Prefetch handlers should inherit either
|
||||||
|
* from CoreCourseModuleActivityPrefetchHandlerBase or CoreCourseModuleResourcePrefetchHandlerBase, depending on whether
|
||||||
|
* they are an activity or a resource. It's not recommended to inherit from this class directly.
|
||||||
|
*/
|
||||||
|
export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePrefetchHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the handler.
|
||||||
|
*/
|
||||||
|
name = 'CoreCourseModulePrefetchHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the module. It should match the "modname" of the module returned in core_course_get_contents.
|
||||||
|
*/
|
||||||
|
modName = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler's component.
|
||||||
|
*/
|
||||||
|
component = 'core_module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RegExp to check updates. If a module has an update whose name matches this RegExp, the module will be marked
|
||||||
|
* as outdated. This RegExp is ignored if hasUpdates function is defined.
|
||||||
|
*/
|
||||||
|
updatesNames = /^.*files$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, this module will be ignored when determining the status of a list of modules. The module will
|
||||||
|
* still be downloaded when downloading the section/course, it only affects whether the button should be displayed.
|
||||||
|
*/
|
||||||
|
skipListStatus = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of download promises to prevent downloading the module twice at the same time.
|
||||||
|
*/
|
||||||
|
protected downloadPromises: { [s: string]: { [s: string]: Promise<void> } } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an ongoing download to the downloadPromises list. When the promise finishes it will be removed.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param promise Promise to add.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise of the current download.
|
||||||
|
*/
|
||||||
|
async addOngoingDownload(id: number, promise: Promise<void>, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
const uniqueId = this.getUniqueId(id);
|
||||||
|
|
||||||
|
if (!this.downloadPromises[siteId]) {
|
||||||
|
this.downloadPromises[siteId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadPromises[siteId][uniqueId] = promise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.downloadPromises[siteId][uniqueId];
|
||||||
|
} finally {
|
||||||
|
delete this.downloadPromises[siteId][uniqueId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of content files that can be downloaded.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @return List of files.
|
||||||
|
*/
|
||||||
|
getContentDownloadableFiles(module: CoreCourseWSModule): CoreCourseModuleContentFile[] {
|
||||||
|
if (!module.contents?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return module.contents.filter((content) => this.isFileDownloadable(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the download size of a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the size.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getDownloadSize(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreFileSizeSum> {
|
||||||
|
try {
|
||||||
|
const files = await this.getFiles(module, courseId);
|
||||||
|
|
||||||
|
return await CorePluginFileDelegate.instance.getFilesDownloadSize(files);
|
||||||
|
} catch {
|
||||||
|
return { size: -1, total: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the downloaded size of a module. If not defined, we'll use getFiles to calculate it (it can be slow).
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Size, or promise resolved with the size.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getDownloadedSize(module: CoreCourseWSModule, courseId: number): Promise<number> {
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.getFilesSizeByComponent(siteId, this.component, module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
// To be overridden.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns module intro files.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved with list of intro files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getIntroFiles(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
return this.getIntroFilesFromInstance(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns module intro files from instance.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param instance The instance to get the intro files (book, assign, ...). If not defined, module will be used.
|
||||||
|
* @return List of intro files.
|
||||||
|
*/
|
||||||
|
getIntroFilesFromInstance(module: CoreCourseWSModule, instance?: ModuleInstance): CoreWSExternalFile[] {
|
||||||
|
if (instance) {
|
||||||
|
if (typeof instance.introfiles != 'undefined') {
|
||||||
|
return instance.introfiles;
|
||||||
|
} else if (instance.intro) {
|
||||||
|
return CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.description) {
|
||||||
|
return CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there's an ongoing download for a certain identifier return it.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise of the current download.
|
||||||
|
*/
|
||||||
|
async getOngoingDownload(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isDownloading(id, siteId)) {
|
||||||
|
// There's already a download ongoing, return the promise.
|
||||||
|
return this.downloadPromises[siteId][this.getUniqueId(id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create unique identifier using component and id.
|
||||||
|
*
|
||||||
|
* @param id Unique ID inside component.
|
||||||
|
* @return Unique ID.
|
||||||
|
*/
|
||||||
|
getUniqueId(id: number): string {
|
||||||
|
return this.component + '#' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID.
|
||||||
|
* @param courseId The course ID the module belongs to.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
|
||||||
|
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
invalidateModule(module: CoreCourseWSModule, courseId: number): Promise<void> {
|
||||||
|
return CoreCourse.instance.invalidateModule(module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Whether the module can be downloaded. The promise should never be rejected.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async isDownloadable(module: CoreCourseWSModule, courseId: number): Promise<boolean> {
|
||||||
|
// By default, mark all instances as downloadable.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a there's an ongoing download for the given identifier.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return True if downloading, false otherwise.
|
||||||
|
*/
|
||||||
|
isDownloading(id: number, siteId?: string): boolean {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return !!(this.downloadPromises[siteId] && this.downloadPromises[siteId][this.getUniqueId(id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is downloadable.
|
||||||
|
*
|
||||||
|
* @param file File to check.
|
||||||
|
* @return Whether the file is downloadable.
|
||||||
|
*/
|
||||||
|
isFileDownloadable(file: CoreCourseModuleContentFile): boolean {
|
||||||
|
return file.type === 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module contents into module.contents if they aren't loaded already.
|
||||||
|
*
|
||||||
|
* @param module Module to load the contents.
|
||||||
|
* @param courseId The course ID. Recommended to speed up the process and minimize data usage.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async loadContents(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove module downloaded files. If not defined, we'll use getFiles to remove them (slow).
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
removeFiles(module: CoreCourseWSModule, courseId: number): Promise<void> {
|
||||||
|
return CoreFilepool.instance.removeFilesByComponent(CoreSites.instance.getCurrentSiteId(), this.component, module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties a module instance should have to be able to retrieve its intro files.
|
||||||
|
*/
|
||||||
|
type ModuleInstance = {
|
||||||
|
introfiles?: CoreWSExternalFile[];
|
||||||
|
intro?: string;
|
||||||
|
};
|
|
@ -0,0 +1,203 @@
|
||||||
|
// (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 { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreCourse, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. It is useful to minimize the amount of
|
||||||
|
* functions that handlers need to implement. It also provides some helper features like preventing a module to be
|
||||||
|
* downloaded twice at the same time.
|
||||||
|
*
|
||||||
|
* If your handler inherits from this service, you just need to override the functions that you want to change.
|
||||||
|
*
|
||||||
|
* This class should be used for RESOURCES whose main purpose is downloading files present in module.contents.
|
||||||
|
*/
|
||||||
|
export class CoreCourseResourcePrefetchHandlerBase extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
return this.downloadOrPrefetch(module, courseId, false, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param prefetch True to prefetch, false to download right away.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files. This is to keep the files
|
||||||
|
* relative paths and make the package work in an iframe. Undefined to download the files
|
||||||
|
* in the filepool root folder.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
if (!CoreApp.instance.isOnline()) {
|
||||||
|
// Cannot download in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isDownloading(module.id, siteId)) {
|
||||||
|
// There's already a download ongoing for this module, return the promise.
|
||||||
|
return this.getOngoingDownload(module.id, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get module info to be able to handle links.
|
||||||
|
const prefetchPromise = this.performDownloadOrPrefetch(siteId, module, courseId, !!prefetch, dirPath);
|
||||||
|
|
||||||
|
return this.addOngoingDownload(module.id, prefetchPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param prefetch True to prefetch, false to download right away.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
protected async performDownloadOrPrefetch(
|
||||||
|
siteId: string,
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
prefetch: boolean,
|
||||||
|
dirPath?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Get module info to be able to handle links.
|
||||||
|
await CoreCourse.instance.getModuleBasicInfo(module.id, siteId);
|
||||||
|
|
||||||
|
// Load module contents (ignore cache so we always have the latest data).
|
||||||
|
await this.loadContents(module, courseId, true);
|
||||||
|
|
||||||
|
// Get the intro files.
|
||||||
|
const introFiles = await this.getIntroFiles(module, courseId, true);
|
||||||
|
|
||||||
|
const contentFiles = this.getContentDownloadableFiles(module);
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
if (dirPath) {
|
||||||
|
// Download intro files in filepool root folder.
|
||||||
|
promises.push(
|
||||||
|
CoreFilepool.instance.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false, this.component, module.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download content files inside dirPath.
|
||||||
|
promises.push(CoreFilepool.instance.downloadOrPrefetchPackage(
|
||||||
|
siteId,
|
||||||
|
contentFiles,
|
||||||
|
prefetch,
|
||||||
|
this.component,
|
||||||
|
module.id,
|
||||||
|
undefined,
|
||||||
|
dirPath,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// No dirPath, download everything in filepool root folder.
|
||||||
|
promises.push(CoreFilepool.instance.downloadOrPrefetchPackage(
|
||||||
|
siteId,
|
||||||
|
introFiles.concat(contentFiles),
|
||||||
|
prefetch,
|
||||||
|
this.component,
|
||||||
|
module.id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(CoreFilterHelper.instance.getFilters('module', module.id, { courseId }));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
// Load module contents if needed.
|
||||||
|
await this.loadContents(module, courseId);
|
||||||
|
|
||||||
|
const files = await this.getIntroFiles(module, courseId);
|
||||||
|
|
||||||
|
return files.concat(this.getContentDownloadableFiles(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID.
|
||||||
|
* @param courseId The course ID the module belongs to.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
CoreCourse.instance.invalidateModule(moduleId),
|
||||||
|
CoreFilepool.instance.invalidateFilesByComponent(siteId, this.component, moduleId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module contents into module.contents if they aren't loaded already.
|
||||||
|
*
|
||||||
|
* @param module Module to load the contents.
|
||||||
|
* @param courseId The course ID. Recommended to speed up the process and minimize data usage.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
loadContents(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<void> {
|
||||||
|
return CoreCourse.instance.loadModuleContents(module, courseId, undefined, false, ignoreCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
courseId = courseId || module.course;
|
||||||
|
if (!courseId) {
|
||||||
|
throw new CoreError('Course ID not supplied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.downloadOrPrefetch(module, courseId, true, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1332,17 +1332,17 @@ export class CoreCourseHelperProvider {
|
||||||
moduleInfo.status = results[1];
|
moduleInfo.status = results[1];
|
||||||
switch (results[1]) {
|
switch (results[1]) {
|
||||||
case CoreConstants.NOT_DOWNLOADED:
|
case CoreConstants.NOT_DOWNLOADED:
|
||||||
moduleInfo.statusIcon = 'cloud-download';
|
moduleInfo.statusIcon = 'fas-cloud-download-alt';
|
||||||
break;
|
break;
|
||||||
case CoreConstants.DOWNLOADING:
|
case CoreConstants.DOWNLOADING:
|
||||||
moduleInfo.statusIcon = 'spinner';
|
moduleInfo.statusIcon = 'spinner';
|
||||||
break;
|
break;
|
||||||
case CoreConstants.OUTDATED:
|
case CoreConstants.OUTDATED:
|
||||||
moduleInfo.statusIcon = 'refresh';
|
moduleInfo.statusIcon = 'fas-redo';
|
||||||
break;
|
break;
|
||||||
case CoreConstants.DOWNLOADED:
|
case CoreConstants.DOWNLOADED:
|
||||||
if (!CoreCourseModulePrefetchDelegate.instance.canCheckUpdates()) {
|
if (!CoreCourseModulePrefetchDelegate.instance.canCheckUpdates()) {
|
||||||
moduleInfo.statusIcon = 'refresh';
|
moduleInfo.statusIcon = 'fas-redo';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -797,7 +797,7 @@ export class CoreFilepoolProvider {
|
||||||
* @param onProgress Function to call on progress.
|
* @param onProgress Function to call on progress.
|
||||||
* @return Promise resolved when the package is downloaded.
|
* @return Promise resolved when the package is downloaded.
|
||||||
*/
|
*/
|
||||||
protected downloadOrPrefetchPackage(
|
downloadOrPrefetchPackage(
|
||||||
siteId: string,
|
siteId: string,
|
||||||
fileList: CoreWSExternalFile[],
|
fileList: CoreWSExternalFile[],
|
||||||
prefetch: boolean,
|
prefetch: boolean,
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CoreWSExternalFile } from '@services/ws';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreSites } from './sites';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegate to register pluginfile information handlers.
|
* Delegate to register pluginfile information handlers.
|
||||||
|
@ -133,11 +134,13 @@ export class CorePluginFileDelegateService extends CoreDelegate<CorePluginFileHa
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial.
|
* @return Promise resolved with file size and a boolean to indicate if it is the total size or only partial.
|
||||||
*/
|
*/
|
||||||
async getFilesDownloadSize(files: CoreWSExternalFile[], siteId: string): Promise<CoreFileSizeSum> {
|
async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise<CoreFileSizeSum> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
const filteredFiles = <CoreWSExternalFile[]>[];
|
const filteredFiles = <CoreWSExternalFile[]>[];
|
||||||
|
|
||||||
await Promise.all(files.map(async (file) => {
|
await Promise.all(files.map(async (file) => {
|
||||||
const state = await CoreFilepool.instance.getFileStateByUrl(siteId, file.fileurl, file.timemodified);
|
const state = await CoreFilepool.instance.getFileStateByUrl(siteId!, file.fileurl, file.timemodified);
|
||||||
|
|
||||||
if (state != CoreConstants.DOWNLOADED && state != CoreConstants.NOT_DOWNLOADABLE) {
|
if (state != CoreConstants.DOWNLOADED && state != CoreConstants.NOT_DOWNLOADABLE) {
|
||||||
filteredFiles.push(file);
|
filteredFiles.push(file);
|
||||||
|
|
|
@ -1181,7 +1181,7 @@ export class CoreDomUtilsProvider {
|
||||||
* @return Promise resolved with the alert modal.
|
* @return Promise resolved with the alert modal.
|
||||||
*/
|
*/
|
||||||
async showAlert(
|
async showAlert(
|
||||||
header: string,
|
header: string | undefined,
|
||||||
message: string,
|
message: string,
|
||||||
buttonText?: string,
|
buttonText?: string,
|
||||||
autocloseTime?: number,
|
autocloseTime?: number,
|
||||||
|
@ -1263,12 +1263,17 @@ export class CoreDomUtilsProvider {
|
||||||
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
* @param autocloseTime Number of milliseconds to wait to close the modal. If not defined, modal won't be closed.
|
||||||
* @return Promise resolved with the alert modal.
|
* @return Promise resolved with the alert modal.
|
||||||
*/
|
*/
|
||||||
showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<HTMLIonAlertElement> {
|
showAlertTranslated(
|
||||||
title = title ? Translate.instance.instant(title) : title;
|
header: string | undefined,
|
||||||
|
message: string,
|
||||||
|
buttonText?: string,
|
||||||
|
autocloseTime?: number,
|
||||||
|
): Promise<HTMLIonAlertElement> {
|
||||||
|
header = header ? Translate.instance.instant(header) : header;
|
||||||
message = message ? Translate.instance.instant(message) : message;
|
message = message ? Translate.instance.instant(message) : message;
|
||||||
buttonText = buttonText ? Translate.instance.instant(buttonText) : buttonText;
|
buttonText = buttonText ? Translate.instance.instant(buttonText) : buttonText;
|
||||||
|
|
||||||
return this.showAlert(title, message, buttonText, autocloseTime);
|
return this.showAlert(header, message, buttonText, autocloseTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue