diff --git a/src/core/features/course/classes/activity-prefetch-handler.ts b/src/core/features/course/classes/activity-prefetch-handler.ts new file mode 100644 index 000000000..1e0131086 --- /dev/null +++ b/src/core/features/course/classes/activity-prefetch-handler.ts @@ -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 { + // 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 { + // 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, + siteId?: string, + ): Promise { + 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, + siteId?: string, + ): Promise { + 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 { + 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 { + 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 { + 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 { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + await CoreFilepool.instance.setPackagePreviousStatus(siteId, this.component, id); + + throw error; + } + +} diff --git a/src/core/features/course/classes/activity-sync.ts b/src/core/features/course/classes/activity-sync.ts new file mode 100644 index 000000000..e00a5dc8d --- /dev/null +++ b/src/core/features/course/classes/activity-sync.ts @@ -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 { + // 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); + } + } + +} diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts new file mode 100644 index 000000000..197010660 --- /dev/null +++ b/src/core/features/course/classes/main-activity-component.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + } + +} diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts new file mode 100644 index 000000000..9e802aea1 --- /dev/null +++ b/src/core/features/course/classes/main-resource-component.ts @@ -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(); // 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 { + 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, done?: () => void, showErrors: boolean = false): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + // @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 { + if (typeof this.statusObserver != 'undefined' || !this.module) { + return; + } + + // Listen for changes on this module status. + this.statusObserver = CoreEvents.on(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 { + + 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; + } + +} diff --git a/src/core/features/course/classes/module-prefetch-handler.ts b/src/core/features/course/classes/module-prefetch-handler.ts new file mode 100644 index 000000000..7b5e10654 --- /dev/null +++ b/src/core/features/course/classes/module-prefetch-handler.ts @@ -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 } } = {}; + + /** + * 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, siteId?: string): Promise { + 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 { + // 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 { + 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 { + // 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 { + // 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 { + 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; +}; diff --git a/src/core/features/course/classes/resource-prefetch-handler.ts b/src/core/features/course/classes/resource-prefetch-handler.ts new file mode 100644 index 000000000..899fb729f --- /dev/null +++ b/src/core/features/course/classes/resource-prefetch-handler.ts @@ -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 { + 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 { + 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 { + // 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[] = []; + + 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 { + // 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 { + 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 { + 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 { + courseId = courseId || module.course; + if (!courseId) { + throw new CoreError('Course ID not supplied.'); + } + + return this.downloadOrPrefetch(module, courseId, true, dirPath); + } + +} diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 3eb36e61b..417ea149e 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -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: diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index aa2518302..211bc7107 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -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, diff --git a/src/core/services/plugin-file-delegate.ts b/src/core/services/plugin-file-delegate.ts index 93cd9340b..c5b791374 100644 --- a/src/core/services/plugin-file-delegate.ts +++ b/src/core/services/plugin-file-delegate.ts @@ -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 { + async getFilesDownloadSize(files: CoreWSExternalFile[], siteId?: string): Promise { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + const filteredFiles = []; 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); diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 490cee48f..444376fa9 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -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 { - title = title ? Translate.instance.instant(title) : title; + showAlertTranslated( + header: string | undefined, + message: string, + buttonText?: string, + autocloseTime?: number, + ): Promise { + 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); } /**