From e8cbead5d614d37d48e93bb5d927fefcf3834cf8 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 2 Feb 2018 13:10:19 +0100 Subject: [PATCH] MOBILE-2335 book: Support context menu prefetch and size --- .../mod/book/components/index/index.html | 4 +- src/addon/mod/book/components/index/index.ts | 24 ++++-- src/components/context-menu/context-menu.scss | 6 ++ src/core/course/providers/helper.ts | 78 ++++++++++++++++++- .../providers/module-prefetch-delegate.ts | 50 ++++++++---- 5 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 src/components/context-menu/context-menu.scss diff --git a/src/addon/mod/book/components/index/index.html b/src/addon/mod/book/components/index/index.html index dcfef3a15..5463e70a5 100644 --- a/src/addon/mod/book/components/index/index.html +++ b/src/addon/mod/book/components/index/index.html @@ -7,8 +7,8 @@ - - + + diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts index 64b9ea7a0..48e2592ed 100644 --- a/src/addon/mod/book/components/index/index.ts +++ b/src/addon/mod/book/components/index/index.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, Input, Output, EventEmitter, Optional } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input, Output, EventEmitter, Optional } from '@angular/core'; import { NavParams, NavController, Content, PopoverController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; import { CoreAppProvider } from '../../../../../providers/app'; @@ -32,24 +32,31 @@ import { AddonModBookTocPopoverComponent } from '../../components/toc-popover/to selector: 'addon-mod-book-index', templateUrl: 'index.html', }) -export class AddonModBookIndexComponent implements OnInit, CoreCourseModuleMainComponent { +export class AddonModBookIndexComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent { @Input() module: any; // The module of the book. @Input() courseId: number; // Course ID the book belongs to. @Output() bookRetrieved?: EventEmitter; - externalUrl: string; - description: string; loaded: boolean; component = AddonModBookProvider.COMPONENT; componentId: number; chapterContent: string; previousChapter: string; nextChapter: string; + + // Data for context menu. + externalUrl: string; + description: string; refreshIcon: string; + prefetchStatusIcon: string; + prefetchText: string; + size: string; protected chapters: AddonModBookTocChapter[]; protected currentChapter: string; protected contentsMap: AddonModBookContentsMap; + protected isDestroyed = false; + protected statusObserver; constructor(private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider, @@ -136,7 +143,7 @@ export class AddonModBookIndexComponent implements OnInit, CoreCourseModuleMainC * Prefetch the module. */ prefetch(): void { - // @todo this.courseHelper.contextMenuPrefetch($scope, this.module, this.courseId); + this.courseHelper.contextMenuPrefetch(this, this.module, this.courseId); } /** @@ -192,7 +199,7 @@ export class AddonModBookIndexComponent implements OnInit, CoreCourseModuleMainC } // All data obtained, now fill the context menu. - // @todo this.courseHelper.fillContextMenu($scope, module, courseId, refresh, mmaModBookComponent); + this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); }).catch(() => { // Ignore errors, they're handled inside the loadChapter function. }); @@ -235,4 +242,9 @@ export class AddonModBookIndexComponent implements OnInit, CoreCourseModuleMainC this.refreshIcon = 'refresh'; }); } + + ngOnDestroy(): void { + this.isDestroyed = true; + this.statusObserver && this.statusObserver.off(); + } } diff --git a/src/components/context-menu/context-menu.scss b/src/components/context-menu/context-menu.scss new file mode 100644 index 000000000..9730ca1e4 --- /dev/null +++ b/src/components/context-menu/context-menu.scss @@ -0,0 +1,6 @@ +core-context-menu-popover { + .item-md ion-icon[item-start] + .item-inner, + .item-md ion-icon[item-start] + .item-input { + @include margin-horizontal(5px, null); + } +} diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index d4b76e357..36fbc2433 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { NavController } from 'ionic-angular'; import { TranslateService } from '@ngx-translate/core'; +import { CoreEventsProvider } from '../../../providers/events'; import { CoreFilepoolProvider } from '../../../providers/filepool'; import { CoreSitesProvider } from '../../../providers/sites'; import { CoreDomUtilsProvider } from '../../../providers/utils/dom'; @@ -114,7 +115,8 @@ export class CoreCourseHelperProvider { private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider, - private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider) { } + private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider, + private eventsProvider: CoreEventsProvider) { } /** * This function treats every module on the sections provided to load the handler data, treat completion @@ -358,8 +360,12 @@ export class CoreCourseHelperProvider { * @return {Promise} Promise resolved when done. */ confirmAndRemoveFiles(module: any, courseId: number): Promise { - return this.domUtils.showConfirm(this.translate.instant('course.confirmdeletemodulefiles')).then(() => { + return this.domUtils.showConfirm(this.translate.instant('core.course.confirmdeletemodulefiles')).then(() => { return this.prefetchDelegate.removeModuleFiles(module, courseId); + }).catch((error) => { + if (error) { + this.domUtils.showErrorModal(error); + } }); } @@ -405,6 +411,39 @@ export class CoreCourseHelperProvider { }); } + /** + * Helper function to prefetch a module, showing a confirmation modal if the size is big. + * This function is meant to be called from a context menu option. It will also modify some data like the prefetch icon. + * + * @param {any} instance The component instance that has the context menu. It should have prefetchStatusIcon and isDestroyed. + * @param {any} module Module to be prefetched + * @param {number} courseId Course ID the module belongs to. + * @return {Promise} Promise resolved when done. + */ + contextMenuPrefetch(instance: any, module: any, courseId: number): Promise { + const initialIcon = instance.prefetchStatusIcon; + let cancelled = false; + + instance.prefetchStatusIcon = 'spinner'; // Show spinner since this operation might take a while. + + // We need to call getDownloadSize, the package might have been updated. + return this.prefetchDelegate.getModuleDownloadSize(module, courseId, true).then((size) => { + return this.domUtils.confirmDownloadSize(size).catch(() => { + // User hasn't confirmed, stop. + cancelled = true; + + return Promise.reject(null); + }).then(() => { + return this.prefetchDelegate.prefetchModule(module, courseId, true); + }); + }).catch((error) => { + instance.prefetchStatusIcon = initialIcon; + if (!instance.isDestroyed && !cancelled) { + this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true); + } + }); + } + /** * Determine the status of a list of courses. * @@ -431,6 +470,41 @@ export class CoreCourseHelperProvider { }); } + /** + * Fill the Context Menu for a certain module. + * + * @param {any} instance The component instance that has the context menu. + * @param {any} module Module to be prefetched + * @param {number} courseId Course ID the module belongs to. + * @param {boolean} [invalidateCache] Invalidates the cache first. + * @param {string} [component] Component of the module. + * @return {Promise} Promise resolved when done. + */ + fillContextMenu(instance: any, module: any, courseId: number, invalidateCache?: boolean, component?: string): Promise { + return this.getModulePrefetchInfo(module, courseId, invalidateCache, component).then((moduleInfo) => { + instance.size = moduleInfo.size > 0 ? moduleInfo.sizeReadable : 0; + instance.prefetchStatusIcon = moduleInfo.statusIcon; + + if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) { + // Module is downloadable, get the text to display to prefetch. + if (moduleInfo.downloadTime > 0) { + instance.prefetchText = this.translate.instant('core.lastdownloaded') + ': ' + moduleInfo.downloadTimeReadable; + } else { + // Module not downloaded, show a default text. + instance.prefetchText = this.translate.instant('core.download'); + } + } + + if (typeof instance.statusObserver == 'undefined' && component) { + instance.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { + if (data.componentId == module.id && data.component == component) { + this.fillContextMenu(instance, module, courseId, false, component); + } + }, this.sitesProvider.getCurrentSiteId()); + } + }); + } + /** * Get a course download promise (if any). * diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts index de95a6287..a80e9c74f 100644 --- a/src/core/course/providers/module-prefetch-delegate.ts +++ b/src/core/course/providers/module-prefetch-delegate.ts @@ -225,6 +225,11 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider, eventsProvider); this.sitesProvider.createTableFromSchema(this.checkUpdatesTableSchema); + + eventsProvider.on(CoreEventsProvider.LOGOUT, this.clearStatusCache.bind(this)); + eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => { + this.updateStatusCache(data.status, data.component, data.componentId); + }, this.sitesProvider.getCurrentSiteId()); } /** @@ -653,6 +658,8 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { promise; if (!refresh && typeof status != 'undefined') { + this.storeCourseAndSection(packageId, courseId, sectionId); + return Promise.resolve(this.determineModuleStatus(module, status, canCheck)); } @@ -664,7 +671,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { // Get the saved package status. return this.filepoolProvider.getPackageStatus(siteId, component, module.id).then((currentStatus) => { - status = handler.determineStatus ? handler.determineStatus(module, status, canCheck) : status; + status = handler.determineStatus ? handler.determineStatus(module, currentStatus, canCheck) : currentStatus; if (status != CoreConstants.DOWNLOADED) { return status; } @@ -696,7 +703,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { // Has updates, mark the module as outdated. status = CoreConstants.OUTDATED; - return this.filepoolProvider.storePackageStatus(siteId, component, module.id, status).catch(() => { + return this.filepoolProvider.storePackageStatus(siteId, status, component, module.id).catch(() => { // Ignore errors. }).then(() => { return status; @@ -710,13 +717,14 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { }, () => { // Error getting updates, show the stored status. updateStatus = false; + this.storeCourseAndSection(packageId, courseId, sectionId); return currentStatus; }); }); }).then((status) => { if (updateStatus) { - this.updateStatusCache(status, courseId, component, module.id, sectionId); + this.updateStatusCache(status, component, module.id, courseId, sectionId); } return this.determineModuleStatus(module, status, canCheck); @@ -770,11 +778,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { promises.push(this.getModuleStatus(module, courseId, updates, refresh).then((modStatus) => { if (modStatus != CoreConstants.NOT_DOWNLOADABLE) { - if (sectionId && sectionId > 0) { - // Store the section ID. - this.statusCache.setValue(packageId, 'sectionId', sectionId); - } - status = this.filepoolProvider.determinePackagesStatus(status, modStatus); result[modStatus].push(module); result.total++; @@ -1123,7 +1126,8 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { // Update status of the module. const packageId = this.filepoolProvider.getPackageId(handler.component, module.id); this.statusCache.setValue(packageId, 'downloadedSize', 0); - this.filepoolProvider.storePackageStatus(siteId, handler.component, module.id, CoreConstants.NOT_DOWNLOADED); + + return this.filepoolProvider.storePackageStatus(siteId, CoreConstants.NOT_DOWNLOADED, handler.component, module.id); } }); } @@ -1144,6 +1148,22 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { } } + /** + * If courseId or sectionId is set, save them in the cache. + * + * @param {string} packageId The package ID. + * @param {number} [courseId] Course ID. + * @param {number} [sectionId] Section ID. + */ + storeCourseAndSection(packageId: string, courseId?: number, sectionId?: number): void { + if (courseId) { + this.statusCache.setValue(packageId, 'courseId', courseId); + } + if (sectionId && sectionId > 0) { + this.statusCache.setValue(packageId, 'sectionId', sectionId); + } + } + /** * Treat the result of the check updates WS call. * @@ -1181,12 +1201,12 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { * Update the status of a module in the "cache". * * @param {string} status New status. - * @param {number} courseId Course ID of the module. * @param {string} component Package's component. * @param {string|number} [componentId] An ID to use in conjunction with the component. + * @param {number} [courseId] Course ID of the module. * @param {number} [sectionId] Section ID of the module. */ - updateStatusCache(status: string, courseId: number, component: string, componentId?: string | number, sectionId?: number) + updateStatusCache(status: string, component: string, componentId?: string | number, courseId?: number, sectionId?: number) : void { const packageId = this.filepoolProvider.getPackageId(component, componentId), cachedStatus = this.statusCache.getValue(packageId, 'status', true); @@ -1195,7 +1215,13 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { // If the status has changed, notify that the section has changed. notify = typeof cachedStatus != 'undefined' && cachedStatus !== status; + // If courseId/sectionId is set, store it. + this.storeCourseAndSection(packageId, courseId, sectionId); + if (notify) { + if (!courseId) { + courseId = this.statusCache.getValue(packageId, 'courseId', true); + } if (!sectionId) { sectionId = this.statusCache.getValue(packageId, 'sectionId', true); } @@ -1205,8 +1231,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate { this.statusCache.setValue(packageId, 'status', status); if (sectionId) { - this.statusCache.setValue(packageId, 'sectionId', sectionId); - this.eventsProvider.trigger(CoreEventsProvider.SECTION_STATUS_CHANGED, { sectionId: sectionId, courseId: courseId