diff --git a/scripts/langindex.json b/scripts/langindex.json index 212d23e5c..504e2bc4d 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1390,6 +1390,7 @@ "core.course.allsections": "local_moodlemobileapp", "core.course.askadmintosupport": "local_moodlemobileapp", "core.course.availablespace": "local_moodlemobileapp", + "core.course.cannotdeletewhiledownloading": "local_moodlemobileapp", "core.course.confirmdeletemodulefiles": "local_moodlemobileapp", "core.course.confirmdownload": "local_moodlemobileapp", "core.course.confirmdownloadunknownsize": "local_moodlemobileapp", diff --git a/src/addon/mod/assign/components/index/addon-mod-assign-index.html b/src/addon/mod/assign/components/index/addon-mod-assign-index.html index 467f9e49d..ee087a9df 100644 --- a/src/addon/mod/assign/components/index/addon-mod-assign-index.html +++ b/src/addon/mod/assign/components/index/addon-mod-assign-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html index 6fcc9f70d..cfd0ff551 100644 --- a/src/addon/mod/book/components/index/addon-mod-book-index.html +++ b/src/addon/mod/book/components/index/addon-mod-book-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/choice/components/index/addon-mod-choice-index.html b/src/addon/mod/choice/components/index/addon-mod-choice-index.html index 762ad5066..bc5de5829 100644 --- a/src/addon/mod/choice/components/index/addon-mod-choice-index.html +++ b/src/addon/mod/choice/components/index/addon-mod-choice-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/data/components/index/addon-mod-data-index.html b/src/addon/mod/data/components/index/addon-mod-data-index.html index 200fb4387..af4a82ad6 100644 --- a/src/addon/mod/data/components/index/addon-mod-data-index.html +++ b/src/addon/mod/data/components/index/addon-mod-data-index.html @@ -12,7 +12,7 @@ - + diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html index 4355c5310..b491576c5 100644 --- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html +++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/folder/components/index/addon-mod-folder-index.html b/src/addon/mod/folder/components/index/addon-mod-folder-index.html index 03eb3838a..2aa7976f3 100644 --- a/src/addon/mod/folder/components/index/addon-mod-folder-index.html +++ b/src/addon/mod/folder/components/index/addon-mod-folder-index.html @@ -4,9 +4,9 @@ - + - + @@ -17,11 +17,11 @@ - +

{{file.name}}

- +
diff --git a/src/addon/mod/folder/components/index/index.ts b/src/addon/mod/folder/components/index/index.ts index 922ec3836..622acb449 100644 --- a/src/addon/mod/folder/components/index/index.ts +++ b/src/addon/mod/folder/components/index/index.ts @@ -29,7 +29,8 @@ import { AddonModFolderHelperProvider } from '../../providers/helper'; templateUrl: 'addon-mod-folder-index.html', }) export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceComponent { - @Input() path: string; // For subfolders. Use the path instead of a boolean so Angular detects them as different states. + @Input() folderInstance?: any; // The mod_folder instance. + @Input() subfolder?: any; // Subfolder to show. component = AddonModFolderProvider.COMPONENT; canGetFolder: boolean; @@ -48,9 +49,9 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo this.canGetFolder = this.folderProvider.isGetFolderWSAvailable(); - if (this.path) { + if (this.subfolder) { // Subfolder. Use module param. - this.showModuleData(this.module, this.module.contents); + this.showModuleData(this.subfolder.contents); this.loaded = true; this.refreshIcon = 'refresh'; } else { @@ -77,15 +78,14 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo } /** - * Convenience function to set scope data using module. - * @param module Module to show. + * Convenience function to set data to display. + * + * @param folderContents Contents to show. */ - protected showModuleData(module: any, folderContents: any): void { - this.description = module.intro || module.description; + protected showModuleData(folderContents: any): void { + this.description = this.folderInstance ? this.folderInstance.intro : this.module.description; - this.dataRetrieved.emit(module); - - if (this.path) { + if (this.subfolder) { // Subfolder. this.contents = folderContents; } else { @@ -107,25 +107,29 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo promise = this.folderProvider.getFolder(this.courseId, this.module.id).then((folder) => { return this.courseProvider.loadModuleContents(this.module, this.courseId, undefined, false, refresh).then(() => { folderContents = this.module.contents; + this.folderInstance = folder; return folder; }); }); } else { - promise = this.courseProvider.getModule(this.module.id, this.courseId).then((folder) => { - if (!folder.contents.length && this.module.contents.length && !this.appProvider.isOnline()) { + promise = this.courseProvider.getModule(this.module.id, this.courseId).then((module) => { + if (!module.contents.length && this.module.contents.length && !this.appProvider.isOnline()) { // The contents might be empty due to a cached data. Use the old ones. - folder.contents = this.module.contents; + module.contents = this.module.contents; } - this.module = folder; - folderContents = folder.contents; + this.module = module; + folderContents = module.contents; - return folder; + return module; }); } - return promise.then((folder) => { - this.showModuleData(folder, folderContents); + return promise.then(() => { + + this.dataRetrieved.emit(this.folderInstance || this.module); + + this.showModuleData(folderContents); }).finally(() => { this.fillContextMenu(refresh); }); diff --git a/src/addon/mod/folder/pages/index/index.html b/src/addon/mod/folder/pages/index/index.html index c435f6866..27f9f1509 100644 --- a/src/addon/mod/folder/pages/index/index.html +++ b/src/addon/mod/folder/pages/index/index.html @@ -8,9 +8,9 @@ - + - + diff --git a/src/addon/mod/folder/pages/index/index.ts b/src/addon/mod/folder/pages/index/index.ts index 74c63b22e..a64d7b499 100644 --- a/src/addon/mod/folder/pages/index/index.ts +++ b/src/addon/mod/folder/pages/index/index.ts @@ -30,13 +30,15 @@ export class AddonModFolderIndexPage { title: string; module: any; courseId: number; - path: string; + folderInstance: any; + subfolder: any; constructor(navParams: NavParams) { this.module = navParams.get('module') || {}; this.courseId = navParams.get('courseId'); - this.path = navParams.get('path'); - this.title = this.module.name; + this.folderInstance = navParams.get('folderInstance'); + this.subfolder = navParams.get('subfolder'); + this.title = this.subfolder ? this.subfolder.name : this.module.name; } /** diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html index e941c514a..7e7935ae6 100644 --- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html +++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html index 235c51297..0c01a1288 100644 --- a/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html +++ b/src/addon/mod/glossary/components/index/addon-mod-glossary-index.html @@ -14,7 +14,7 @@ - + diff --git a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html index 35cbae509..6be152d02 100644 --- a/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addon/mod/imscp/components/index/addon-mod-imscp-index.html @@ -9,7 +9,7 @@ - + diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html index f7ac672e0..834abcf38 100644 --- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/page/components/index/addon-mod-page-index.html b/src/addon/mod/page/components/index/addon-mod-page-index.html index 378796f11..12f6ddbd3 100644 --- a/src/addon/mod/page/components/index/addon-mod-page-index.html +++ b/src/addon/mod/page/components/index/addon-mod-page-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html index 88de59576..ec8685448 100644 --- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html +++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/resource/components/index/addon-mod-resource-index.html b/src/addon/mod/resource/components/index/addon-mod-resource-index.html index 7d2e36a94..9fcfdaa83 100644 --- a/src/addon/mod/resource/components/index/addon-mod-resource-index.html +++ b/src/addon/mod/resource/components/index/addon-mod-resource-index.html @@ -6,7 +6,7 @@ - + diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html index a9fcfd058..cd5e6b01a 100644 --- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html +++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/survey/components/index/addon-mod-survey-index.html b/src/addon/mod/survey/components/index/addon-mod-survey-index.html index 42c41ddd0..eb101c594 100644 --- a/src/addon/mod/survey/components/index/addon-mod-survey-index.html +++ b/src/addon/mod/survey/components/index/addon-mod-survey-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html index e043a6299..44a57c1ca 100644 --- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html +++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html @@ -19,7 +19,7 @@ - + diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html index 3ce7e2c32..c10216d25 100644 --- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html +++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html @@ -7,7 +7,7 @@ - + diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json index 3561e6111..3c3ea32d7 100644 --- a/src/assets/lang/en.json +++ b/src/assets/lang/en.json @@ -1390,6 +1390,7 @@ "core.course.allsections": "All sections", "core.course.askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", "core.course.availablespace": " You currently have about {{available}} free space.", + "core.course.cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "core.course.confirmdeletemodulefiles": "Are you sure you want to delete these files?", "core.course.confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "core.course.confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/core/course/classes/main-resource-component.ts b/src/core/course/classes/main-resource-component.ts index 50f432894..1bbff628d 100644 --- a/src/core/course/classes/main-resource-component.ts +++ b/src/core/course/classes/main-resource-component.ts @@ -49,6 +49,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, protected isDestroyed; // Whether the component is destroyed, used when calling fillContextMenu. protected contextMenuStatusObserver; // Observer of package status changed, used when calling fillContextMenu. + protected contextFileStatusObserver; // Observer of file status changed, 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. @@ -260,9 +261,17 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, /** * Confirm and remove downloaded files. + * + * @param done Function to call when done. */ - removeFiles(): void { - this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId); + removeFiles(done?: () => void): void { + if (this.prefetchStatus == CoreConstants.DOWNLOADING) { + this.domUtils.showAlertTranslated(null, 'core.course.cannotdeletewhiledownloading'); + + return; + } + + this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId, done); } /** @@ -285,6 +294,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, ngOnDestroy(): void { this.isDestroyed = true; this.contextMenuStatusObserver && this.contextMenuStatusObserver.off(); + this.contextFileStatusObserver && this.contextFileStatusObserver.off(); } /** diff --git a/src/core/course/lang/en.json b/src/core/course/lang/en.json index 2fe5518e9..69cbe5cf9 100644 --- a/src/core/course/lang/en.json +++ b/src/core/course/lang/en.json @@ -5,6 +5,7 @@ "allsections": "All sections", "askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.", "availablespace": " You currently have about {{available}} free space.", + "cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.", "confirmdeletemodulefiles": "Are you sure you want to delete these files?", "confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?", diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 7fbc8b740..b93d3191b 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -13,12 +13,12 @@ // limitations under the License. import { Injectable, Injector } from '@angular/core'; -import { NavController } from 'ionic-angular'; +import { NavController, Loading } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '@providers/app'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFilepoolProvider, CoreFilepoolComponentFileEventData } from '@providers/filepool'; import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreSitesProvider } from '@providers/sites'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; @@ -408,16 +408,29 @@ export class CoreCourseHelperProvider { * * @param module Module to remove the files. * @param courseId Course ID the module belongs to. + * @param done Function to call when done. It will close the context menu. * @return Promise resolved when done. */ - confirmAndRemoveFiles(module: any, courseId: number): Promise { - return this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles').then(() => { - return this.prefetchDelegate.removeModuleFiles(module, courseId); - }).catch((error) => { + async confirmAndRemoveFiles(module: any, courseId: number, done?: () => void): Promise { + let modal: Loading; + + try { + + await this.domUtils.showDeleteConfirm('core.course.confirmdeletemodulefiles'); + + modal = this.domUtils.showModalLoading(); + + await this.prefetchDelegate.removeModuleFiles(module, courseId); + + done && done(); + + } catch (error) { if (error) { this.domUtils.showErrorModal(error); } - }); + } finally { + modal && modal.dismiss(); + } } /** @@ -800,6 +813,8 @@ export class CoreCourseHelperProvider { * @return Promise resolved when done. */ fillContextMenu(instance: any, module: any, courseId: number, invalidateCache?: boolean, component?: string): Promise { + const siteId = this.sitesProvider.getCurrentSiteId(); + return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => { instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0; instance.prefetchStatusIcon = moduleInfo.statusIcon; @@ -825,7 +840,32 @@ export class CoreCourseHelperProvider { if (data.componentId == module.id && data.component == component) { this.fillContextMenu(instance, module, courseId, false, component); } - }, this.sitesProvider.getCurrentSiteId()); + }, siteId); + } + + if (typeof instance.contextFileStatusObserver == 'undefined' && component) { + // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity. + const debouncedUpdateSize = this.utils.debounce(() => { + this.prefetchDelegate.getModuleDownloadedSize(module, courseId).then((moduleSize) => { + instance.size = moduleSize > 0 ? this.textUtils.bytesToSize(moduleSize, 2) : 0; + }); + }, 1000); + + instance.contextFileStatusObserver = this.eventsProvider.on(CoreEventsProvider.COMPONENT_FILE_ACTION, + (data: CoreFilepoolComponentFileEventData) => { + + if (data.component != component || data.componentId != module.id) { + // The event doesn't belong to this component, ignore. + return; + } + + if (!this.filepoolProvider.isFileEventDownloadedOrDeleted(data)) { + return; + } + + // Update the module size. + debouncedUpdateSize(); + }, siteId); } }); } diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index 6e05b283b..ef0c1ad04 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -15,7 +15,7 @@ import { Injectable } from '@angular/core'; import { CoreEventsProvider } from '@providers/events'; import { CoreFileProvider } from '@providers/file'; -import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFilepoolProvider, CoreFilepoolComponentFileEventData } from '@providers/filepool'; import { CoreLoggerProvider } from '@providers/logger'; import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites'; import { CoreTimeUtilsProvider } from '@providers/utils/time'; @@ -276,6 +276,15 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { this.updateStatusCache(data.status, data.component, data.componentId); }, this.sitesProvider.getCurrentSiteId()); + + // If a file inside a module is downloaded/deleted, clear the corresponding cache. + eventsProvider.on(CoreEventsProvider.COMPONENT_FILE_ACTION, (data: CoreFilepoolComponentFileEventData) => { + if (!this.filepoolProvider.isFileEventDownloadedOrDeleted(data)) { + return; + } + + this.statusCache.invalidate(this.filepoolProvider.getPackageId(data.component, data.componentId)); + }, this.sitesProvider.getCurrentSiteId()); } /** diff --git a/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html b/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html index f3c8b832d..538055ea5 100644 --- a/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html +++ b/src/core/siteplugins/components/module-index/core-siteplugins-module-index.html @@ -5,7 +5,7 @@ - + diff --git a/src/providers/events.ts b/src/providers/events.ts index abed87a95..4da2900c0 100644 --- a/src/providers/events.ts +++ b/src/providers/events.ts @@ -47,6 +47,7 @@ export class CoreEventsProvider { static PACKAGE_STATUS_CHANGED = 'package_status_changed'; static COURSE_STATUS_CHANGED = 'course_status_changed'; static SECTION_STATUS_CHANGED = 'section_status_changed'; + static COMPONENT_FILE_ACTION = 'component_file_action'; static SITE_PLUGINS_LOADED = 'site_plugins_loaded'; static SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated'; static LOGIN_SITE_CHECKED = 'login_site_checked'; diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts index a4faa08f3..a3889773d 100644 --- a/src/providers/filepool.ts +++ b/src/providers/filepool.ts @@ -144,7 +144,7 @@ export interface CoreFilepoolQueueEntry { /** * File links (to link the file to components and componentIds). */ - links?: any[]; + links?: CoreFilepoolComponentLink[]; } /** @@ -197,6 +197,66 @@ export interface CoreFilepoolPackageEntry { extra?: string; } +/** + * A component link. + */ +export interface CoreFilepoolComponentLink { + /** + * Link's component. + */ + component: string; + + /** + * Link's componentId. + */ + componentId?: string | number; +} + +/** + * File actions. + */ +export const enum CoreFilepoolFileActions { + DOWNLOAD = 'download', + DOWNLOADING = 'downloading', + DELETED = 'deleted', + OUTDATED = 'outdated', +} + +/** + * Data sent to file events. + */ +export interface CoreFilepoolFileEventData { + /** + * The file ID. + */ + fileId: string; + + /** + * The file ID. + */ + action: CoreFilepoolFileActions; + + /** + * Whether the action was a success. Only for DOWNLOAD action. + */ + success?: boolean; +} + +/** + * Data sent to component file events. + */ +export interface CoreFilepoolComponentFileEventData extends CoreFilepoolFileEventData { + /** + * The component. + */ + component: string; + + /** + * The component ID. + */ + componentId: string | number; +} + /* * Factory for handling downloading files and retrieve downloaded files. * @@ -495,7 +555,7 @@ export class CoreFilepoolProvider { * @param links Array of objects containing the component and optionally componentId. * @return Promise resolved on success. */ - protected addFileLinks(siteId: string, fileId: string, links: any[]): Promise { + protected addFileLinks(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): Promise { const promises = []; links.forEach((link) => { promises.push(this.addFileLink(siteId, fileId, link.component, link.componentId)); @@ -575,7 +635,9 @@ export class CoreFilepoolProvider { * @return Promise resolved when the file is downloaded. */ protected async addToQueue(siteId: string, fileId: string, url: string, priority: number, revision: number, - timemodified: number, filePath: string, onProgress?: (event: any) => any, options: any = {}, link?: any): Promise { + timemodified: number, filePath: string, onProgress?: (event: any) => any, options: any = {}, + link?: CoreFilepoolComponentLink): Promise { + await this.dbReady; this.logger.debug(`Adding ${fileId} to the queue`); @@ -596,7 +658,7 @@ export class CoreFilepoolProvider { // Check if the queue is running. this.checkQueueProcessing(); - this.notifyFileDownloading(siteId, fileId); + this.notifyFileDownloading(siteId, fileId, link ? [link] : []); return this.getQueuePromise(siteId, fileId, true, onProgress); } @@ -623,7 +685,6 @@ export class CoreFilepoolProvider { await this.dbReady; let fileId, - link, queueDeferred; if (!this.fileProvider.isAvailable()) { @@ -651,12 +712,7 @@ export class CoreFilepoolProvider { const primaryKey = { siteId: siteId, fileId: fileId }; // Set up the component. - if (typeof component != 'undefined') { - link = { - component: component, - componentId: this.fixComponentId(componentId) - }; - } + const link = this.createComponentLink(component, componentId); // Retrieve the queue deferred now if it exists. // This is to prevent errors if file is removed from queue while we're checking if the file is in queue. @@ -866,7 +922,7 @@ export class CoreFilepoolProvider { return this.sitesProvider.getSiteDb(siteId).then((db) => { const conditions = { component: component, - componentId: componentId || '' + componentId: this.fixComponentId(componentId) }; return db.countRecords(this.LINKS_TABLE, conditions).then((count) => { @@ -877,6 +933,34 @@ export class CoreFilepoolProvider { }); } + /** + * Prepare a component link. + * + * @param component The component to link the file to. + * @param componentId An ID to use in conjunction with the component. + * @return Link, null if nothing to link. + */ + protected createComponentLink(component: string, componentId?: string | number): CoreFilepoolComponentLink { + if (typeof component != 'undefined' && component != null) { + return { component: component, componentId: this.fixComponentId(componentId) }; + } + + return null; + } + + /** + * Prepare list of links from component and componentId. + * + * @param component The component to link the file to. + * @param componentId An ID to use in conjunction with the component. + * @return Links. + */ + protected createComponentLinks(component: string, componentId?: string | number): CoreFilepoolComponentLink[] { + const link = this.createComponentLink(component, componentId); + + return link ? [link] : []; + } + /** * Given the current status of a list of packages and the status of one of the packages, * determine the new status for the list of packages. The status of a list of packages is: @@ -1179,8 +1263,9 @@ export class CoreFilepoolProvider { downloadUrl(siteId: string, fileUrl: string, ignoreStale?: boolean, component?: string, componentId?: string | number, timemodified: number = 0, onProgress?: (event: any) => any, filePath?: string, options: any = {}, revision?: number) : Promise { - let fileId, - promise; + let fileId; + let promise; + let alreadyDownloaded = true; if (this.fileProvider.isAvailable()) { return this.fixPluginfileURL(siteId, fileUrl).then((file) => { @@ -1193,18 +1278,22 @@ export class CoreFilepoolProvider { options.revision = revision || this.getRevisionFromUrl(fileUrl); fileId = this.getFileIdByUrl(fileUrl); + const links = this.createComponentLinks(component, componentId); + return this.hasFileInPool(siteId, fileId).then((fileObject) => { if (typeof fileObject === 'undefined') { // We do not have the file, download and add to pool. - this.notifyFileDownloading(siteId, fileId); + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); } else if (this.isFileOutdated(fileObject, options.revision, options.timemodified) && - this.appProvider.isOnline() && !ignoreStale) { + this.appProvider.isOnline() && !ignoreStale) { // The file is outdated, force the download and update it. - this.notifyFileDownloading(siteId, fileId); + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); } @@ -1220,14 +1309,16 @@ export class CoreFilepoolProvider { return response; }, () => { // The file was not found in the pool, weird. - this.notifyFileDownloading(siteId, fileId); + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, fileObject); }); }, () => { // The file is not in the pool just yet. - this.notifyFileDownloading(siteId, fileId); + this.notifyFileDownloading(siteId, fileId, links); + alreadyDownloaded = false; return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress); }).then((response) => { @@ -1236,11 +1327,14 @@ export class CoreFilepoolProvider { // Ignore errors. }); } - this.notifyFileDownloaded(siteId, fileId); + + if (!alreadyDownloaded) { + this.notifyFileDownloaded(siteId, fileId, links); + } return response; }, (err) => { - this.notifyFileDownloadError(siteId, fileId); + this.notifyFileDownloadError(siteId, fileId, links); return Promise.reject(err); }); @@ -1404,10 +1498,16 @@ export class CoreFilepoolProvider { protected getComponentFiles(db: SQLiteDB, component: string, componentId?: string | number): Promise { const conditions = { component: component, - componentId: componentId || '' + componentId: this.fixComponentId(componentId) }; - return db.getRecords(this.LINKS_TABLE, conditions); + return db.getRecords(this.LINKS_TABLE, conditions).then((items) => { + items.forEach((item) => { + item.componentId = this.fixComponentId(item.componentId); + }); + + return items; + }); } /** @@ -1516,6 +1616,12 @@ export class CoreFilepoolProvider { protected getFileLinks(siteId: string, fileId: string): Promise { return this.sitesProvider.getSiteDb(siteId).then((db) => { return db.getRecords(this.LINKS_TABLE, { fileId: fileId }); + }).then((items) => { + items.forEach((item) => { + item.componentId = this.fixComponentId(item.componentId); + }); + + return items; }); } @@ -2385,6 +2491,17 @@ export class CoreFilepoolProvider { }); } + /** + * Whether a file action indicates a file was downloaded or deleted. + * + * @param data Event data. + * @return Whether downloaded or deleted. + */ + isFileEventDownloadedOrDeleted(data: CoreFilepoolFileEventData): boolean { + return (data.action == CoreFilepoolFileActions.DOWNLOAD && data.success == true) || + data.action == CoreFilepoolFileActions.DELETED; + } + /** * Check whether a file is downloadable. * @@ -2439,14 +2556,41 @@ export class CoreFilepoolProvider { return !!entry.isexternalfile || (entry.revision < 1 && !entry.timemodified); } + /** + * Notify an action performed on a file to a list of components. + * + * @param siteId The site ID. + * @param eventData The file event data. + * @param links The links to the components. + */ + protected notifyFileActionToComponents(siteId: string, eventData: CoreFilepoolFileEventData, + links: CoreFilepoolComponentLink[]): void { + + links.forEach((link) => { + const data: CoreFilepoolComponentFileEventData = Object.assign({ + component: link.component, + componentId: link.componentId, + }, eventData); + + this.eventsProvider.trigger(CoreEventsProvider.COMPONENT_FILE_ACTION, data, siteId); + }); + } + /** * Notify a file has been deleted. * * @param siteId The site ID. * @param fileId The file ID. + * @param links The links to components. */ - protected notifyFileDeleted(siteId: string, fileId: string): void { - this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), { action: 'deleted' }); + protected notifyFileDeleted(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void { + const data: CoreFilepoolFileEventData = { + fileId: fileId, + action: CoreFilepoolFileActions.DELETED, + }; + + this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), data); + this.notifyFileActionToComponents(siteId, data, links); } /** @@ -2454,9 +2598,17 @@ export class CoreFilepoolProvider { * * @param siteId The site ID. * @param fileId The file ID. + * @param links The links to components. */ - protected notifyFileDownloaded(siteId: string, fileId: string): void { - this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), { action: 'download', success: true }); + protected notifyFileDownloaded(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void { + const data: CoreFilepoolFileEventData = { + fileId: fileId, + action: CoreFilepoolFileActions.DOWNLOAD, + success: true, + }; + + this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), data); + this.notifyFileActionToComponents(siteId, data, links); } /** @@ -2464,9 +2616,17 @@ export class CoreFilepoolProvider { * * @param siteId The site ID. * @param fileId The file ID. + * @param links The links to components. */ - protected notifyFileDownloadError(siteId: string, fileId: string): void { - this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), { action: 'download', success: false }); + protected notifyFileDownloadError(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void { + const data: CoreFilepoolFileEventData = { + fileId: fileId, + action: CoreFilepoolFileActions.DOWNLOAD, + success: false, + }; + + this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), data); + this.notifyFileActionToComponents(siteId, data, links); } /** @@ -2474,9 +2634,17 @@ export class CoreFilepoolProvider { * * @param siteId The site ID. * @param fileId The file ID. + * @param links The links to components. */ - protected notifyFileDownloading(siteId: string, fileId: string): void { - this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), { action: 'downloading' }); + protected notifyFileDownloading(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void { + const data: CoreFilepoolFileEventData = { + fileId: fileId, + action: CoreFilepoolFileActions.DOWNLOADING, + }; + + this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), data); + this.notifyFileActionToComponents(siteId, data, links); + } /** @@ -2484,9 +2652,16 @@ export class CoreFilepoolProvider { * * @param siteId The site ID. * @param fileId The file ID. + * @param links The links to components. */ - protected notifyFileOutdated(siteId: string, fileId: string): void { - this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), { action: 'outdated' }); + protected notifyFileOutdated(siteId: string, fileId: string, links: CoreFilepoolComponentLink[]): void { + const data: CoreFilepoolFileEventData = { + fileId: fileId, + action: CoreFilepoolFileActions.OUTDATED, + }; + + this.eventsProvider.trigger(this.getFileEventName(siteId, fileId), data); + this.notifyFileActionToComponents(siteId, data, links); } /** @@ -2612,7 +2787,6 @@ export class CoreFilepoolProvider { }).finally(() => { this.treatQueueDeferred(siteId, fileId, true); }); - this.notifyFileDownloaded(siteId, fileId); return; } @@ -2627,7 +2801,7 @@ export class CoreFilepoolProvider { }); this.treatQueueDeferred(siteId, fileId, true); - this.notifyFileDownloaded(siteId, fileId); + this.notifyFileDownloaded(siteId, fileId, links); // Wait for the item to be removed from queue before resolving the promise. // If the item could not be removed from queue we still resolve the promise. @@ -2677,12 +2851,12 @@ export class CoreFilepoolProvider { // Consider this as a silent error, never reject the promise here. }).then(() => { this.treatQueueDeferred(siteId, fileId, false, errorMessage); - this.notifyFileDownloadError(siteId, fileId); + this.notifyFileDownloadError(siteId, fileId, links); }); } else { // We considered the file as legit but did not get it, failure. this.treatQueueDeferred(siteId, fileId, false, errorMessage); - this.notifyFileDownloadError(siteId, fileId); + this.notifyFileDownloadError(siteId, fileId, links); return Promise.reject(errorObject); } @@ -2730,30 +2904,37 @@ export class CoreFilepoolProvider { // If file not found, use the path without extension. return path; }).then((path) => { - const promises = []; + const conditions = { + fileId: fileId + }; - // Remove entry from filepool store. - promises.push(db.deleteRecords(this.FILES_TABLE, { fileId: fileId })); + // Get links to components to notify them after remove. + return this.getFileLinks(siteId, fileId).then((links) => { + const promises = []; - // Remove links. - promises.push(db.deleteRecords(this.LINKS_TABLE, { fileId: fileId })); + // Remove entry from filepool store. + promises.push(db.deleteRecords(this.FILES_TABLE, conditions)); - // Remove the file. - if (this.fileProvider.isAvailable()) { - promises.push(this.fileProvider.removeFile(path).catch((error) => { - if (error && error.code == 1) { - // Not found, ignore error since maybe it was deleted already. - } else { - return Promise.reject(error); - } - })); - } + // Remove links. + promises.push(db.deleteRecords(this.LINKS_TABLE, conditions)); - return Promise.all(promises).then(() => { - this.notifyFileDeleted(siteId, fileId); + // Remove the file. + if (this.fileProvider.isAvailable()) { + promises.push(this.fileProvider.removeFile(path).catch((error) => { + if (error && error.code == 1) { + // Not found, ignore error since maybe it was deleted already. + } else { + return Promise.reject(error); + } + })); + } - return this.pluginFileDelegate.fileDeleted(fileUrl, path, siteId).catch((error) => { - // Ignore errors. + return Promise.all(promises).then(() => { + this.notifyFileDeleted(siteId, fileId, links); + + return this.pluginFileDelegate.fileDeleted(fileUrl, path, siteId).catch((error) => { + // Ignore errors. + }); }); }); }); diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index bedd0e4b0..bebc3ce1c 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -1392,4 +1392,27 @@ export class CoreUtilsProvider { return filtered; } + + /** + * Debounce a function so consecutive calls are ignored until a certain time has passed since the last call. + * + * @param context The context to apply to the function. + * @param fn Function to debounce. + * @param delay Time that must pass until the function is called. + * @return Debounced function. + */ + debounce(fn: (...args: any[]) => any, delay: number): (...args: any[]) => void { + + let timeoutID: number; + + const debounced = (...args: any[]): void => { + clearTimeout(timeoutID); + + timeoutID = window.setTimeout(() => { + fn.apply(null, args); + }, delay); + }; + + return debounced; + } }