+
-
+
+
-
-
- {{ 'core.course.hiddenfromstudents' | translate }}
-
-
-
+
\ No newline at end of file
diff --git a/src/core/course/components/module/module.scss b/src/core/course/components/module/module.scss
index 8eeee189e..c76d9e6f1 100644
--- a/src/core/course/components/module/module.scss
+++ b/src/core/course/components/module/module.scss
@@ -2,29 +2,116 @@ core-course-module {
a.core-course-module-handler {
align-items: flex-start;
- item-inner {
+ min-height: 52px;
+
+ &.item .item-inner {
padding-right: 0;
}
+ .label {
+ margin-top: 0;
+ margin-right: 0;
+ margin-bottom: 0;
+ }
+ .core-module-icon {
+ align-items: flex-start;
+ }
}
- .core-module-icon {
- align-items: flex-start;
- }
-
- .core-module-buttons {
+ .core-module-title {
display: flex;
flex-flow: row;
- align-items: center;
- z-index: 1;
- cursor: pointer;
- pointer-events: auto;
- position: absolute;
- right: 0;
- top: 4px;
+ align-items: flex-start;
- .spinner {
- right: 7px;
+ core-format-text {
+ flex-grow: 2;
+ }
+ .core-module-buttons {
+ margin: 0;
+ }
+
+ .core-module-buttons,
+ .core-module-buttons-more {
+ display: flex;
+ flex-flow: row;
+ align-items: center;
+ z-index: 1;
+ }
+
+ .core-module-buttons core-course-module-completion,
+ .core-module-buttons-more button {
+ cursor: pointer;
+ pointer-events: auto;
+ }
+
+ .core-module-buttons-more .spinner {
+ right: 13px;
position: absolute;
}
}
+}
+
+.md core-course-module {
+ .core-module-description,
+ .core-module-description .core-show-more {
+ padding-right: $label-md-margin-end;
+ }
+
+ a.core-course-module-handler .core-module-icon {
+ margin-top: $label-md-margin-top;
+ margin-bottom: $label-md-margin-bottom;
+ }
+
+ .core-module-title core-format-text {
+ padding-top: $label-md-margin-top + 3;
+ }
+ .button-md {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+ .core-module-buttons-more {
+ min-height: 52px;
+ min-width: 53px;
+ }
+}
+
+.ios core-course-module {
+ .core-module-description,
+ .core-module-description .core-show-more {
+ padding-right: $label-ios-margin-end;
+ }
+
+ a.core-course-module-handler .core-module-icon {
+ margin-top: $label-ios-margin-top;
+ margin-bottom: $label-ios-margin-bottom;
+ }
+
+ .core-module-title core-format-text {
+ padding-top: $label-ios-margin-top + 3;
+ }
+
+ .core-module-buttons-more {
+ min-height: 53px;
+ min-width: 58px;
+ }
+}
+
+.wp core-course-module {
+ .core-module-description,
+ .core-module-description .core-show-more {
+ padding-right: ($item-wp-padding-end / 2);
+ }
+
+ a.core-course-module-handler .core-module-icon {
+ margin-top: $item-wp-padding-top;
+ margin-bottom: $item-wp-padding-bottom;
+ }
+
+ .core-module-title core-format-text {
+ padding-top: $item-wp-padding-top + 3;
+ }
+
+ .button-wp {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
}
\ No newline at end of file
diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts
index b450093b1..15a66286b 100644
--- a/src/core/course/components/module/module.ts
+++ b/src/core/course/components/module/module.ts
@@ -12,9 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
+import { Component, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NavController } from 'ionic-angular';
+import { CoreEventsProvider } from '../../../../providers/events';
+import { CoreSitesProvider } from '../../../../providers/sites';
+import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
+import { CoreCourseHelperProvider } from '../../providers/helper';
import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate';
+import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '../../providers/module-prefetch-delegate';
+import { CoreConstants } from '../../../constants';
/**
* Component to display a module entry in a list of modules.
@@ -27,12 +33,43 @@ import { CoreCourseModuleHandlerButton } from '../../providers/module-delegate';
selector: 'core-course-module',
templateUrl: 'module.html'
})
-export class CoreCourseModuleComponent implements OnInit {
+export class CoreCourseModuleComponent implements OnInit, OnDestroy {
@Input() module: any; // The module to render.
@Input() courseId: number; // The course the module belongs to.
+ @Input('downloadEnabled') set enabled(value: boolean) {
+ this.downloadEnabled = value;
+
+ if (this.module.handlerData.showDownloadButton && this.downloadEnabled && !this.statusObserver) {
+ // First time that the download is enabled. Initialize the data.
+ this.spinner = true; // Show spinner while calculating the status.
+
+ this.prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(this.module);
+
+ // Get current status to decide which icon should be shown.
+ this.prefetchDelegate.getModuleStatus(this.module, this.courseId).then(this.showStatus.bind(this));
+
+ // Listen for changes on this module status.
+ this.statusObserver = this.eventsProvider.on(CoreEventsProvider.PACKAGE_STATUS_CHANGED, (data) => {
+ if (data.componentId === this.module.id && this.prefetchHandler &&
+ data.component === this.prefetchHandler.component) {
+ this.showStatus(data.status);
+ }
+ }, this.sitesProvider.getCurrentSiteId());
+ }
+ }
@Output() completionChanged?: EventEmitter
; // Will emit an event when the module completion changes.
- constructor(private navCtrl: NavController) {
+ showDownload: boolean; // Whether to display the download button.
+ showRefresh: boolean; // Whether to display the refresh button.
+ spinner: boolean; // Whether to display a spinner.
+ downloadEnabled: boolean; // Whether the download of sections and modules is enabled.
+
+ protected prefetchHandler: CoreCourseModulePrefetchHandler;
+ protected statusObserver;
+
+ constructor(protected navCtrl: NavController, protected prefetchDelegate: CoreCourseModulePrefetchDelegate,
+ protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider,
+ protected eventsProvider: CoreEventsProvider, protected sitesProvider: CoreSitesProvider) {
this.completionChanged = new EventEmitter();
}
@@ -68,4 +105,55 @@ export class CoreCourseModuleComponent implements OnInit {
button.action(event, this.navCtrl, this.module, this.courseId);
}
}
+
+ /**
+ * Download the module.
+ *
+ * @param {Event} event Click event.
+ * @param {boolean} refresh Whether it's refreshing.
+ */
+ download(event: Event, refresh: boolean): void {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (!this.prefetchHandler) {
+ return;
+ }
+
+ // Show spinner since this operation might take a while.
+ this.spinner = true;
+
+ // Get download size to ask for confirm if it's high.
+ this.prefetchHandler.getDownloadSize(module, this.courseId).then((size) => {
+ this.courseHelper.prefetchModule(this.prefetchHandler, this.module, size, this.courseId, refresh).catch((error) => {
+ // Error or cancelled.
+ this.spinner = false;
+ });
+ }).catch((error) => {
+ // Error getting download size, hide spinner.
+ this.spinner = false;
+ this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ });
+ }
+
+ /**
+ * Show download buttons according to module status.
+ *
+ * @param {string} status Module status.
+ */
+ protected showStatus(status: string): void {
+ if (status) {
+ this.spinner = status === CoreConstants.DOWNLOADING;
+ this.showDownload = status === CoreConstants.NOT_DOWNLOADED;
+ this.showRefresh = status === CoreConstants.OUTDATED ||
+ (!this.prefetchDelegate.canCheckUpdates() && status === CoreConstants.DOWNLOADED);
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ this.statusObserver && this.statusObserver.off();
+ }
}
diff --git a/src/core/course/formats/singleactivity/components/singleactivity.ts b/src/core/course/formats/singleactivity/components/singleactivity.ts
index cd5cae32c..a94f8730f 100644
--- a/src/core/course/formats/singleactivity/components/singleactivity.ts
+++ b/src/core/course/formats/singleactivity/components/singleactivity.ts
@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
+import { Component, Input, OnChanges, SimpleChange, ViewChild } from '@angular/core';
import { CoreCourseModuleDelegate } from '../../../providers/module-delegate';
import { CoreCourseUnsupportedModuleComponent } from '../../../components/unsupported-module/unsupported-module';
+import { CoreDynamicComponent } from '../../../../../components/dynamic-component/dynamic-component';
/**
* Component to display single activity format. It will determine the right component to use and instantiate it.
@@ -30,6 +31,8 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
@Input() sections: any[]; // List of course sections.
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
+ @ViewChild(CoreDynamicComponent) dynamicComponent: CoreDynamicComponent;
+
componentClass: any; // The class of the component to render.
data: any = {}; // Data to pass to the component.
@@ -52,4 +55,15 @@ export class CoreCourseFormatSingleActivityComponent implements OnChanges {
this.data.module = module;
}
}
+
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void): Promise {
+ return Promise.resolve(this.dynamicComponent.callComponentFunction('doRefresh', [refresher, done]));
+ }
}
diff --git a/src/core/course/formats/singleactivity/providers/handler.ts b/src/core/course/formats/singleactivity/providers/handler.ts
index 17f1a5734..ed7d44eef 100644
--- a/src/core/course/formats/singleactivity/providers/handler.ts
+++ b/src/core/course/formats/singleactivity/providers/handler.ts
@@ -62,6 +62,16 @@ export class CoreCourseFormatSingleActivityHandler implements CoreCourseFormatHa
return course.fullname || '';
}
+ /**
+ * Whether the option to enable section/module download should be displayed. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @return {boolean} Whether the option to enable section/module download should be displayed
+ */
+ displayEnableDownload(course: any): boolean {
+ return false;
+ }
+
/**
* Whether the default section selector should be displayed. Defaults to true.
*
diff --git a/src/core/course/pages/section/section.html b/src/core/course/pages/section/section.html
index 2360f470f..8420d3fb3 100644
--- a/src/core/course/pages/section/section.html
+++ b/src/core/course/pages/section/section.html
@@ -4,8 +4,8 @@
-
-
+
+
diff --git a/src/core/course/pages/section/section.ts b/src/core/course/pages/section/section.ts
index 35a434feb..d0cd1647c 100644
--- a/src/core/course/pages/section/section.ts
+++ b/src/core/course/pages/section/section.ts
@@ -24,6 +24,7 @@ import { CoreCourseHelperProvider } from '../../providers/helper';
import { CoreCourseFormatDelegate } from '../../providers/format-delegate';
import { CoreCourseModulePrefetchDelegate } from '../../providers/module-prefetch-delegate';
import { CoreCourseOptionsDelegate, CoreCourseOptionsHandlerToDisplay } from '../../providers/options-delegate';
+import { CoreCourseFormatComponent } from '../../components/format/format';
import { CoreCoursesProvider } from '../../../courses/providers/courses';
/**
@@ -36,6 +37,7 @@ import { CoreCoursesProvider } from '../../../courses/providers/courses';
})
export class CoreCourseSectionPage implements OnDestroy {
@ViewChild(Content) content: Content;
+ @ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent;
title: string;
course: any;
@@ -51,6 +53,7 @@ export class CoreCourseSectionPage implements OnDestroy {
prefetchCourseIcon: 'spinner'
};
moduleId: number;
+ displayEnableDownload: boolean;
protected module: any;
protected completionObserver;
@@ -71,6 +74,7 @@ export class CoreCourseSectionPage implements OnDestroy {
// Get the title to display. We dont't have sections yet.
this.title = courseFormatDelegate.getCourseTitle(this.course);
+ this.displayEnableDownload = courseFormatDelegate.displayEnableDownload(this.course);
this.completionObserver = eventsProvider.on(CoreEventsProvider.COMPLETION_MODULE_VIEWED, (data) => {
if (data && data.courseId == this.course.id) {
@@ -150,7 +154,19 @@ export class CoreCourseSectionPage implements OnDestroy {
promises.push(promise.then((completionStatus) => {
// Get all the sections.
- promises.push(this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
+ return this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
+ if (refresh) {
+ // Invalidate the recently downloaded module list. To ensure info can be prefetched.
+ const modules = this.courseProvider.getSectionsModules(sections);
+
+ return this.prefetchDelegate.invalidateModules(modules, this.course.id).then(() => {
+ return sections;
+ });
+ } else {
+ return sections;
+ }
+ }).then((sections) => {
+
this.courseHelper.addHandlerDataForModules(sections, this.course.id, completionStatus);
// Format the name of each section and check if it has content.
@@ -173,7 +189,7 @@ export class CoreCourseSectionPage implements OnDestroy {
// Get the title again now that we have sections.
this.title = this.courseFormatDelegate.getCourseTitle(this.course, this.sections);
- }));
+ });
}));
// Load the course handlers.
@@ -195,7 +211,9 @@ export class CoreCourseSectionPage implements OnDestroy {
doRefresh(refresher: any): void {
this.invalidateData().finally(() => {
this.loadData(true).finally(() => {
- refresher.complete();
+ this.formatComponent.doRefresh(refresher).finally(() => {
+ refresher.complete();
+ });
});
});
}
diff --git a/src/core/course/providers/default-format.ts b/src/core/course/providers/default-format.ts
index 7112efa56..2bf1991d1 100644
--- a/src/core/course/providers/default-format.ts
+++ b/src/core/course/providers/default-format.ts
@@ -56,6 +56,16 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
return true;
}
+ /**
+ * Whether the option to enable section/module download should be displayed. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @return {boolean} Whether the option to enable section/module download should be displayed
+ */
+ displayEnableDownload(course: any): boolean {
+ return true;
+ }
+
/**
* Whether the default section selector should be displayed. Defaults to true.
*
diff --git a/src/core/course/providers/format-delegate.ts b/src/core/course/providers/format-delegate.ts
index 27553d52b..48d43bd34 100644
--- a/src/core/course/providers/format-delegate.ts
+++ b/src/core/course/providers/format-delegate.ts
@@ -43,6 +43,14 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler {
*/
canViewAllSections?(course: any): boolean;
+ /**
+ * Whether the option to enable section/module download should be displayed. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @type {boolean} Whether the option to enable section/module download should be displayed.
+ */
+ displayEnableDownload?(course: any): boolean;
+
/**
* Whether the default section selector should be displayed. Defaults to true.
*
@@ -150,6 +158,16 @@ export class CoreCourseFormatDelegate extends CoreDelegate {
return this.executeFunction(course.format, 'canViewAllSections', [course]);
}
+ /**
+ * Whether the option to enable section/module download should be displayed. Defaults to true.
+ *
+ * @param {any} course The course to check.
+ * @return {boolean} Whether the option to enable section/module download should be displayed
+ */
+ displayEnableDownload(course: any): boolean {
+ return this.executeFunction(course.format, 'displayEnableDownload', [course]);
+ }
+
/**
* Whether the default section selector should be displayed. Defaults to true.
*
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-delegate.ts b/src/core/course/providers/module-delegate.ts
index dd2dec7c9..1a2e8c057 100644
--- a/src/core/course/providers/module-delegate.ts
+++ b/src/core/course/providers/module-delegate.ts
@@ -37,6 +37,7 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
/**
* Get the component to render the module. This is needed to support singleactivity course format.
+ * The component returned must implement CoreCourseModuleMainComponent.
*
* @param {any} course The course object.
* @param {any} module The module object.
@@ -67,6 +68,14 @@ export interface CoreCourseModuleHandlerData {
*/
class?: string;
+ /**
+ * Whether to display a button to download/refresh the module if it's downloadable.
+ * If it's set to true, the app will show a download/refresh button when needed and will handle the download of the
+ * module using CoreCourseModulePrefetchDelegate.
+ * @type {boolean}
+ */
+ showDownloadButton?: boolean;
+
/**
* The buttons to display in the module item.
* @type {CoreCourseModuleHandlerButton[]}
@@ -91,6 +100,20 @@ export interface CoreCourseModuleHandlerData {
action?(event: Event, navCtrl: NavController, module: any, courseId: number, options?: NavOptions): void;
}
+/**
+ * Interface that all the components to render the module in singleactivity must implement.
+ */
+export interface CoreCourseModuleMainComponent {
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void): Promise;
+}
+
/**
* A button to display in a module item.
*/
diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts
index 6980c3cad..a80e9c74f 100644
--- a/src/core/course/providers/module-prefetch-delegate.ts
+++ b/src/core/course/providers/module-prefetch-delegate.ts
@@ -202,9 +202,6 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
};
protected ROOT_CACHE_KEY = 'mmCourse:';
-
- protected handlers: { [s: string]: CoreCourseModulePrefetchHandler } = {}; // All registered handlers.
- protected enabledHandlers: { [s: string]: CoreCourseModulePrefetchHandler } = {}; // Handlers enabled for the current site.
protected statusCache = new CoreCache();
// Promises for check updates, to prevent performing the same request twice at the same time.
@@ -225,9 +222,14 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
private courseProvider: CoreCourseProvider, private filepoolProvider: CoreFilepoolProvider,
private timeUtils: CoreTimeUtilsProvider, private fileProvider: CoreFileProvider,
protected eventsProvider: CoreEventsProvider) {
- super('CoreCourseModulePrefetchDelegate', loggerProvider, sitesProvider);
+ 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());
}
/**
@@ -656,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));
}
@@ -667,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;
}
@@ -699,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;
@@ -713,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);
@@ -773,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++;
@@ -859,7 +859,7 @@ export class CoreCourseModulePrefetchDelegate extends CoreDelegate {
* @return {CoreCourseModulePrefetchHandler} Prefetch handler.
*/
getPrefetchHandlerFor(module: any): CoreCourseModulePrefetchHandler {
- return this.enabledHandlers[module.modname];
+ return this.getHandler(module.modname, true);
}
/**
@@ -1126,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);
}
});
}
@@ -1147,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.
*
@@ -1184,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);
@@ -1198,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);
}
@@ -1208,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
diff --git a/src/core/user/providers/user-profile-field-delegate.ts b/src/core/user/providers/user-profile-field-delegate.ts
index 5f924896d..c3e8b7282 100644
--- a/src/core/user/providers/user-profile-field-delegate.ts
+++ b/src/core/user/providers/user-profile-field-delegate.ts
@@ -99,7 +99,8 @@ export class CoreUserProfileFieldDelegate extends CoreDelegate {
*/
getDataForField(field: any, signup: boolean, registerAuth: string, formValues: any): Promise {
const type = field.type || field.datatype,
- handler = this.getHandler(type, !signup);
+ handler = this.getHandler(type, !signup);
+
if (handler) {
const name = 'profile_field_' + field.shortname;
if (handler.getData) {
diff --git a/src/directives/link.ts b/src/directives/link.ts
index d565a90c5..2069ce479 100644
--- a/src/directives/link.ts
+++ b/src/directives/link.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Directive, Input, OnInit, ElementRef } from '@angular/core';
+import { Directive, Input, OnInit, ElementRef, Optional } from '@angular/core';
import { NavController, Content } from 'ionic-angular';
import { CoreSitesProvider } from '../providers/sites';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
@@ -40,7 +40,7 @@ export class CoreLinkDirective implements OnInit {
constructor(element: ElementRef, private domUtils: CoreDomUtilsProvider, private utils: CoreUtilsProvider,
private sitesProvider: CoreSitesProvider, private urlUtils: CoreUrlUtilsProvider,
private contentLinksHelper: CoreContentLinksHelperProvider, private navCtrl: NavController,
- private content: Content) {
+ @Optional() private content: Content) {
// This directive can be added dynamically. In that case, the first param is the anchor HTMLElement.
this.element = element.nativeElement || element;
}
@@ -81,7 +81,7 @@ export class CoreLinkDirective implements OnInit {
protected navigate(href: string): void {
const contentLinksScheme = CoreConfigConstants.customurlscheme + '://link=';
- if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0) {
+ if (href.indexOf('cdvfile://') === 0 || href.indexOf('file://') === 0 || href.indexOf('filesystem:') === 0) {
// We have a local file.
this.utils.openFile(href).catch((error) => {
this.domUtils.showErrorModal(error);
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index 80388199e..ab2dba3e6 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -477,7 +477,7 @@ export class CoreFilepoolProvider {
componentId: componentId || ''
};
- return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, undefined);
+ return db.insertOrUpdateRecord(this.LINKS_TABLE, newEntry, { fileId: fileId });
});
}
@@ -1178,7 +1178,9 @@ export class CoreFilepoolProvider {
return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress);
}).then((response) => {
if (typeof component != 'undefined') {
- this.addFileLink(siteId, fileId, component, componentId);
+ this.addFileLink(siteId, fileId, component, componentId).catch(() => {
+ // Ignore errors.
+ });
}
this.notifyFileDownloaded(siteId, fileId);
@@ -2237,9 +2239,11 @@ export class CoreFilepoolProvider {
}),
whereAndParams = db.getInOrEqual(fileIds);
+ whereAndParams[0] = 'fileId ' + whereAndParams[0];
+
if (onlyUnknown) {
whereAndParams[0] += ' AND (isexternalfile = ? OR (revision < ? AND timemodified = ?))';
- whereAndParams[1] = whereAndParams[1].params.concat([0, 1, 0]);
+ whereAndParams[1] = whereAndParams[1].concat([0, 1, 0]);
}
return db.updateRecordsWhere(this.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]);
@@ -2443,8 +2447,12 @@ export class CoreFilepoolProvider {
if (entry && !this.isFileOutdated(entry, options.revision, options.timemodified)) {
// We have the file, it is not stale, we can update links and remove from queue.
this.logger.debug('Queued file already in store, ignoring...');
- this.addFileLinks(siteId, fileId, links);
- this.removeFromQueue(siteId, fileId).finally(() => {
+ this.addFileLinks(siteId, fileId, links).catch(() => {
+ // Ignore errors.
+ });
+ this.removeFromQueue(siteId, fileId).catch(() => {
+ // Ignore errors.
+ }).finally(() => {
this.treatQueueDeferred(siteId, fileId, true);
});
this.notifyFileDownloaded(siteId, fileId);
@@ -2457,7 +2465,9 @@ export class CoreFilepoolProvider {
return this.downloadForPoolByUrl(siteId, fileUrl, options, filePath, onProgress, entry).then(() => {
// Success, we add links and remove from queue.
- this.addFileLinks(siteId, fileId, links);
+ this.addFileLinks(siteId, fileId, links).catch(() => {
+ // Ignore errors.
+ });
this.treatQueueDeferred(siteId, fileId, true);
this.notifyFileDownloaded(siteId, fileId);
diff --git a/src/providers/utils/dom.ts b/src/providers/utils/dom.ts
index 3578d8b9a..7706c15fd 100644
--- a/src/providers/utils/dom.ts
+++ b/src/providers/utils/dom.ts
@@ -32,9 +32,12 @@ export class CoreDomUtilsProvider {
// List of input types that support keyboard.
protected INPUT_SUPPORT_KEYBOARD = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', 'password',
'search', 'tel', 'text', 'time', 'url', 'week'];
+ protected INSTANCE_ID_ATTR_NAME = 'core-instance-id';
protected element = document.createElement('div'); // Fake element to use in some functions, to prevent creating it each time.
protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call.
+ protected instances: {[id: string]: any} = {}; // Store component/directive instances by id.
+ protected lastInstanceId = 0;
constructor(private translate: TranslateService, private loadingCtrl: LoadingController, private toastCtrl: ToastController,
private alertCtrl: AlertController, private textUtils: CoreTextUtilsProvider, private appProvider: CoreAppProvider,
@@ -410,6 +413,20 @@ export class CoreDomUtilsProvider {
return this.textUtils.decodeHTML(this.translate.instant('core.error'));
}
+ /**
+ * Retrieve component/directive instance.
+ * Please use this function only if you cannot retrieve the instance using parent/child methods: ViewChild (or similar)
+ * or Angular's injection.
+ *
+ * @param {Element} element The root element of the component/directive.
+ * @return {any} The instance, undefined if not found.
+ */
+ getInstanceByElement(element: Element): any {
+ const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
+
+ return this.instances[id];
+ }
+
/**
* Check if an element is outside of screen (viewport).
*
@@ -513,6 +530,25 @@ export class CoreDomUtilsProvider {
return this.element.innerHTML;
}
+ /**
+ * Remove a component/directive instance using the DOM Element.
+ *
+ * @param {Element} element The root element of the component/directive.
+ */
+ removeInstanceByElement(element: Element): void {
+ const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
+ delete this.instances[id];
+ }
+
+ /**
+ * Remove a component/directive instance using the ID.
+ *
+ * @param {string} id The ID to remove.
+ */
+ removeInstanceById(id: string): void {
+ delete this.instances[id];
+ }
+
/**
* Search for certain classes in an element contents and replace them with the specified new values.
*
@@ -547,28 +583,26 @@ export class CoreDomUtilsProvider {
// Treat elements with src (img, audio, video, ...).
media = this.element.querySelectorAll('img, video, audio, source, track');
- for (const i in media) {
- const el = media[i];
- let newSrc = paths[this.textUtils.decodeURIComponent(el.getAttribute('src'))];
+ media.forEach((media: HTMLElement) => {
+ let newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('src'))];
if (typeof newSrc != 'undefined') {
- el.setAttribute('src', newSrc);
+ media.setAttribute('src', newSrc);
}
// Treat video posters.
- if (el.tagName == 'VIDEO' && el.getAttribute('poster')) {
- newSrc = paths[this.textUtils.decodeURIComponent(el.getAttribute('poster'))];
+ if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
+ newSrc = paths[this.textUtils.decodeURIComponent(media.getAttribute('poster'))];
if (typeof newSrc !== 'undefined') {
- el.setAttribute('poster', newSrc);
+ media.setAttribute('poster', newSrc);
}
}
- }
+ });
// Now treat links.
anchors = this.element.querySelectorAll('a');
- for (const i in anchors) {
- const anchor = anchors[i],
- href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')),
+ anchors.forEach((anchor: HTMLElement) => {
+ const href = this.textUtils.decodeURIComponent(anchor.getAttribute('href')),
newUrl = paths[href];
if (typeof newUrl != 'undefined') {
@@ -578,7 +612,7 @@ export class CoreDomUtilsProvider {
anchorFn(anchor, href);
}
}
- }
+ });
return this.element.innerHTML;
}
@@ -885,6 +919,22 @@ export class CoreDomUtilsProvider {
return loader;
}
+ /**
+ * Stores a component/directive instance.
+ *
+ * @param {Element} element The root element of the component/directive.
+ * @param {any} instance The instance to store.
+ * @return {string} ID to identify the instance.
+ */
+ storeInstanceByElement(element: Element, instance: any): string {
+ const id = String(this.lastInstanceId++);
+
+ element.setAttribute(this.INSTANCE_ID_ATTR_NAME, id);
+ this.instances[id] = instance;
+
+ return id;
+ }
+
/**
* Check if an element supports input via keyboard.
*