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); + } +}