diff --git a/src/addon/mod/imscp/components/components.module.ts b/src/addon/mod/imscp/components/components.module.ts
new file mode 100644
index 000000000..259c6c729
--- /dev/null
+++ b/src/addon/mod/imscp/components/components.module.ts
@@ -0,0 +1,49 @@
+// (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 { CommonModule } from '@angular/common';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreCourseComponentsModule } from '@core/course/components/components.module';
+import { AddonModImscpIndexComponent } from './index/index';
+import { AddonModImscpTocPopoverComponent } from './toc-popover/toc-popover';
+
+@NgModule({
+ declarations: [
+ AddonModImscpIndexComponent,
+ AddonModImscpTocPopoverComponent,
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CoreCourseComponentsModule
+ ],
+ providers: [
+ ],
+ exports: [
+ AddonModImscpIndexComponent,
+ AddonModImscpTocPopoverComponent
+ ],
+ entryComponents: [
+ AddonModImscpIndexComponent,
+ AddonModImscpTocPopoverComponent
+ ]
+})
+export class AddonModImscpComponentsModule {}
diff --git a/src/addon/mod/imscp/components/index/index.html b/src/addon/mod/imscp/components/index/index.html
new file mode 100644
index 000000000..3bd7cdebd
--- /dev/null
+++ b/src/addon/mod/imscp/components/index/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/mod/imscp/components/index/index.scss b/src/addon/mod/imscp/components/index/index.scss
new file mode 100644
index 000000000..4b4f8dc60
--- /dev/null
+++ b/src/addon/mod/imscp/components/index/index.scss
@@ -0,0 +1,12 @@
+addon-mod-imscp-index {
+ .addon-mod-imscp-container {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+ core-iframe {
+ flex-basis: 100%;
+ }
+}
diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts
new file mode 100644
index 000000000..cf57417d1
--- /dev/null
+++ b/src/addon/mod/imscp/components/index/index.ts
@@ -0,0 +1,163 @@
+// (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 { Component, Injector } from '@angular/core';
+import { PopoverController } from 'ionic-angular';
+import { CoreAppProvider } from '@providers/app';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
+import { AddonModImscpProvider } from '../../providers/imscp';
+import { AddonModImscpPrefetchHandler } from '../../providers/prefetch-handler';
+import { AddonModImscpTocPopoverComponent } from '../../components/toc-popover/toc-popover';
+
+/**
+ * Component that displays a IMSCP.
+ */
+@Component({
+ selector: 'addon-mod-imscp-index',
+ templateUrl: 'index.html',
+})
+export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent {
+ component = AddonModImscpProvider.COMPONENT;
+
+ items = [];
+ currentItem: string;
+ src = '';
+
+ // Initialize empty previous/next to prevent showing arrows for an instant before they're hidden.
+ previousItem = '';
+ nextItem = '';
+
+ constructor(injector: Injector, private imscpProvider: AddonModImscpProvider, private courseProvider: CoreCourseProvider,
+ private appProvider: CoreAppProvider, private popoverCtrl: PopoverController,
+ private imscpPrefetch: AddonModImscpPrefetchHandler) {
+ super(injector);
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ super.ngOnInit();
+
+ this.loadContent().then(() => {
+ this.imscpProvider.logView(this.module.instance).then(() => {
+ this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
+ });
+ });
+ }
+
+ /**
+ * Perform the invalidate content function.
+ *
+ * @return {Promise} Resolved when done.
+ */
+ protected invalidateContent(): Promise {
+ return this.imscpProvider.invalidateContent(this.module.id, this.courseId);
+ }
+
+ /**
+ * Download imscp contents.
+ *
+ * @param {boolean} [refresh] Whether we're refreshing data.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchContent(refresh?: boolean): Promise {
+ let downloadFailed = false;
+ const promises = [];
+
+ promises.push(this.imscpProvider.getImscp(this.courseId, this.module.id).then((imscp) => {
+ this.description = imscp.intro || imscp.description;
+ this.dataRetrieved.emit(imscp);
+ }));
+
+ promises.push(this.imscpPrefetch.download(this.module, this.courseId).catch(() => {
+ // Mark download as failed but go on since the main files could have been downloaded.
+ downloadFailed = true;
+
+ return this.courseProvider.loadModuleContents(this.module, this.courseId).catch((error) => {
+ // Error getting module contents, fail.
+ this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
+
+ return Promise.reject(null);
+ });
+ }));
+
+ return Promise.all(promises).then(() => {
+ this.items = this.imscpProvider.createItemList(this.module.contents);
+ if (this.items.length && typeof this.currentItem == 'undefined') {
+ this.currentItem = this.items[0].href;
+ }
+
+ return this.loadItem(this.currentItem).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true);
+
+ return Promise.reject(null);
+ });
+ }).then(() => {
+ if (downloadFailed && this.appProvider.isOnline()) {
+ // We could load the main file but the download failed. Show error message.
+ this.domUtils.showErrorModal('core.errordownloadingsomefiles', true);
+ }
+
+ // All data obtained, now fill the context menu.
+ this.fillContextMenu(refresh);
+ });
+ }
+
+ /**
+ * Loads an item.
+ *
+ * @param {string} itemId Item ID.
+ * @return {Promise} Promise resolved when done.
+ */
+ loadItem(itemId: string): Promise {
+ return this.imscpProvider.getIframeSrc(this.module, itemId).then((src) => {
+ this.currentItem = itemId;
+ this.previousItem = this.imscpProvider.getPreviousItem(this.items, itemId);
+ this.nextItem = this.imscpProvider.getNextItem(this.items, itemId);
+
+ if (this.src && src == this.src) {
+ // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed.
+ this.src = '';
+ setTimeout(() => {
+ this.src = src;
+ });
+ } else {
+ this.src = src;
+ }
+ });
+ }
+
+ /**
+ * Show the TOC.
+ *
+ * @param {MouseEvent} event Event.
+ */
+ showToc(event: MouseEvent): void {
+ const popover = this.popoverCtrl.create(AddonModImscpTocPopoverComponent, { items: this.items });
+
+ popover.onDidDismiss((itemId) => {
+ if (!itemId) {
+ // Not valid, probably a category.
+ return;
+ }
+ this.loadItem(itemId);
+ });
+
+ popover.present({
+ ev: event
+ });
+ }
+}
diff --git a/src/addon/mod/imscp/components/toc-popover/toc-popover.html b/src/addon/mod/imscp/components/toc-popover/toc-popover.html
new file mode 100644
index 000000000..6bda8d594
--- /dev/null
+++ b/src/addon/mod/imscp/components/toc-popover/toc-popover.html
@@ -0,0 +1,5 @@
+
+
+ {{item.title}}
+
+
diff --git a/src/addon/mod/imscp/components/toc-popover/toc-popover.ts b/src/addon/mod/imscp/components/toc-popover/toc-popover.ts
new file mode 100644
index 000000000..ab88dec0e
--- /dev/null
+++ b/src/addon/mod/imscp/components/toc-popover/toc-popover.ts
@@ -0,0 +1,50 @@
+// (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 { Component } from '@angular/core';
+import { NavParams, ViewController } from 'ionic-angular';
+
+/**
+ * Component to display the TOC of a IMSCP.
+ */
+@Component({
+ selector: 'addon-mod-imscp-toc-popover',
+ templateUrl: 'toc-popover.html'
+})
+export class AddonModImscpTocPopoverComponent {
+ items = [];
+
+ constructor(navParams: NavParams, private viewCtrl: ViewController) {
+ this.items = navParams.get('items') || [];
+ }
+
+ /**
+ * Function called when an item is clicked.
+ *
+ * @param {string} id ID of the clicked item.
+ */
+ loadItem(id: string): void {
+ this.viewCtrl.dismiss(id);
+ }
+
+ /**
+ * Get dummy array for padding.
+ *
+ * @param {number} n Array length.
+ * @return {number[]} Dummy array with n elements.
+ */
+ getNumberForPadding(n: number): number[] {
+ return new Array(n);
+ }
+}
diff --git a/src/addon/mod/imscp/imscp.module.ts b/src/addon/mod/imscp/imscp.module.ts
new file mode 100644
index 000000000..577312484
--- /dev/null
+++ b/src/addon/mod/imscp/imscp.module.ts
@@ -0,0 +1,51 @@
+// (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 { AddonModImscpComponentsModule } from './components/components.module';
+import { AddonModImscpModuleHandler } from './providers/module-handler';
+import { AddonModImscpProvider } from './providers/imscp';
+import { AddonModImscpPrefetchHandler } from './providers/prefetch-handler';
+import { AddonModImscpLinkHandler } from './providers/link-handler';
+import { AddonModImscpPluginFileHandler } from './providers/pluginfile-handler';
+import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
+import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
+import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
+import { CorePluginFileDelegate } from '@providers/plugin-file-delegate';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ AddonModImscpComponentsModule
+ ],
+ providers: [
+ AddonModImscpProvider,
+ AddonModImscpModuleHandler,
+ AddonModImscpPrefetchHandler,
+ AddonModImscpLinkHandler,
+ AddonModImscpPluginFileHandler
+ ]
+})
+export class AddonModImscpModule {
+ constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModImscpModuleHandler,
+ prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModImscpPrefetchHandler,
+ contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModImscpLinkHandler,
+ pluginfileDelegate: CorePluginFileDelegate, pluginfileHandler: AddonModImscpPluginFileHandler) {
+ moduleDelegate.registerHandler(moduleHandler);
+ prefetchDelegate.registerHandler(prefetchHandler);
+ contentLinksDelegate.registerHandler(linkHandler);
+ pluginfileDelegate.registerHandler(pluginfileHandler);
+ }
+}
diff --git a/src/addon/mod/imscp/lang/en.json b/src/addon/mod/imscp/lang/en.json
new file mode 100644
index 000000000..f2c9c32bd
--- /dev/null
+++ b/src/addon/mod/imscp/lang/en.json
@@ -0,0 +1,4 @@
+{
+ "deploymenterror": "Content package error!",
+ "showmoduledescription": "Show description"
+}
\ No newline at end of file
diff --git a/src/addon/mod/imscp/pages/index/index.html b/src/addon/mod/imscp/pages/index/index.html
new file mode 100644
index 000000000..d45932fcf
--- /dev/null
+++ b/src/addon/mod/imscp/pages/index/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/mod/imscp/pages/index/index.module.ts b/src/addon/mod/imscp/pages/index/index.module.ts
new file mode 100644
index 000000000..bc7feec3c
--- /dev/null
+++ b/src/addon/mod/imscp/pages/index/index.module.ts
@@ -0,0 +1,33 @@
+// (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 { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonModImscpComponentsModule } from '../../components/components.module';
+import { AddonModImscpIndexPage } from './index';
+
+@NgModule({
+ declarations: [
+ AddonModImscpIndexPage,
+ ],
+ imports: [
+ CoreDirectivesModule,
+ AddonModImscpComponentsModule,
+ IonicPageModule.forChild(AddonModImscpIndexPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonModImscpIndexPageModule {}
diff --git a/src/addon/mod/imscp/pages/index/index.ts b/src/addon/mod/imscp/pages/index/index.ts
new file mode 100644
index 000000000..b94d81b8b
--- /dev/null
+++ b/src/addon/mod/imscp/pages/index/index.ts
@@ -0,0 +1,48 @@
+// (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 { Component, ViewChild } from '@angular/core';
+import { IonicPage, NavParams } from 'ionic-angular';
+import { AddonModImscpIndexComponent } from '../../components/index/index';
+
+/**
+ * Imscp that displays a IMSCP.
+ */
+@IonicPage({ segment: 'addon-mod-imscp-index' })
+@Component({
+ selector: 'page-addon-mod-imscp-index',
+ templateUrl: 'index.html',
+})
+export class AddonModImscpIndexPage {
+ @ViewChild(AddonModImscpIndexComponent) imscpComponent: AddonModImscpIndexComponent;
+
+ title: string;
+ module: any;
+ courseId: number;
+
+ constructor(navParams: NavParams) {
+ this.module = navParams.get('module') || {};
+ this.courseId = navParams.get('courseId');
+ this.title = this.module.name;
+ }
+
+ /**
+ * Update some data based on the imscp instance.
+ *
+ * @param {any} imscp Imscp instance.
+ */
+ updateData(imscp: any): void {
+ this.title = imscp.name || this.title;
+ }
+}
diff --git a/src/addon/mod/imscp/providers/imscp.ts b/src/addon/mod/imscp/providers/imscp.ts
new file mode 100644
index 000000000..cb66631a7
--- /dev/null
+++ b/src/addon/mod/imscp/providers/imscp.ts
@@ -0,0 +1,319 @@
+// (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 { Injectable } from '@angular/core';
+import { CoreAppProvider } from '@providers/app';
+import { CoreFilepoolProvider } from '@providers/filepool';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { CoreCourseProvider } from '@core/course/providers/course';
+
+/**
+ * Service that provides some features for IMSCP.
+ */
+@Injectable()
+export class AddonModImscpProvider {
+ static COMPONENT = 'mmaModImscp';
+
+ protected ROOT_CACHE_KEY = 'mmaModImscp:';
+
+ constructor(private appProvider: CoreAppProvider, private courseProvider: CoreCourseProvider,
+ private filepoolProvider: CoreFilepoolProvider, private sitesProvider: CoreSitesProvider,
+ private textUtils: CoreTextUtilsProvider, private utils: CoreUtilsProvider) {}
+
+ /**
+ * Get the IMSCP toc as an array.
+ *
+ * @param {any[]} contents The module contents.
+ * @return {any} The toc.
+ */
+ protected getToc(contents: any[]): any {
+ if (!contents || !contents.length) {
+ return [];
+ }
+
+ return JSON.parse(contents[0].content);
+ }
+
+ /**
+ * Get the imscp toc as an array of items (not nested) to build the navigation tree.
+ *
+ * @param {any[]} contents The module contents.
+ * @return {any[]} The toc as a list.
+ */
+ createItemList(contents: any[]): any[] {
+ const items = [];
+
+ this.getToc(contents).forEach((el) => {
+ items.push({href: el.href, title: el.title, level: el.level});
+ el.subitems.forEach((sel) => {
+ items.push({href: sel.href, title: sel.title, level: sel.level});
+ });
+ });
+
+ return items;
+ }
+
+ /**
+ * Get the previous item to the given one.
+ *
+ * @param {any[]} items The items list.
+ * @param {string} itemId The current item.
+ * @return {string} The previous item id.
+ */
+ getPreviousItem(items: any[], itemId: string): string {
+ const position = this.getItemPosition(items, itemId);
+
+ if (position != -1) {
+ for (let i = position - 1; i >= 0; i--) {
+ if (items[i] && items[i].href) {
+ return items[i].href;
+ }
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Get the next item to the given one.
+ *
+ * @param {any[]} items The items list.
+ * @param {string} itemId The current item.
+ * @return {string} The next item id.
+ */
+ getNextItem(items: any[], itemId: string): string {
+ const position = this.getItemPosition(items, itemId);
+
+ if (position != -1) {
+ for (let i = position + 1; i < items.length; i++) {
+ if (items[i] && items[i].href) {
+ return items[i].href;
+ }
+ }
+ }
+
+ return '';
+ }
+
+ /**
+ * Get the position of a item.
+ *
+ * @param {any[]} items The items list.
+ * @param {string} itemId The item to search.
+ * @return {number} The item position.
+ */
+ protected getItemPosition(items: any[], itemId: string): number {
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].href == itemId) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Check if we should ommit the file download.
+ *
+ * @param {string} fileName The file name
+ * @return {boolean} True if we should ommit the file.
+ */
+ protected checkSpecialFiles(fileName: string): boolean {
+ return fileName == 'imsmanifest.xml';
+ }
+
+ /**
+ * Get cache key for imscp data WS calls.
+ *
+ * @param {number} courseId Course ID.
+ * @return {string} Cache key.
+ */
+ protected getImscpDataCacheKey(courseId: number): string {
+ return this.ROOT_CACHE_KEY + 'imscp:' + courseId;
+ }
+
+ /**
+ * Get a imscp with key=value. If more than one is found, only the first will be returned.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} key Name of the property to check.
+ * @param {any} value Value to search.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the imscp is retrieved.
+ */
+ protected getImscpByKey(courseId: number, key: string, value: any, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const params = {
+ courseids: [courseId]
+ };
+ const preSets = {
+ cacheKey: this.getImscpDataCacheKey(courseId)
+ };
+
+ return site.read('mod_imscp_get_imscps_by_courses', params, preSets).then((response) => {
+ if (response && response.imscps) {
+ const currentImscp = response.imscps.find((imscp) => imscp[key] == value);
+ if (currentImscp) {
+ return currentImscp;
+ }
+ }
+
+ return Promise.reject(null);
+ });
+ });
+ }
+
+ /**
+ * Get a imscp by course module ID.
+ *
+ * @param {number} courseId Course ID.
+ * @param {number} cmId Course module ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the imscp is retrieved.
+ */
+ getImscp(courseId: number, cmId: number, siteId?: string): Promise {
+ return this.getImscpByKey(courseId, 'coursemodule', cmId, siteId);
+ }
+
+ /**
+ * Given a filepath, get a certain fileurl from module contents.
+ *
+ * @param {any[]} contents Module contents.
+ * @param {string} targetFilePath Path of the searched file.
+ * @return {string} File URL.
+ */
+ protected getFileUrlFromContents(contents: any[], targetFilePath: string): string {
+ let indexUrl;
+ contents.forEach((content) => {
+ if (content.type == 'file' && !indexUrl) {
+ const filePath = this.textUtils.concatenatePaths(content.filepath, content.filename);
+ const filePathAlt = filePath.charAt(0) === '/' ? filePath.substr(1) : '/' + filePath;
+ // Check if it's main file.
+ if (filePath === targetFilePath || filePathAlt === targetFilePath) {
+ indexUrl = content.fileurl;
+ }
+ }
+ });
+
+ return indexUrl;
+ }
+
+ /**
+ * Get src of a imscp item.
+ *
+ * @param {any} module The module object.
+ * @param {string} [itemHref] Href of item to get. If not defined, gets src of main item.
+ * @return {Promise} Promise resolved with the item src.
+ */
+ getIframeSrc(module: any, itemHref?: string): Promise {
+ if (!itemHref) {
+ const toc = this.getToc(module.contents);
+ if (!toc.length) {
+ return Promise.reject(null);
+ }
+ itemHref = toc[0].href;
+ }
+
+ const siteId = this.sitesProvider.getCurrentSiteId();
+
+ return this.filepoolProvider.getPackageDirUrlByUrl(siteId, module.url).then((dirPath) => {
+ return this.textUtils.concatenatePaths(dirPath, itemHref);
+ }).catch(() => {
+ // Error getting directory, there was an error downloading or we're in browser. Return online URL if connected.
+ if (this.appProvider.isOnline()) {
+ const indexUrl = this.getFileUrlFromContents(module.contents, itemHref);
+
+ if (indexUrl) {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.fixPluginfileURL(indexUrl);
+ });
+ }
+ }
+
+ return Promise.reject(null);
+ });
+ }
+
+ /**
+ * Invalidate the prefetched content.
+ *
+ * @param {number} moduleId The module ID.
+ * @param {number} courseId Course ID of the module.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the content is invalidated.
+ */
+ invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ const promises = [];
+
+ promises.push(this.invalidateImscpData(courseId, siteId));
+ promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModImscpProvider.COMPONENT, moduleId));
+ promises.push(this.courseProvider.invalidateModule(moduleId, siteId));
+
+ return this.utils.allPromises(promises);
+ }
+
+ /**
+ * Invalidates imscp data.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateImscpData(courseId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getImscpDataCacheKey(courseId));
+ });
+ }
+
+ /**
+ * Check if a file is downloadable. The file param must have 'type' and 'filename' attributes
+ * like in core_course_get_contents response.
+ *
+ * @param {any} file File to check.
+ * @return {boolean} True if downloadable, false otherwise.
+ */
+ isFileDownloadable(file: any): boolean {
+ return file.type === 'file' && !this.checkSpecialFiles(file.filename);
+ }
+
+ /**
+ * Return whether or not the plugin is enabled in a certain site.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
+ */
+ isPluginEnabled(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.canDownloadFiles();
+ });
+ }
+
+ /**
+ * Report a IMSCP as being viewed.
+ *
+ * @param {string} id Module ID.
+ * @return {Promise} Promise resolved when the WS call is successful.
+ */
+ logView(id: string): Promise {
+ const params = {
+ imscpid: id
+ };
+
+ return this.sitesProvider.getCurrentSite().write('mod_imscp_view_imscp', params);
+ }
+}
diff --git a/src/addon/mod/imscp/providers/link-handler.ts b/src/addon/mod/imscp/providers/link-handler.ts
new file mode 100644
index 000000000..7e7be9821
--- /dev/null
+++ b/src/addon/mod/imscp/providers/link-handler.ts
@@ -0,0 +1,29 @@
+// (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 { Injectable } from '@angular/core';
+import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
+import { CoreCourseHelperProvider } from '@core/course/providers/helper';
+
+/**
+ * Handler to treat links to IMSCP.
+ */
+@Injectable()
+export class AddonModImscpLinkHandler extends CoreContentLinksModuleIndexHandler {
+ name = 'AddonModImscpLinkHandler';
+
+ constructor(courseHelper: CoreCourseHelperProvider) {
+ super(courseHelper, 'AddonModImscp', 'imscp');
+ }
+}
diff --git a/src/addon/mod/imscp/providers/module-handler.ts b/src/addon/mod/imscp/providers/module-handler.ts
new file mode 100644
index 000000000..70cc43b6b
--- /dev/null
+++ b/src/addon/mod/imscp/providers/module-handler.ts
@@ -0,0 +1,72 @@
+// (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 { Injectable } from '@angular/core';
+import { NavController, NavOptions } from 'ionic-angular';
+import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { AddonModImscpIndexComponent } from '../components/index/index';
+import { AddonModImscpProvider } from './imscp';
+
+/**
+ * Handler to support IMSCP modules.
+ */
+@Injectable()
+export class AddonModImscpModuleHandler implements CoreCourseModuleHandler {
+ name = 'AddonModImscp';
+ modName = 'imscp';
+
+ constructor(private courseProvider: CoreCourseProvider, protected imscpProvider: AddonModImscpProvider) { }
+
+ /**
+ * Check if the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.imscpProvider.isPluginEnabled();
+ }
+
+ /**
+ * Get the data required to display the module in the course contents view.
+ *
+ * @param {any} module The module object.
+ * @param {number} courseId The course ID.
+ * @param {number} sectionId The section ID.
+ * @return {CoreCourseModuleHandlerData} Data to render the module.
+ */
+ getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
+ return {
+ icon: this.courseProvider.getModuleIconSrc('imscp'),
+ title: module.name,
+ class: 'addon-mod_imscp-handler',
+ showDownloadButton: true,
+ action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
+ navCtrl.push('AddonModImscpIndexPage', {module: module, courseId: courseId}, options);
+ }
+ };
+ }
+
+ /**
+ * 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.
+ * @return {any} The component to use, undefined if not found.
+ */
+ getMainComponent(course: any, module: any): any {
+ return AddonModImscpIndexComponent;
+ }
+}
diff --git a/src/addon/mod/imscp/providers/pluginfile-handler.ts b/src/addon/mod/imscp/providers/pluginfile-handler.ts
new file mode 100644
index 000000000..e64e9b44f
--- /dev/null
+++ b/src/addon/mod/imscp/providers/pluginfile-handler.ts
@@ -0,0 +1,56 @@
+// (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 { Injectable } from '@angular/core';
+import { CorePluginFileHandler } from '@providers/plugin-file-delegate';
+
+/**
+ * Handler to treat links to IMSCP.
+ */
+@Injectable()
+export class AddonModImscpPluginFileHandler implements CorePluginFileHandler {
+ name = 'AddonModImscpPluginFileHandler';
+
+ /**
+ * Return the RegExp to match the revision on pluginfile URLs.
+ *
+ * @param {string[]} args Arguments of the pluginfile URL defining component and filearea at least.
+ * @return {RegExp} RegExp to match the revision on pluginfile URLs.
+ */
+ getComponentRevisionRegExp(args: string[]): RegExp {
+ // Check filearea.
+ if (args[2] == 'content') {
+ // Component + Filearea + Revision
+ return new RegExp('/mod_imscp/content/([0-9]+)/');
+ }
+
+ if (args[2] == 'backup') {
+ // Component + Filearea + Revision
+ return new RegExp('/mod_imscp/backup/([0-9]+)/');
+ }
+
+ return null;
+ }
+
+ /**
+ * Should return the string to remove the revision on pluginfile url.
+ *
+ * @param {string[]} args Arguments of the pluginfile URL defining component and filearea at least.
+ * @return {string} String to remove the revision on pluginfile url.
+ */
+ getComponentRevisionReplace(args: string[]): string {
+ // Component + Filearea + Revision
+ return '/mod_imscp/' + args[2] + '/0/';
+ }
+}
diff --git a/src/addon/mod/imscp/providers/prefetch-handler.ts b/src/addon/mod/imscp/providers/prefetch-handler.ts
new file mode 100644
index 000000000..b82877731
--- /dev/null
+++ b/src/addon/mod/imscp/providers/prefetch-handler.ts
@@ -0,0 +1,131 @@
+// (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 { Injectable, Injector } from '@angular/core';
+import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
+import { CoreFilepoolProvider } from '@providers/filepool';
+import { AddonModImscpProvider } from './imscp';
+
+/**
+ * Handler to prefetch IMSCPs.
+ */
+@Injectable()
+export class AddonModImscpPrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
+ name = 'AddonModImscp';
+ modName = 'imscp';
+ component = AddonModImscpProvider.COMPONENT;
+ isResource = true;
+
+ constructor(injector: Injector, protected imscpProvider: AddonModImscpProvider,
+ protected filepoolProvider: CoreFilepoolProvider) {
+ super(injector);
+ }
+
+ /**
+ * 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. @see downloadOrPrefetch.
+ * @return {Promise} Promise resolved when all content is downloaded.
+ */
+ download(module: any, courseId: number, dirPath?: string): Promise {
+ return this.prefetch(module, courseId, false, dirPath);
+ }
+
+ /**
+ * Download or prefetch the content.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @param {boolean} [prefetch] True to prefetch, false to download right away.
+ * @param {string} [dirPath] Path of the directory where to store all the content files. This is to keep the files
+ * relative paths and make the package work in an iframe. Undefined to download the files
+ * in the filepool root folder.
+ * @return {Promise} Promise resolved when all content is downloaded. Data returned is not reliable.
+ */
+ downloadOrPrefetch(module: any, courseId: number, prefetch?: boolean, dirPath?: string): Promise {
+ const siteId = this.sitesProvider.getCurrentSiteId();
+
+ return this.filepoolProvider.getPackageDirPathByUrl(siteId, module.url).then((dirPath) => {
+ const promises = [];
+
+ promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath));
+ promises.push(this.imscpProvider.getImscp(courseId, module.id, siteId));
+
+ return Promise.all(promises);
+ });
+ }
+
+ /**
+ * Returns module intro files.
+ *
+ * @param {any} module The module object returned by WS.
+ * @param {number} courseId Course ID.
+ * @return {Promise} Promise resolved with list of intro files.
+ */
+ getIntroFiles(module: any, courseId: number): Promise {
+ return this.imscpProvider.getImscp(courseId, module.id).catch(() => {
+ // Not found, return undefined so module description is used.
+ }).then((imscp) => {
+ return this.getIntroFilesFromInstance(module, imscp);
+ });
+ }
+
+ /**
+ * Invalidate the prefetched content.
+ *
+ * @param {number} moduleId The module ID.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateContent(moduleId: number, courseId: number): Promise {
+ return this.imscpProvider.invalidateContent(moduleId, courseId);
+ }
+
+ /**
+ * Invalidate WS calls needed to determine module status.
+ *
+ * @param {any} module Module.
+ * @param {number} courseId Course ID the module belongs to.
+ * @return {Promise} Promise resolved when invalidated.
+ */
+ invalidateModule(module: any, courseId: number): Promise {
+ const promises = [];
+
+ promises.push(this.imscpProvider.invalidateImscpData(courseId));
+ promises.push(this.courseProvider.invalidateModule(module.id));
+
+ return Promise.all(promises);
+ }
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ *
+ * @return {boolean|Promise} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
+ */
+ isEnabled(): boolean | Promise {
+ return this.imscpProvider.isPluginEnabled();
+ }
+
+ /**
+ * Check if a file is downloadable.
+ *
+ * @param {any} file File to check.
+ * @return {boolean} Whether the file is downloadable.
+ */
+ isFileDownloadable(file: any): boolean {
+ return this.imscpProvider.isFileDownloadable(file);
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index f4212aef9..97c53e6aa 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -86,6 +86,7 @@ import { AddonModPageModule } from '@addon/mod/page/page.module';
import { AddonModQuizModule } from '@addon/mod/quiz/quiz.module';
import { AddonModUrlModule } from '@addon/mod/url/url.module';
import { AddonModSurveyModule } from '@addon/mod/survey/survey.module';
+import { AddonModImscpModule } from '@addon/mod/imscp/imscp.module';
import { AddonMessageOutputModule } from '@addon/messageoutput/messageoutput.module';
import { AddonMessageOutputAirnotifierModule } from '@addon/messageoutput/airnotifier/airnotifier.module';
import { AddonMessagesModule } from '@addon/messages/messages.module';
@@ -182,6 +183,7 @@ export const CORE_PROVIDERS: any[] = [
AddonModQuizModule,
AddonModUrlModule,
AddonModSurveyModule,
+ AddonModImscpModule,
AddonMessageOutputModule,
AddonMessageOutputAirnotifierModule,
AddonMessagesModule,
diff --git a/src/components/components.module.ts b/src/components/components.module.ts
index f1de97468..11f1b3efa 100644
--- a/src/components/components.module.ts
+++ b/src/components/components.module.ts
@@ -42,6 +42,7 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
import { CoreSendMessageFormComponent } from './send-message-form/send-message-form';
import { CoreTimerComponent } from './timer/timer';
import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha/recaptcha';
+import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar';
@NgModule({
declarations: [
@@ -70,7 +71,8 @@ import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha
CoreSendMessageFormComponent,
CoreTimerComponent,
CoreRecaptchaComponent,
- CoreRecaptchaModalComponent
+ CoreRecaptchaModalComponent,
+ CoreNavigationBarComponent
],
entryComponents: [
CoreContextMenuPopoverComponent,
@@ -106,7 +108,8 @@ import { CoreRecaptchaComponent, CoreRecaptchaModalComponent } from './recaptcha
CoreDynamicComponent,
CoreSendMessageFormComponent,
CoreTimerComponent,
- CoreRecaptchaComponent
+ CoreRecaptchaComponent,
+ CoreNavigationBarComponent
]
})
export class CoreComponentsModule {}
diff --git a/src/components/iframe/iframe.scss b/src/components/iframe/iframe.scss
index 8ebb42eb7..abc26dfee 100644
--- a/src/components/iframe/iframe.scss
+++ b/src/components/iframe/iframe.scss
@@ -4,6 +4,7 @@ core-iframe {
}
iframe {
border: 0;
+ display: block;
}
.core-loading-container {
diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts
index 67ed9ed94..1f50b217e 100644
--- a/src/components/iframe/iframe.ts
+++ b/src/components/iframe/iframe.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter } from '@angular/core';
+import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Platform } from 'ionic-angular';
import { CoreFileProvider } from '@providers/file';
@@ -29,7 +29,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
selector: 'core-iframe',
templateUrl: 'iframe.html'
})
-export class CoreIframeComponent implements OnInit {
+export class CoreIframeComponent implements OnInit, OnChanges {
@ViewChild('iframe') iframe: ElementRef;
@Input() src: string;
@@ -56,7 +56,6 @@ export class CoreIframeComponent implements OnInit {
ngOnInit(): void {
const iframe: HTMLIFrameElement = this.iframe && this.iframe.nativeElement;
- this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.src);
this.iframeWidth = this.domUtils.formatPixelsSize(this.iframeWidth) || '100%';
this.iframeHeight = this.domUtils.formatPixelsSize(this.iframeHeight) || '100%';
@@ -82,6 +81,15 @@ export class CoreIframeComponent implements OnInit {
}
}
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange }): void {
+ if (changes.src) {
+ this.safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(changes.src.currentValue);
+ }
+ }
+
/**
* Given an element, return the content window and document.
*
diff --git a/src/components/navigation-bar/navigation-bar.html b/src/components/navigation-bar/navigation-bar.html
new file mode 100644
index 000000000..07d9810ac
--- /dev/null
+++ b/src/components/navigation-bar/navigation-bar.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/navigation-bar/navigation-bar.ts b/src/components/navigation-bar/navigation-bar.ts
new file mode 100644
index 000000000..76f1bfe47
--- /dev/null
+++ b/src/components/navigation-bar/navigation-bar.ts
@@ -0,0 +1,47 @@
+// (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 { Component, EventEmitter, Input, Output } from '@angular/core';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+
+/**
+ * Component to show a "bar" with arrows to navigate forward/backward and a "info" icon to display more data.
+ *
+ * This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked.
+ * If no previous/next item is defined, that arrow won't be shown. It will also show a button to show more info.
+ *
+ * Example usage:
+ *
+ */
+@Component({
+ selector: 'core-navigation-bar',
+ templateUrl: 'navigation-bar.html',
+})
+export class CoreNavigationBarComponent {
+ @Input() previous?: any; // Previous item. If not defined, the previous arrow won't be shown.
+ @Input() next?: any; // Next item. If not defined, the next arrow won't be shown.
+ @Input() info?: string; // Info to show when clicking the info button. If not defined, the info button won't be shown.
+ @Input() title?: string; // Title to show when seeing the info (new page).
+ @Input() component?: string; // Component the bar belongs to.
+ @Input() componentId?: number; // Component ID.
+ @Output() action?: EventEmitter; // Function to call when an arrow is clicked. Will receive as a param the item to load.
+
+ constructor(private textUtils: CoreTextUtilsProvider) {
+ this.action = new EventEmitter();
+ }
+
+ showInfo(): void {
+ this.textUtils.expandText(this.title, this.info, this.component, this.componentId);
+ }
+}