diff --git a/src/components/compile-html/compile-html.ts b/src/components/compile-html/compile-html.ts
index 26b3cc480..44cb24bac 100644
--- a/src/components/compile-html/compile-html.ts
+++ b/src/components/compile-html/compile-html.ts
@@ -28,6 +28,7 @@ import { CoreComponentsModule } from '../components.module';
import { CoreDirectivesModule } from '../../directives/directives.module';
import { CorePipesModule } from '../../pipes/pipes.module';
import { CoreCourseComponentsModule } from '../../core/course/components/components.module';
+import { CoreCourseDirectivesModule } from '../../core/course/directives/directives.module';
import { CoreCoursesComponentsModule } from '../../core/courses/components/components.module';
import { CoreSiteHomeComponentsModule } from '../../core/sitehome/components/components.module';
import { CoreUserComponentsModule } from '../../core/user/components/components.module';
@@ -80,7 +81,8 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy {
// List of imports for dynamic module. Since the template can have any component we need to import all core components modules.
protected IMPORTS = [
IonicModule, TranslateModule.forChild(), CoreComponentsModule, CoreDirectivesModule, CorePipesModule,
- CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule
+ CoreCourseComponentsModule, CoreCoursesComponentsModule, CoreSiteHomeComponentsModule, CoreUserComponentsModule,
+ CoreCourseDirectivesModule
];
// Other Ionic/Angular providers that don't depend on where they are injected.
diff --git a/src/core/course/directives/directives.module.ts b/src/core/course/directives/directives.module.ts
new file mode 100644
index 000000000..9b43d41cc
--- /dev/null
+++ b/src/core/course/directives/directives.module.ts
@@ -0,0 +1,27 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { NgModule } from '@angular/core';
+import { CoreCourseDownloadModuleMainFileDirective } from './download-module-main-file';
+
+@NgModule({
+ declarations: [
+ CoreCourseDownloadModuleMainFileDirective
+ ],
+ imports: [],
+ exports: [
+ CoreCourseDownloadModuleMainFileDirective
+ ]
+})
+export class CoreCourseDirectivesModule {}
diff --git a/src/core/course/directives/download-module-main-file.ts b/src/core/course/directives/download-module-main-file.ts
new file mode 100644
index 000000000..b2a8d4324
--- /dev/null
+++ b/src/core/course/directives/download-module-main-file.ts
@@ -0,0 +1,83 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// 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 { Directive, Input, OnInit, ElementRef } from '@angular/core';
+import { NavController } from 'ionic-angular';
+import { CoreCourseProvider } from '../providers/course';
+import { CoreCourseHelperProvider } from '../providers/helper';
+import { CoreDomUtilsProvider } from '../../../providers/utils/dom';
+import { CoreUtilsProvider } from '../../../providers/utils/utils';
+
+/**
+ * Directive to allow downloading and open the main file of a module.
+ * When the item with this directive is clicked, the module will be downloaded (if needed) and opened.
+ * This is meant for modules like mod_resource.
+ *
+ * This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.
+ */
+@Directive({
+ selector: '[core-course-download-module-main-file]'
+})
+export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
+ @Input() module: any; // The module.
+ @Input() moduleId: string | number; // The module ID. Required if module is not supplied.
+ @Input() courseId: string | number; // The course ID.
+ @Input() component?: string; // Component to link the file to.
+ @Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId.
+ @Input() files?: any[]; // List of files of the module. If not provided, use module.contents.
+
+ protected element: HTMLElement;
+
+ constructor(element: ElementRef, protected domUtils: CoreDomUtilsProvider, protected courseHelper: CoreCourseHelperProvider,
+ protected courseProvider: CoreCourseProvider) {
+ this.element = element.nativeElement || element;
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ this.element.addEventListener('click', (ev: Event): void => {
+ if (!this.module && !this.moduleId) {
+ return;
+ }
+
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ const modal = this.domUtils.showModalLoading(),
+ courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
+ let promise;
+
+ if (this.module) {
+ // We already have the module.
+ promise = Promise.resolve(module);
+ } else {
+ // Try to get the module from cache.
+ this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
+ promise = this.courseProvider.getModule(this.moduleId, courseId);
+ }
+
+ promise.then((module) => {
+ const componentId = this.componentId || module.id;
+
+ return this.courseHelper.downloadModuleAndOpenFile(module, courseId, this.component, componentId, this.files);
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ }).finally(() => {
+ modal.dismiss();
+ });
+ });
+ }
+}
diff --git a/src/core/course/providers/course.ts b/src/core/course/providers/course.ts
index 9239b8921..083ed039f 100644
--- a/src/core/course/providers/course.ts
+++ b/src/core/course/providers/course.ts
@@ -201,7 +201,7 @@ export class CoreCourseProvider {
* @return {Promise} Promise resolved with the module.
*/
getModule(moduleId: number, courseId?: number, sectionId?: number, preferCache?: boolean, ignoreCache?: boolean,
- siteId?: string): Promise {
+ siteId?: string): Promise {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
let promise;
diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts
index fedffa140..5bd0893d9 100644
--- a/src/core/course/providers/helper.ts
+++ b/src/core/course/providers/helper.ts
@@ -15,8 +15,11 @@
import { Injectable } from '@angular/core';
import { NavController } 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 { CoreFileHelperProvider } from '@providers/file-helper';
import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@@ -116,7 +119,8 @@ export class CoreCourseHelperProvider {
private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
private utils: CoreUtilsProvider, private translate: TranslateService, private loginHelper: CoreLoginHelperProvider,
private courseOptionsDelegate: CoreCourseOptionsDelegate, private siteHomeProvider: CoreSiteHomeProvider,
- private eventsProvider: CoreEventsProvider) { }
+ private eventsProvider: CoreEventsProvider, private fileHelper: CoreFileHelperProvider,
+ private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider) { }
/**
* This function treats every module on the sections provided to load the handler data, treat completion
@@ -470,6 +474,227 @@ export class CoreCourseHelperProvider {
});
}
+ /**
+ * Convenience function to open a module main file, downloading the package if needed.
+ * This is meant for modules like mod_resource.
+ *
+ * @param {any} module The module to download.
+ * @param {number} courseId The course ID of the module.
+ * @param {string} [component] The component to link the files to.
+ * @param {string|number} [componentId] An ID to use in conjunction with the component.
+ * @param {any[]} [files] List of files of the module. If not provided, use module.contents.
+ * @param {string} [siteId] The site ID. If not defined, current site.
+ * @return {Promise} Resolved on success.
+ */
+ downloadModuleAndOpenFile(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[],
+ siteId?: string): Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ let promise;
+ if (files) {
+ promise = Promise.resolve(files);
+ } else {
+ promise = this.courseProvider.loadModuleContents(module, courseId).then(() => {
+ files = module.contents;
+ });
+ }
+
+ // Make sure that module contents are loaded.
+ return promise.then(() => {
+ if (!files || !files.length) {
+ return Promise.reject(null);
+ }
+
+ return this.sitesProvider.getSite(siteId);
+ }).then((site) => {
+ const mainFile = files[0],
+ fileUrl = this.fileHelper.getFileUrl(mainFile);
+
+ // Check if the file should be opened in browser.
+ if (this.fileHelper.shouldOpenInBrowser(mainFile)) {
+ if (this.appProvider.isOnline()) {
+ // Open in browser.
+ let fixedUrl = site.fixPluginfileURL(fileUrl).replace('&offline=1', '');
+ // Remove forcedownload when followed by another param.
+ fixedUrl = fixedUrl.replace(/forcedownload=\d+&/, '');
+ // Remove forcedownload when not followed by any param.
+ fixedUrl = fixedUrl.replace(/[\?|\&]forcedownload=\d+/, '');
+
+ this.utils.openInBrowser(fixedUrl);
+
+ if (this.fileProvider.isAvailable()) {
+ // Download the file if needed (file outdated or not downloaded).
+ // Download will be in background, don't return the promise.
+ this.downloadModule(module, courseId, component, componentId, files, siteId);
+ }
+
+ return;
+ } else {
+ // Not online, get the offline file. It will fail if not found.
+ return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl).then((path) => {
+ return this.utils.openFile(path);
+ }).catch((error) => {
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ });
+ }
+ }
+
+ // File shouldn't be opened in browser. Download the module if it needs to be downloaded.
+ return this.downloadModuleWithMainFileIfNeeded(module, courseId, component, componentId, files, siteId)
+ .then((result) => {
+ if (result.path.indexOf('http') === 0) {
+ return this.utils.openOnlineFile(result.path).catch((error) => {
+ // Error opening the file, some apps don't allow opening online files.
+ if (!this.fileProvider.isAvailable()) {
+ return Promise.reject(error);
+ } else if (result.status === CoreConstants.DOWNLOADING) {
+ return Promise.reject(this.translate.instant('core.erroropenfiledownloading'));
+ }
+
+ let promise;
+ if (result.status === CoreConstants.NOT_DOWNLOADED) {
+ // Not downloaded, download it now and return the local file.
+ promise = this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => {
+ return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
+ });
+ } else {
+ // File is outdated or stale and can't be opened in online, return the local URL.
+ promise = this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
+ }
+
+ return promise.then((path) => {
+ return this.utils.openFile(path);
+ });
+ });
+ } else {
+ return this.utils.openFile(result.path);
+ }
+ });
+ });
+ }
+
+ /**
+ * Convenience function to download a module that has a main file and return the local file's path and other info.
+ * This is meant for modules like mod_resource.
+ *
+ * @param {any} module The module to download.
+ * @param {number} courseId The course ID of the module.
+ * @param {string} [component] The component to link the files to.
+ * @param {string|number} [componentId] An ID to use in conjunction with the component.
+ * @param {any[]} [files] List of files of the module. If not provided, use module.contents.
+ * @param {string} [siteId] The site ID. If not defined, current site.
+ * @return {Promise<{fixedUrl: string, path: string, status: string}>} Promise resolved when done.
+ */
+ protected downloadModuleWithMainFileIfNeeded(module: any, courseId: number, component?: string, componentId?: string | number,
+ files?: any[], siteId?: string): Promise<{fixedUrl: string, path: string, status: string}> {
+
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ if (!files || !files.length) {
+ // Module not valid, stop.
+ return Promise.reject(null);
+ }
+
+ const mainFile = files[0],
+ fileUrl = this.fileHelper.getFileUrl(mainFile),
+ timemodified = this.fileHelper.getFileTimemodified(mainFile),
+ prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module),
+ result = {
+ fixedUrl: undefined,
+ path: undefined,
+ status: undefined
+ };
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const fixedUrl = site.fixPluginfileURL(fileUrl);
+ result.fixedUrl = fixedUrl;
+
+ if (this.fileProvider.isAvailable()) {
+ // The file system is available.
+ return this.filepoolProvider.getPackageStatus(siteId, component, componentId).then((status) => {
+ result.status = status;
+
+ const isWifi = !this.appProvider.isNetworkAccessLimited(),
+ isOnline = this.appProvider.isOnline();
+
+ if (status === CoreConstants.DOWNLOADED) {
+ // Get the local file URL.
+ return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
+ } else if (status === CoreConstants.DOWNLOADING && !this.appProvider.isDesktop()) {
+ // Return the online URL.
+ return fixedUrl;
+ } else {
+ if (!isOnline && status === CoreConstants.NOT_DOWNLOADED) {
+ // Not downloaded and we're offline, reject.
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ }
+
+ return this.filepoolProvider.shouldDownloadBeforeOpen(fixedUrl, mainFile.filesize).then(() => {
+ // Download and then return the local URL.
+ return this.downloadModule(module, courseId, component, componentId, files, siteId).then(() => {
+ return this.filepoolProvider.getInternalUrlByUrl(siteId, fileUrl);
+ });
+ }, () => {
+ // Start the download if in wifi, but return the URL right away so the file is opened.
+ if (isWifi && isOnline) {
+ this.downloadModule(module, courseId, component, componentId, files, siteId);
+ }
+
+ if (!this.fileHelper.isStateDownloaded(status) || isOnline) {
+ // Not downloaded or online, return the online URL.
+ return fixedUrl;
+ } else {
+ // Outdated but offline, so we return the local URL. Use getUrlByUrl so it's added to the queue.
+ return this.filepoolProvider.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified,
+ false, false, mainFile);
+ }
+ });
+ }
+ }).then((path) => {
+ result.path = path;
+
+ return result;
+ });
+ } else {
+ // We use the live URL.
+ result.path = fixedUrl;
+
+ return result;
+ }
+ });
+ }
+
+ /**
+ * Convenience function to download a module.
+ *
+ * @param {any} module The module to download.
+ * @param {number} courseId The course ID of the module.
+ * @param {string} [component] The component to link the files to.
+ * @param {string|number} [componentId] An ID to use in conjunction with the component.
+ * @param {any[]} [files] List of files of the module. If not provided, use module.contents.
+ * @param {string} [siteId] The site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ downloadModule(module: any, courseId: number, component?: string, componentId?: string | number, files?: any[], siteId?: string)
+ : Promise {
+
+ const prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module);
+
+ if (prefetchHandler) {
+ // Use the prefetch handler to download the module.
+ if (prefetchHandler.download) {
+ return prefetchHandler.download(module, courseId);
+ } else {
+ return prefetchHandler.prefetch(module, courseId, true);
+ }
+ }
+
+ // There's no prefetch handler for the module, just download the files.
+ files = files || module.contents;
+
+ return this.filepoolProvider.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId);
+ }
+
/**
* Fill the Context Menu for a certain module.
*
diff --git a/src/core/course/providers/module-prefetch-delegate.ts b/src/core/course/providers/module-prefetch-delegate.ts
index dce46a02a..84f4b0b15 100644
--- a/src/core/course/providers/module-prefetch-delegate.ts
+++ b/src/core/course/providers/module-prefetch-delegate.ts
@@ -86,9 +86,20 @@ export interface CoreCourseModulePrefetchHandler extends CoreDelegateHandler {
* @param {any} module Module.
* @param {number} courseId Course ID the module belongs to.
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
+ * @param {string} [dirPath] Path of the directory where to store all the content files.
* @return {Promise} Promise resolved when done.
*/
- prefetch(module: any, courseId?: number, single?: boolean): Promise;
+ prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise;
+
+ /**
+ * Download the module.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @param {string} [dirPath] Path of the directory where to store all the content files.
+ * @return {Promise} Promise resolved when all content is downloaded.
+ */
+ download?(module: any, courseId: number, dirPath?: string): Promise;
/**
* Check if a certain module can use core_course_check_updates to check if it has updates.
diff --git a/src/core/siteaddons/classes/module-prefetch-handler.ts b/src/core/siteaddons/classes/module-prefetch-handler.ts
index 3972f0d56..f87750000 100644
--- a/src/core/siteaddons/classes/module-prefetch-handler.ts
+++ b/src/core/siteaddons/classes/module-prefetch-handler.ts
@@ -109,7 +109,17 @@ export class CoreSiteAddonsModulePrefetchHandler extends CoreCourseModulePrefetc
promises.push(this.siteAddonsProvider.callWS(method, params, {cacheKey: cacheKey}));
} else {
// It's a method to get content.
- promises.push(this.siteAddonsProvider.getContent(this.component, method, args));
+ promises.push(this.siteAddonsProvider.getContent(this.component, method, args).then((result) => {
+ const subPromises = [];
+
+ // Prefetch the files in the content.
+ if (result.files && result.files.length) {
+ subPromises.push(this.filepoolProvider.downloadOrPrefetchFiles(siteId, result.files, prefetch, false,
+ this.component, module.id, dirPath));
+ }
+
+ return Promise.all(subPromises);
+ }));
}
}
diff --git a/src/core/siteaddons/providers/siteaddons.ts b/src/core/siteaddons/providers/siteaddons.ts
index fdd3dff6d..37be3d7fe 100644
--- a/src/core/siteaddons/providers/siteaddons.ts
+++ b/src/core/siteaddons/providers/siteaddons.ts
@@ -43,6 +43,32 @@ export interface CoreSiteAddonsModuleHandler {
handlerSchema: any;
}
+export interface CoreSiteAddonsGetContentResult {
+ /**
+ * The content in HTML.
+ * @type {string}
+ */
+ html: string;
+
+ /**
+ * The javascript for the content.
+ * @type {string}
+ */
+ javascript: string;
+
+ /**
+ * The files for the content.
+ * @type {any[]}
+ */
+ files?: any[];
+
+ /**
+ * Other data.
+ * @type {any}
+ */
+ otherdata?: any;
+}
+
/**
* Service to provide functionalities regarding site addons.
*/
@@ -104,9 +130,9 @@ export class CoreSiteAddonsProvider {
* @param {string} method Method to execute in the class.
* @param {any} args The params for the method.
* @param {string} [siteId] Site ID. If not defined, current site.
- * @return {Promise<{html: string, javascript: string}>} Promise resolved with the content and the javascript.
+ * @return {Promise} Promise resolved with the result.
*/
- getContent(component: string, method: string, args: any, siteId?: string): Promise<{html: string, javascript: string}> {
+ getContent(component: string, method: string, args: any, siteId?: string): Promise {
this.logger.debug(`Get content for component '${component}' and method '${method}'`);
return this.sitesProvider.getSite(siteId).then((site) => {
@@ -129,6 +155,16 @@ export class CoreSiteAddonsProvider {
};
return this.sitesProvider.getCurrentSite().read('tool_mobile_get_content', data, preSets);
+ }).then((result) => {
+ if (result.otherdata) {
+ try {
+ result.otherdata = JSON.parse(result.otherdata);
+ } catch (ex) {
+ // Ignore errors.
+ }
+ }
+
+ return result;
});
});
}
diff --git a/src/providers/file-helper.ts b/src/providers/file-helper.ts
index a7a936747..88feef0dc 100644
--- a/src/providers/file-helper.ts
+++ b/src/providers/file-helper.ts
@@ -237,4 +237,31 @@ export class CoreFileHelperProvider {
isStateDownloaded(state: string): boolean {
return state === CoreConstants.DOWNLOADED || state === CoreConstants.OUTDATED;
}
+
+ /**
+ * Whether the file has to be opened in browser (external repository).
+ * The file must have a mimetype attribute.
+ *
+ * @param {any} file The file to check.
+ * @return {boolean} Whether the file should be opened in browser.
+ */
+ shouldOpenInBrowser(file: any): boolean {
+ if (!file || !file.isexternalfile || !file.mimetype) {
+ return false;
+ }
+
+ const mimetype = file.mimetype;
+ if (mimetype.indexOf('application/vnd.google-apps.') != -1) {
+ // Google Docs file, always open in browser.
+ return true;
+ }
+
+ if (file.repositorytype == 'onedrive') {
+ // In OneDrive, open in browser the office docs
+ return mimetype.indexOf('application/vnd.openxmlformats-officedocument') != -1 ||
+ mimetype == 'text/plain' || mimetype == 'document/unknown';
+ }
+
+ return false;
+ }
}
diff --git a/src/providers/filepool.ts b/src/providers/filepool.ts
index 259ba617f..c6fa08632 100644
--- a/src/providers/filepool.ts
+++ b/src/providers/filepool.ts
@@ -2718,7 +2718,7 @@ export class CoreFilepoolProvider {
*/
storePackageStatus(siteId: string, status: string, component: string, componentId?: string | number, extra?: string)
: Promise {
- this.logger.debug(`Set status '${status}'' for package ${component} ${componentId}`);
+ this.logger.debug(`Set status '${status}' for package ${component} ${componentId}`);
componentId = this.fixComponentId(componentId);
return this.sitesProvider.getSite(siteId).then((site) => {