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];
|
||||
switch (results[1]) {
|
||||
case CoreConstants.NOT_DOWNLOADED:
|
||||
moduleInfo.statusIcon = 'cloud-download';
|
||||
moduleInfo.statusIcon = 'fas-cloud-download-alt';
|
||||
break;
|
||||
case CoreConstants.DOWNLOADING:
|
||||
moduleInfo.statusIcon = 'spinner';
|
||||
break;
|
||||
case CoreConstants.OUTDATED:
|
||||
moduleInfo.statusIcon = 'refresh';
|
||||
moduleInfo.statusIcon = 'fas-redo';
|
||||
break;
|
||||
case CoreConstants.DOWNLOADED:
|
||||
if (!CoreCourseModulePrefetchDelegate.instance.canCheckUpdates()) {
|
||||
moduleInfo.statusIcon = 'refresh';
|
||||
moduleInfo.statusIcon = 'fas-redo';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -797,7 +797,7 @@ export class CoreFilepoolProvider {
|
|||
* @param onProgress Function to call on progress.
|
||||
* @return Promise resolved when the package is downloaded.
|
||||
*/
|
||||
protected downloadOrPrefetchPackage(
|
||||
downloadOrPrefetchPackage(
|
||||
siteId: string,
|
||||
fileList: CoreWSExternalFile[],
|
||||
prefetch: boolean,
|
||||
|
|
|
@ -20,6 +20,7 @@ import { CoreWSExternalFile } from '@services/ws';
|
|||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { makeSingleton } from '@singletons';
|
||||
import { CoreSites } from './sites';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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[]>[];
|
||||
|
||||
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) {
|
||||
filteredFiles.push(file);
|
||||
|
|
|
@ -1181,7 +1181,7 @@ export class CoreDomUtilsProvider {
|
|||
* @return Promise resolved with the alert modal.
|
||||
*/
|
||||
async showAlert(
|
||||
header: string,
|
||||
header: string | undefined,
|
||||
message: string,
|
||||
buttonText?: string,
|
||||
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.
|
||||
* @return Promise resolved with the alert modal.
|
||||
*/
|
||||
showAlertTranslated(title: string, message: string, buttonText?: string, autocloseTime?: number): Promise<HTMLIonAlertElement> {
|
||||
title = title ? Translate.instance.instant(title) : title;
|
||||
showAlertTranslated(
|
||||
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;
|
||||
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