diff --git a/src/addon/messages/components/discussions/discussions.html b/src/addon/messages/components/discussions/discussions.html index 2e7f4f993..cade6e7c3 100644 --- a/src/addon/messages/components/discussions/discussions.html +++ b/src/addon/messages/components/discussions/discussions.html @@ -33,8 +33,8 @@

-
{{discussion.message.timecreated / 1000 | coreDateDayOrTime}}
-
+ + {{discussion.message.timecreated / 1000 | coreDateDayOrTime}}

diff --git a/src/addon/messages/providers/mainmenu-handler.ts b/src/addon/messages/providers/mainmenu-handler.ts index f49660b56..8afb58d63 100644 --- a/src/addon/messages/providers/mainmenu-handler.ts +++ b/src/addon/messages/providers/mainmenu-handler.ts @@ -32,8 +32,15 @@ import { AddonPushNotificationsDelegate } from '@addon/pushnotifications/provide export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCronHandler { name = 'AddonMessages'; priority = 800; - protected badge = ''; - protected loading = true; + protected handler: CoreMainMenuHandlerToDisplay = { + icon: 'chatbubbles', + title: 'addon.messages.messages', + page: 'AddonMessagesIndexPage', + class: 'addon-messages-handler', + showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled. + badge: '', + loading: true + }; constructor(private messagesProvider: AddonMessagesProvider, private sitesProvider: CoreSitesProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, @@ -51,8 +58,8 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr // Reset info on logout. eventsProvider.on(CoreEventsProvider.LOGOUT, (data) => { - this.badge = ''; - this.loading = true; + this.handler.badge = ''; + this.handler.loading = true; }); // If a message push notification is received, refresh the count. @@ -82,19 +89,11 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr * @return {CoreMainMenuHandlerToDisplay} Data needed to render the handler. */ getDisplayData(): CoreMainMenuHandlerToDisplay { - if (this.loading) { + if (this.handler.loading) { this.updateBadge(); } - return { - icon: 'chatbubbles', - title: 'addon.messages.messages', - page: 'AddonMessagesIndexPage', - class: 'addon-messages-handler', - showBadge: true, // Do not check isMessageCountEnabled because we'll use fallback it not enabled. - badge: this.badge, - loading: this.loading - }; + return this.handler; } /** @@ -110,17 +109,13 @@ export class AddonMessagesMainMenuHandler implements CoreMainMenuHandler, CoreCr this.messagesProvider.getUnreadConversationsCount(undefined, siteId).then((unread) => { // Leave badge enter if there is a 0+ or a 0. - this.badge = parseInt(unread, 10) > 0 ? unread : ''; + this.handler.badge = parseInt(unread, 10) > 0 ? unread : ''; // Update badge. this.pushNotificationsProvider.updateAddonCounter('AddonMessages', unread, siteId); }).catch(() => { - this.badge = ''; + this.handler.badge = ''; }).finally(() => { - this.loading = false; - this.eventsProvider.trigger(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, { - name: this.name, - badge: this.badge - }, siteId); + this.handler.loading = false; }); } diff --git a/src/addon/mod/book/components/index/index.html b/src/addon/mod/book/components/index/index.html index 5463e70a5..0f9b9a735 100644 --- a/src/addon/mod/book/components/index/index.html +++ b/src/addon/mod/book/components/index/index.html @@ -13,7 +13,7 @@ - + diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts index 808a77128..4f97a6e83 100644 --- a/src/addon/mod/book/providers/book.ts +++ b/src/addon/mod/book/providers/book.ts @@ -153,9 +153,9 @@ export class AddonModBookProvider { return promise.then((url) => { // Fetch the URL content. - const observable = this.http.get(url); + const promise = this.http.get(url).toPromise(); - return this.utils.observableToPromise(observable).then((response: Response): any => { + return promise.then((response: Response): any => { const content = response.text(); if (typeof content !== 'string') { return Promise.reject(null); @@ -381,19 +381,11 @@ export class AddonModBookProvider { * @return {Promise} Promise resolved when the WS call is successful. */ logView(id: number, chapterId: string): Promise { - if (id) { - const params = { - bookid: id, - chapterid: chapterId - }; + const params = { + bookid: id, + chapterid: chapterId + }; - return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params).then((response) => { - if (!response.status) { - return Promise.reject(null); - } - }); - } - - return Promise.reject(null); + return this.sitesProvider.getCurrentSite().write('mod_book_view_book', params); } } diff --git a/src/addon/mod/resource/components/components.module.ts b/src/addon/mod/resource/components/components.module.ts new file mode 100644 index 000000000..1f2cdda66 --- /dev/null +++ b/src/addon/mod/resource/components/components.module.ts @@ -0,0 +1,45 @@ +// (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 { AddonModResourceIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModResourceIndexComponent + ], + imports: [ + CommonModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCourseComponentsModule + ], + providers: [ + ], + exports: [ + AddonModResourceIndexComponent + ], + entryComponents: [ + AddonModResourceIndexComponent + ] +}) +export class AddonModResourceComponentsModule {} diff --git a/src/addon/mod/resource/components/index/index.html b/src/addon/mod/resource/components/index/index.html new file mode 100644 index 000000000..5b4289d28 --- /dev/null +++ b/src/addon/mod/resource/components/index/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + +
diff --git a/src/addon/mod/resource/components/index/index.ts b/src/addon/mod/resource/components/index/index.ts new file mode 100644 index 000000000..353ede9f9 --- /dev/null +++ b/src/addon/mod/resource/components/index/index.ts @@ -0,0 +1,218 @@ +// (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, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreAppProvider } from '@providers/app'; +import { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { CoreCourseModuleMainComponent } from '@core/course/providers/module-delegate'; +import { AddonModResourceProvider } from '../../providers/resource'; +import { AddonModResourcePrefetchHandler } from '../../providers/prefetch-handler'; +import { AddonModResourceHelperProvider } from '../../providers/helper'; + +/** + * Component that displays a resource. + */ +@Component({ + selector: 'addon-mod-resource-index', + templateUrl: 'index.html', +}) +export class AddonModResourceIndexComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent { + @Input() module: any; // The module of the resource. + @Input() courseId: number; // Course ID the resource belongs to. + @Output() resourceRetrieved?: EventEmitter; + + loaded: boolean; + component = AddonModResourceProvider.COMPONENT; + componentId: number; + + canGetResource: boolean; + mode: string; + src: string; + contentText: string; + + // Data for context menu. + externalUrl: string; + description: string; + refreshIcon: string; + prefetchStatusIcon: string; + prefetchText: string; + size: string; + + protected isDestroyed = false; + protected statusObserver; + + constructor(private resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, + private domUtils: CoreDomUtilsProvider, private appProvider: CoreAppProvider, private textUtils: CoreTextUtilsProvider, + private courseHelper: CoreCourseHelperProvider, private translate: TranslateService, + private prefetchHandler: AddonModResourcePrefetchHandler, private resourceHelper: AddonModResourceHelperProvider) { + this.resourceRetrieved = new EventEmitter(); + + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.description = this.module.description; + this.componentId = this.module.id; + this.externalUrl = this.module.url; + this.loaded = false; + this.refreshIcon = 'spinner'; + + this.canGetResource = this.resourceProvider.isGetResourceWSAvailable(); + + this.fetchContent().then(() => { + this.resourceProvider.logView(this.module.instance).then(() => { + this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus); + }); + }); + } + + /** + * Refresh the data. + * + * @param {any} [refresher] Refresher. + * @param {Function} [done] Function to call when done. + * @return {Promise} Promise resolved when done. + */ + doRefresh(refresher?: any, done?: () => void): Promise { + if (this.loaded) { + this.refreshIcon = 'spinner'; + + return this.resourceProvider.invalidateContent(this.module.id, this.courseId).catch(() => { + // Ignore errors. + }).then(() => { + return this.fetchContent(true); + }).finally(() => { + this.refreshIcon = 'refresh'; + refresher && refresher.complete(); + done && done(); + }); + } + } + + /** + * Expand the description. + */ + expandDescription(): void { + this.textUtils.expandText(this.translate.instant('core.description'), this.description, this.component, this.module.id); + } + + /** + * Prefetch the module. + */ + prefetch(): void { + this.courseHelper.contextMenuPrefetch(this, this.module, this.courseId); + } + + /** + * Confirm and remove downloaded files. + */ + removeFiles(): void { + this.courseHelper.confirmAndRemoveFiles(this.module, this.courseId); + } + + /** + * Download resource contents. + * + * @param {boolean} [refresh] Whether we're refreshing data. + * @return {Promise} Promise resolved when done. + */ + protected fetchContent(refresh?: boolean): Promise { + // Load module contents if needed. Passing refresh is needed to force reloading contents. + return this.courseProvider.loadModuleContents(this.module, this.courseId, null, false, refresh).then(() => { + if (!this.module.contents || !this.module.contents.length) { + return Promise.reject(null); + } + + // Get the resource instance to get the latest name/description and to know if it's embedded. + if (this.canGetResource) { + return this.resourceProvider.getResourceData(this.courseId, this.module.id).catch(() => { + // Ignore errors. + }); + } + + return this.courseProvider.getModule(this.module.id, this.courseId).catch(() => { + // Ignore errors. + }); + }).then((resource) => { + if (resource) { + this.description = resource.intro || resource.description; + this.resourceRetrieved.emit(resource); + } + + if (this.resourceHelper.isDisplayedInIframe(this.module)) { + let downloadFailed = false; + + return this.prefetchHandler.download(this.module, this.courseId).catch(() => { + // Mark download as failed but go on since the main files could have been downloaded. + downloadFailed = true; + }).then(() => { + return this.resourceHelper.getIframeSrc(this.module).then((src) => { + this.mode = 'iframe'; + + if (this.src && src.toString() == this.src.toString()) { + // 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; + } + + if (downloadFailed && this.appProvider.isOnline()) { + // We could load the main file but the download failed. Show error message. + this.domUtils.showErrorModal('core.errordownloadingsomefiles', true); + } + }); + }); + } else if (this.resourceHelper.isDisplayedEmbedded(this.module, resource && resource.display)) { + this.mode = 'embedded'; + + return this.resourceHelper.getEmbeddedHtml(this.module).then((html) => { + this.contentText = html; + }); + } else { + this.mode = 'external'; + } + }).then(() => { + // All data obtained, now fill the context menu. + this.courseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component); + }).catch((error) => { + // Error getting data, fail. + this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + }).finally(() => { + this.loaded = true; + this.refreshIcon = 'refresh'; + }); + } + + /** + * Opens a file. + */ + open(): void { + this.resourceHelper.openModuleFile(this.module, this.courseId); + } + + ngOnDestroy(): void { + this.isDestroyed = true; + this.statusObserver && this.statusObserver.off(); + } +} diff --git a/src/addon/mod/resource/lang/en.json b/src/addon/mod/resource/lang/en.json new file mode 100644 index 000000000..33c872d40 --- /dev/null +++ b/src/addon/mod/resource/lang/en.json @@ -0,0 +1,4 @@ +{ + "errorwhileloadingthecontent": "Error while loading the content.", + "openthefile": "Open the file" +} \ No newline at end of file diff --git a/src/addon/mod/resource/pages/index/index.html b/src/addon/mod/resource/pages/index/index.html new file mode 100644 index 000000000..dad3b8a4b --- /dev/null +++ b/src/addon/mod/resource/pages/index/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/addon/mod/resource/pages/index/index.module.ts b/src/addon/mod/resource/pages/index/index.module.ts new file mode 100644 index 000000000..6d6dc347d --- /dev/null +++ b/src/addon/mod/resource/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 { AddonModResourceComponentsModule } from '../../components/components.module'; +import { AddonModResourceIndexPage } from './index'; + +@NgModule({ + declarations: [ + AddonModResourceIndexPage, + ], + imports: [ + CoreDirectivesModule, + AddonModResourceComponentsModule, + IonicPageModule.forChild(AddonModResourceIndexPage), + TranslateModule.forChild() + ], +}) +export class AddonModResourceIndexPageModule {} diff --git a/src/addon/mod/resource/pages/index/index.ts b/src/addon/mod/resource/pages/index/index.ts new file mode 100644 index 000000000..0bc1de345 --- /dev/null +++ b/src/addon/mod/resource/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 { AddonModResourceIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a resource. + */ +@IonicPage({ segment: 'addon-mod-resource-index' }) +@Component({ + selector: 'page-addon-mod-resource-index', + templateUrl: 'index.html', +}) +export class AddonModResourceIndexPage { + @ViewChild(AddonModResourceIndexComponent) resourceComponent: AddonModResourceIndexComponent; + + 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 resource instance. + * + * @param {any} resource Resource instance. + */ + updateData(resource: any): void { + this.title = resource.name || this.title; + } +} diff --git a/src/addon/mod/resource/providers/helper.ts b/src/addon/mod/resource/providers/helper.ts new file mode 100644 index 000000000..a9077b3d5 --- /dev/null +++ b/src/addon/mod/resource/providers/helper.ts @@ -0,0 +1,167 @@ +// (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 { CoreDomUtilsProvider } from '@providers/utils/dom'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreCourseHelperProvider } from '@core/course/providers/helper'; +import { AddonModResourceProvider } from './resource'; +import { TranslateService } from '@ngx-translate/core'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreFilepoolProvider } from '@providers/filepool'; +import { CoreFileProvider } from '@providers/file'; +import { CoreAppProvider } from '@providers/app'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; +import { CoreTextUtilsProvider } from '@providers/utils/text'; +import { CoreConstants } from '@core/constants'; + +/** + * Service that provides helper functions for resources. + */ +@Injectable() +export class AddonModResourceHelperProvider { + + /* Constants to determine how a resource should be displayed in Moodle. */ + // Try the best way. + protected DISPLAY_AUTO = 0; + // Display using object tag. + protected DISPLAY_EMBED = 1; + + constructor(private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider, + private resourceProvider: AddonModResourceProvider, private courseHelper: CoreCourseHelperProvider, + private textUtils: CoreTextUtilsProvider, private mimetypeUtils: CoreMimetypeUtilsProvider, + private fileProvider: CoreFileProvider, private appProvider: CoreAppProvider, + private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider, + private sitesProvider: CoreSitesProvider, private translate: TranslateService) { + } + + /** + * Get the HTML to display an embedded resource. + * + * @param {any} module The module object. + * @return {Promise} Promise resolved with the iframe src. + */ + getEmbeddedHtml(module: any): Promise { + return this.courseHelper.downloadModuleWithMainFileIfNeeded(module, module.course, AddonModResourceProvider.COMPONENT, + module.id, module.contents).then((result) => { + const file = module.contents[0], + ext = this.mimetypeUtils.getFileExtension(file.filename), + type = this.mimetypeUtils.getExtensionType(ext), + mimeType = this.mimetypeUtils.getMimeType(ext); + + if (type == 'image') { + return ''; + } + + if (type == 'audio' || type == 'video') { + return '<' + type + ' controls title="' + file.filename + '"" src="' + result.path + '">' + + '' + + ''; + } + + // Shouldn't reach here, the user should have called CoreMimetypeUtilsProvider#canBeEmbedded. + return ''; + }); + } + + /** + * Download all the files needed and returns the src of the iframe. + * + * @param {any} module The module object. + * @return {Promise} Promise resolved with the iframe src. + */ + getIframeSrc(module: any): Promise { + if (!module.contents.length) { + return Promise.reject(null); + } + + const mainFile = module.contents[0]; + let mainFilePath = mainFile.filename; + + if (mainFile.filepath !== '/') { + mainFilePath = mainFile.filepath.substr(1) + mainFilePath; + } + + return this.filepoolProvider.getPackageDirUrlByUrl(this.sitesProvider.getCurrentSiteId(), module.url).then((dirPath) => { + // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser. + return this.textUtils.concatenatePaths(dirPath, mainFilePath); + }).catch(() => { + // Error getting directory, there was an error downloading or we're in browser. Return online URL. + if (this.appProvider.isOnline() && mainFile.fileurl) { + // This URL is going to be injected in an iframe, we need this to make it work. + return Promise.resolve(this.sitesProvider.getCurrentSite().fixPluginfileURL(mainFile.fileurl)); + } + + return Promise.reject(null); + }); + } + + /** + * Whether the resource has to be displayed embedded. + * + * @param {any} module The module object. + * @param {number} [display] The display mode (if available). + * @return {boolean} Whether the resource should be displayed embeded. + */ + isDisplayedEmbedded(module: any, display: number): boolean { + if (!module.contents.length || !this.fileProvider.isAvailable()) { + return false; + } + + const ext = this.mimetypeUtils.getFileExtension(module.contents[0].filename); + + return (display == this.DISPLAY_EMBED || display == this.DISPLAY_AUTO) && this.mimetypeUtils.canBeEmbedded(ext); + } + + /** + * Whether the resource has to be displayed in an iframe. + * + * @param {any} module The module object. + * @return {boolean} Whether the resource should be displayed in an iframe. + */ + isDisplayedInIframe(module: any): boolean { + if (!module.contents.length || !this.fileProvider.isAvailable()) { + return false; + } + + const ext = this.mimetypeUtils.getFileExtension(module.contents[0].filename), + mimetype = this.mimetypeUtils.getMimeType(ext); + + return mimetype == 'text/html'; + } + + /** + * Opens a file of the resource activity. + * + * @param {any} module Module where to get the contents. + * @param {number} courseId Course Id, used for completion purposes. + * @return {Promise} Resolved when done. + */ + openModuleFile(module: any, courseId: number): Promise { + const modal = this.domUtils.showModalLoading(); + + // Download and open the file from the resource contents. + return this.courseHelper.downloadModuleAndOpenFile(module, courseId, AddonModResourceProvider.COMPONENT, module.id, + module.contents).then(() => { + this.resourceProvider.logView(module.instance).then(() => { + this.courseProvider.checkModuleCompletion(courseId, module.completionstatus); + }); + }).catch((error) => { + this.domUtils.showErrorModalDefault(error, 'addon.mod_resource.errorwhileloadingthecontent', true); + }).finally(() => { + modal.dismiss(); + }); + } +} diff --git a/src/addon/mod/resource/providers/link-handler.ts b/src/addon/mod/resource/providers/link-handler.ts new file mode 100644 index 000000000..267174f97 --- /dev/null +++ b/src/addon/mod/resource/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 resource. + */ +@Injectable() +export class AddonModResourceLinkHandler extends CoreContentLinksModuleIndexHandler { + name = 'AddonModResourceLinkHandler'; + + constructor(courseHelper: CoreCourseHelperProvider) { + super(courseHelper, AddonModResourceLinkHandler.name, 'resource'); + } +} diff --git a/src/addon/mod/resource/providers/module-handler.ts b/src/addon/mod/resource/providers/module-handler.ts new file mode 100644 index 000000000..f952695a0 --- /dev/null +++ b/src/addon/mod/resource/providers/module-handler.ts @@ -0,0 +1,130 @@ +// (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 { AddonModResourceProvider } from './resource'; +import { AddonModResourceHelperProvider } from './helper'; +import { AddonModResourceIndexComponent } from '../components/index/index'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreMimetypeUtilsProvider } from '@providers/utils/mimetype'; + +/** + * Handler to support resource modules. + */ +@Injectable() +export class AddonModResourceModuleHandler implements CoreCourseModuleHandler { + name = 'resource'; + + constructor(protected resourceProvider: AddonModResourceProvider, private courseProvider: CoreCourseProvider, + protected mimetypeUtils: CoreMimetypeUtilsProvider, private resourceHelper: AddonModResourceHelperProvider) { } + + /** + * 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.resourceProvider.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 { + const handlerData = { + icon: this.courseProvider.getModuleIconSrc('resource'), + title: module.name, + class: 'addon-mod_resource-handler', + showDownloadButton: true, + action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void { + navCtrl.push('AddonModResourceIndexPage', {module: module, courseId: courseId}, options); + }, + buttons: [ { + hidden: !this.resourceHelper.isDisplayedInIframe(module), + icon: 'document', + label: 'addon.mod_resource.openthefile', + action: (event: Event, navCtrl: NavController, module: any, courseId: number): void => { + this.hideOpenButton(module, courseId).then((hide) => { + if (!hide) { + this.resourceHelper.openModuleFile(module, courseId); + } + }); + } + } ] + }; + + this.getIcon(module, courseId).then((icon) => { + handlerData.icon = icon; + }); + + this.hideOpenButton(module, courseId).then((hideOpenButton) => { + handlerData.buttons[0].hidden = hideOpenButton; + }); + + return handlerData; + } + + /** + * Returns if contents are loaded to show open button. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @return {Promise} Resolved when done. + */ + protected hideOpenButton(module: any, courseId: number): Promise { + return this.courseProvider.loadModuleContents(module, courseId).then(() => { + return this.resourceHelper.isDisplayedInIframe(module); + }); + } + + /** + * Returns the activity icon. + * + * @param {any} module The module object. + * @param {number} courseId The course ID. + * @return {Promise} Icon URL. + */ + protected getIcon(module: any, courseId: number): Promise { + return this.courseProvider.loadModuleContents(module, courseId).then(() => { + if (module.contents.length) { + const filename = module.contents[0].filename, + extension = this.mimetypeUtils.getFileExtension(filename); + if (module.contents.length == 1 || (extension != 'html' && extension != 'htm')) { + return this.mimetypeUtils.getFileIcon(filename); + } + } + + return this.courseProvider.getModuleIconSrc('resource'); + }); + } + + /** + * 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 AddonModResourceIndexComponent; + } +} diff --git a/src/addon/mod/resource/providers/pluginfile-handler.ts b/src/addon/mod/resource/providers/pluginfile-handler.ts new file mode 100644 index 000000000..daa3da78b --- /dev/null +++ b/src/addon/mod/resource/providers/pluginfile-handler.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 { Injectable } from '@angular/core'; +import { CorePluginFileHandler } from '@providers/plugin-file-delegate'; + +/** + * Handler to treat links to resource. + */ +@Injectable() +export class AddonModResourcePluginFileHandler implements CorePluginFileHandler { + name = 'AddonModResourcePluginFileHandler'; + + /** + * 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_resource/content/([0-9]+)/'); + } + } + + /** + * 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_resource/content/0/'; + } +} diff --git a/src/addon/mod/resource/providers/prefetch-handler.ts b/src/addon/mod/resource/providers/prefetch-handler.ts new file mode 100644 index 000000000..a17dc360e --- /dev/null +++ b/src/addon/mod/resource/providers/prefetch-handler.ts @@ -0,0 +1,103 @@ +// (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 { AddonModResourceProvider } from './resource'; +import { AddonModResourceHelperProvider } from './helper'; +import { CoreFilepoolProvider } from '@providers/filepool'; + +/** + * Handler to prefetch resources. + */ +@Injectable() +export class AddonModResourcePrefetchHandler extends CoreCourseModulePrefetchHandlerBase { + name = 'resource'; + component = AddonModResourceProvider.COMPONENT; + isResource = true; + + constructor(injector: Injector, protected resourceProvider: AddonModResourceProvider, + protected filepoolProvider: CoreFilepoolProvider, protected resourceHelper: AddonModResourceHelperProvider) { + super(injector); + } + + /** + * 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 { + let promise; + + if (this.resourceHelper.isDisplayedInIframe(module)) { + promise = this.filepoolProvider.getPackageDirPathByUrl(this.sitesProvider.getCurrentSiteId(), module.url); + } else { + promise = Promise.resolve(); + } + + return promise.then((dirPath) => { + const promises = []; + + promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath)); + + if (this.resourceProvider.isGetResourceWSAvailable()) { + promises.push(this.resourceProvider.getResourceData(courseId, module.id)); + } + + return Promise.all(promises); + }); + } + + /** + * 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.resourceProvider.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.resourceProvider.invalidateResourceData(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.resourceProvider.isPluginEnabled(); + } +} diff --git a/src/addon/mod/resource/providers/resource.ts b/src/addon/mod/resource/providers/resource.ts new file mode 100644 index 000000000..99559bb42 --- /dev/null +++ b/src/addon/mod/resource/providers/resource.ts @@ -0,0 +1,160 @@ +// (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 { CoreLoggerProvider } from '@providers/logger'; +import { CoreSitesProvider } from '@providers/sites'; +import { CoreUtilsProvider } from '@providers/utils/utils'; +import { CoreCourseProvider } from '@core/course/providers/course'; +import { CoreFilepoolProvider } from '@providers/filepool'; + +/** + * Service that provides some features for resources. + */ +@Injectable() +export class AddonModResourceProvider { + static COMPONENT = 'mmaModResource'; + + protected ROOT_CACHE_KEY = 'mmaModResource:'; + protected logger; + + constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private courseProvider: CoreCourseProvider, + private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider) { + this.logger = logger.getInstance('AddonModResourceProvider'); + } + + /** + * Get cache key for resource data WS calls. + * + * @param {number} courseId Course ID. + * @return {string} Cache key. + */ + protected getResourceCacheKey(courseId: number): string { + return this.ROOT_CACHE_KEY + 'resource:' + courseId; + } + + /** + * Get a resource data. + * + * @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 resource is retrieved. + */ + protected getResourceDataByKey(courseId: number, key: string, value: any, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + const params = { + courseids: [courseId] + }, + preSets = { + cacheKey: this.getResourceCacheKey(courseId) + }; + + return site.read('mod_resource_get_resources_by_courses', params, preSets).then((response) => { + if (response && response.resources) { + const currentResource = response.resources.find((resource) => { + return resource[key] == value; + }); + if (currentResource) { + return currentResource; + } + } + + return Promise.reject(null); + }); + }); + } + + /** + * Get a resource 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 resource is retrieved. + */ + getResourceData(courseId: number, cmId: number, siteId?: string): Promise { + return this.getResourceDataByKey(courseId, 'coursemodule', cmId, siteId); + } + + /** + * 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 data is invalidated. + */ + invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || this.sitesProvider.getCurrentSiteId(); + + const promises = []; + + promises.push(this.invalidateResourceData(courseId, siteId)); + promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModResourceProvider.COMPONENT, moduleId)); + promises.push(this.courseProvider.invalidateModule(moduleId, siteId)); + + return this.utils.allPromises(promises); + } + + /** + * Invalidates resource 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. + */ + invalidateResourceData(courseId: number, siteId?: string): Promise { + return this.sitesProvider.getSite(siteId).then((site) => { + return site.invalidateWsCacheForKey(this.getResourceCacheKey(courseId)); + }); + } + + /** + * Returns whether or not getResource WS available or not. + * + * @return {boolean} If WS is abalaible. + * @since 3.3 + */ + isGetResourceWSAvailable(): boolean { + return this.sitesProvider.wsAvailableInCurrentSite('mod_resource_get_resources_by_courses'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @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 the resource as being viewed. + * + * @param {number} id Module ID. + * @return {Promise} Promise resolved when the WS call is successful. + */ + logView(id: number): Promise { + const params = { + resourceid: id + }; + + return this.sitesProvider.getCurrentSite().write('mod_resource_view_resource', params); + } +} diff --git a/src/addon/mod/resource/resource.module.ts b/src/addon/mod/resource/resource.module.ts new file mode 100644 index 000000000..5b738513e --- /dev/null +++ b/src/addon/mod/resource/resource.module.ts @@ -0,0 +1,53 @@ +// (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 { AddonModResourceComponentsModule } from './components/components.module'; +import { AddonModResourceModuleHandler } from './providers/module-handler'; +import { AddonModResourceProvider } from './providers/resource'; +import { AddonModResourcePrefetchHandler } from './providers/prefetch-handler'; +import { AddonModResourceLinkHandler } from './providers/link-handler'; +import { AddonModResourcePluginFileHandler } from './providers/pluginfile-handler'; +import { AddonModResourceHelperProvider } from './providers/helper'; +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: [ + AddonModResourceComponentsModule + ], + providers: [ + AddonModResourceProvider, + AddonModResourceModuleHandler, + AddonModResourceHelperProvider, + AddonModResourcePrefetchHandler, + AddonModResourceLinkHandler, + AddonModResourcePluginFileHandler + ] +}) +export class AddonModResourceModule { + constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModResourceModuleHandler, + prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModResourcePrefetchHandler, + contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModResourceLinkHandler, + pluginfileDelegate: CorePluginFileDelegate, pluginfileHandler: AddonModResourcePluginFileHandler) { + moduleDelegate.registerHandler(moduleHandler); + prefetchDelegate.registerHandler(prefetchHandler); + contentLinksDelegate.registerHandler(linkHandler); + pluginfileDelegate.registerHandler(pluginfileHandler); + } +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 406450a66..d863e9515 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -74,6 +74,7 @@ import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofile import { AddonFilesModule } from '@addon/files/files.module'; import { AddonModBookModule } from '@addon/mod/book/book.module'; import { AddonModLabelModule } from '@addon/mod/label/label.module'; +import { AddonModResourceModule } from '@addon/mod/resource/resource.module'; import { AddonMessagesModule } from '@addon/messages/messages.module'; import { AddonPushNotificationsModule } from '@addon/pushnotifications/pushnotifications.module'; @@ -151,6 +152,7 @@ export const CORE_PROVIDERS: any[] = [ AddonFilesModule, AddonModBookModule, AddonModLabelModule, + AddonModResourceModule, AddonMessagesModule, AddonPushNotificationsModule ], diff --git a/src/app/app.scss b/src/app/app.scss index d5fd9b7ce..84557fa0e 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -483,10 +483,12 @@ textarea { .core-circle:before { content: ' \25CF'; - font-size: 20px; } @each $color-name, $color-base, $color-contrast in get-colors($colors) { + .core-#{$color-name}-circle { + margin: 0 4px; + } .core-#{$color-name}-circle:before { @extend .core-circle:before; color: $color-base; diff --git a/src/classes/site.ts b/src/classes/site.ts index 682561a4e..330b59ea3 100644 --- a/src/classes/site.ts +++ b/src/classes/site.ts @@ -689,13 +689,13 @@ export class CoreSite { * @return {Promise} Promise resolved with the WS response. */ protected getFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, emergency?: boolean): Promise { - const id = this.getCacheId(method, data); - let promise; - if (!this.db || !preSets.getFromCache) { return Promise.reject(null); } + const id = this.getCacheId(method, data); + let promise; + if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) { promise = this.db.getRecords(this.WS_CACHE_TABLE, { key: preSets.cacheKey }).then((entries) => { if (!entries.length) { @@ -751,36 +751,37 @@ export class CoreSite { * @return {Promise} Promise resolved when the response is saved. */ protected saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise { - const id = this.getCacheId(method, data), - entry: any = { - id: id, - data: JSON.stringify(response) - }; - let cacheExpirationTime = CoreConfigConstants.cache_expiration_time, - promise; - if (!this.db) { return Promise.reject(null); + } + + let promise; + + if (preSets.uniqueCacheKey) { + // Cache key must be unique, delete all entries with same cache key. + promise = this.deleteFromCache(method, data, preSets, true).catch(() => { + // Ignore errors. + }); } else { - if (preSets.uniqueCacheKey) { - // Cache key must be unique, delete all entries with same cache key. - promise = this.deleteFromCache(method, data, preSets, true).catch(() => { - // Ignore errors. - }); - } else { - promise = Promise.resolve(); + promise = Promise.resolve(); + } + + return promise.then(() => { + const id = this.getCacheId(method, data), + entry: any = { + id: id, + data: JSON.stringify(response) + }; + let cacheExpirationTime = CoreConfigConstants.cache_expiration_time; + + cacheExpirationTime = isNaN(cacheExpirationTime) ? 300000 : cacheExpirationTime; + entry.expirationTime = new Date().getTime() + cacheExpirationTime; + if (preSets.cacheKey) { + entry.key = preSets.cacheKey; } - return promise.then(() => { - cacheExpirationTime = isNaN(cacheExpirationTime) ? 300000 : cacheExpirationTime; - entry.expirationTime = new Date().getTime() + cacheExpirationTime; - if (preSets.cacheKey) { - entry.key = preSets.cacheKey; - } - - return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, { id: id }); - }); - } + return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, { id: id }); + }); } /** @@ -793,17 +794,17 @@ export class CoreSite { * @return {Promise} Promise resolved when the entries are deleted. */ protected deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise { - const id = this.getCacheId(method, data); - if (!this.db) { return Promise.reject(null); - } else { - if (allCacheKey) { - return this.db.deleteRecords(this.WS_CACHE_TABLE, { key: preSets.cacheKey }); - } else { - return this.db.deleteRecords(this.WS_CACHE_TABLE, { id: id }); - } } + + const id = this.getCacheId(method, data); + + if (allCacheKey) { + return this.db.deleteRecords(this.WS_CACHE_TABLE, { key: preSets.cacheKey }); + } + + return this.db.deleteRecords(this.WS_CACHE_TABLE, { id: id }); } /* @@ -985,9 +986,9 @@ export class CoreSite { return Promise.resolve({ code: 0 }); } - const observable = this.http.post(checkUrl, { service: service }).timeout(CoreConstants.WS_TIMEOUT); + const promise = this.http.post(checkUrl, { service: service }).timeout(CoreConstants.WS_TIMEOUT).toPromise(); - return this.utils.observableToPromise(observable).then((data: any) => { + return promise.then((data: any) => { if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') { if (!retrying) { this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl); diff --git a/src/components/iframe/iframe.scss b/src/components/iframe/iframe.scss index 12c75ceb3..562b80430 100644 --- a/src/components/iframe/iframe.scss +++ b/src/components/iframe/iframe.scss @@ -2,4 +2,7 @@ core-iframe { > div { height: 100%; } + iframe { + border: 0; + } } diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts index 005ec72a9..fa61c91fe 100644 --- a/src/components/iframe/iframe.ts +++ b/src/components/iframe/iframe.ts @@ -59,7 +59,7 @@ export class CoreIframeComponent implements OnInit { this.iframeHeight = this.domUtils.formatPixelsSize(this.iframeHeight) || '100%'; // Show loading only with external URLs. - this.loading = !!this.src.match(/^https?:\/\//i); + this.loading = !this.src || !!this.src.match(/^https?:\/\//i); this.treatFrame(iframe); diff --git a/src/core/course/components/module/module.scss b/src/core/course/components/module/module.scss index 8a15d7dd8..ecc368e10 100644 --- a/src/core/course/components/module/module.scss +++ b/src/core/course/components/module/module.scss @@ -59,6 +59,8 @@ core-course-module { a.core-course-module-handler .core-module-icon { margin-top: $label-md-margin-top; margin-bottom: $label-md-margin-bottom; + width: 24px; + height: 24px; } .core-module-title core-format-text { diff --git a/src/core/course/components/module/module.ts b/src/core/course/components/module/module.ts index fef73a640..aaee9cb5c 100644 --- a/src/core/course/components/module/module.ts +++ b/src/core/course/components/module/module.ts @@ -102,6 +102,9 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { */ buttonClicked(event: Event, button: CoreCourseModuleHandlerButton): void { if (button && button.action) { + event.preventDefault(); + event.stopPropagation(); + button.action(event, this.navCtrl, this.module, this.courseId); } } diff --git a/src/core/course/providers/helper.ts b/src/core/course/providers/helper.ts index 236b227dd..9457b4ff3 100644 --- a/src/core/course/providers/helper.ts +++ b/src/core/course/providers/helper.ts @@ -585,7 +585,7 @@ export class CoreCourseHelperProvider { * @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, + 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(); @@ -598,7 +598,6 @@ export class CoreCourseHelperProvider { const mainFile = files[0], fileUrl = this.fileHelper.getFileUrl(mainFile), timemodified = this.fileHelper.getFileTimemodified(mainFile), - prefetchHandler = this.prefetchDelegate.getPrefetchHandlerFor(module), result = { fixedUrl: undefined, path: undefined, diff --git a/src/core/mainmenu/pages/menu/menu.ts b/src/core/mainmenu/pages/menu/menu.ts index d909aa13a..19476a899 100644 --- a/src/core/mainmenu/pages/menu/menu.ts +++ b/src/core/mainmenu/pages/menu/menu.ts @@ -64,7 +64,6 @@ export class CoreMainMenuPage implements OnDestroy { }; protected moreTabAdded = false; protected redirectPageLoaded = false; - protected updateBadgeObserver; constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, navParams: NavParams, private navCtrl: NavController, private eventsProvider: CoreEventsProvider) { @@ -85,15 +84,6 @@ export class CoreMainMenuPage implements OnDestroy { const site = this.sitesProvider.getCurrentSite(), displaySiteHome = site.getInfo() && site.getInfo().userhomepage === 0; - this.updateBadgeObserver = this.eventsProvider.on(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, (data) => { - const tab = this.tabs.find((tab) => { - return tab.showBadge && tab['name'] == data.name; - }); - if (tab) { - tab.badge = data.badge; - } - }, site.getId()); - this.subscription = this.menuDelegate.getHandlers().subscribe((handlers) => { handlers = handlers.slice(0, CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Get main handlers. @@ -138,6 +128,5 @@ export class CoreMainMenuPage implements OnDestroy { */ ngOnDestroy(): void { this.subscription && this.subscription.unsubscribe(); - this.updateBadgeObserver && this.updateBadgeObserver.off(); } } diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts index 0c396bfd9..8f36a3101 100644 --- a/src/core/mainmenu/pages/more/more.ts +++ b/src/core/mainmenu/pages/more/more.ts @@ -40,7 +40,6 @@ export class CoreMainMenuMorePage implements OnDestroy { protected subscription; protected langObserver; protected updateSiteObserver; - protected updateBadgeObserver; constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider, private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider, @@ -61,15 +60,6 @@ export class CoreMainMenuMorePage implements OnDestroy { this.handlers = handlers.slice(CoreMainMenuProvider.NUM_MAIN_HANDLERS); // Remove the main handlers. this.handlersLoaded = this.menuDelegate.areHandlersLoaded(); }); - - this.updateBadgeObserver = this.eventsProvider.on(CoreMainMenuDelegate.UPDATE_BADGE_EVENT, (data) => { - const handler = this.handlers.find((handler) => { - return handler.showBadge && handler['name'] == data.name; - }); - if (handler) { - handler.badge = data.badge; - } - }, this.sitesProvider.getCurrentSiteId()); } /** @@ -79,7 +69,6 @@ export class CoreMainMenuMorePage implements OnDestroy { if (this.subscription) { this.subscription.unsubscribe(); } - this.updateBadgeObserver && this.updateBadgeObserver.off(); } /** diff --git a/src/core/mainmenu/providers/delegate.ts b/src/core/mainmenu/providers/delegate.ts index ef87d35f3..23ac7e847 100644 --- a/src/core/mainmenu/providers/delegate.ts +++ b/src/core/mainmenu/providers/delegate.ts @@ -113,8 +113,6 @@ export class CoreMainMenuDelegate extends CoreDelegate { protected siteHandlers: Subject = new BehaviorSubject([]); protected featurePrefix = '$mmSideMenuDelegate_'; - static UPDATE_BADGE_EVENT = 'update_main_menu_badge'; - constructor(protected loggerProvider: CoreLoggerProvider, protected sitesProvider: CoreSitesProvider, protected eventsProvider: CoreEventsProvider) { super('CoreMainMenuDelegate', loggerProvider, sitesProvider, eventsProvider); diff --git a/src/providers/lang.ts b/src/providers/lang.ts index 9f4dfd699..5622c64e4 100644 --- a/src/providers/lang.ts +++ b/src/providers/lang.ts @@ -161,7 +161,7 @@ export class CoreLangProvider { return language; }).catch(() => { // User hasn't defined a language. If default language is forced, use it. - if (CoreConfigConstants.forcedefaultlanguage && !CoreConfigConstants.forcedefaultlanguage) { + if (!CoreConfigConstants.forcedefaultlanguage) { return CoreConfigConstants.default_lang; } diff --git a/src/providers/sites.ts b/src/providers/sites.ts index c4ecc8268..4b34327a9 100644 --- a/src/providers/sites.ts +++ b/src/providers/sites.ts @@ -387,9 +387,9 @@ export class CoreSitesProvider { data.service = 'c'; } - const observable = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.WS_TIMEOUT); + const promise = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.WS_TIMEOUT).toPromise(); - return this.utils.observableToPromise(observable).catch((error) => { + return promise.catch((error) => { return Promise.reject(error.message); }).then((data: any) => { if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { @@ -426,9 +426,9 @@ export class CoreSitesProvider { password: password, service: service }, - observable = this.http.post(siteUrl + '/login/token.php', params).timeout(CoreConstants.WS_TIMEOUT); + promise = this.http.post(siteUrl + '/login/token.php', params).timeout(CoreConstants.WS_TIMEOUT).toPromise(); - return this.utils.observableToPromise(observable).then((data: any): any => { + return promise.then((data: any): any => { if (typeof data == 'undefined') { return Promise.reject(this.translate.instant('core.cannotconnect')); } else { diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts index f482b279d..40044549d 100644 --- a/src/providers/utils/utils.ts +++ b/src/providers/utils/utils.ts @@ -850,26 +850,6 @@ export class CoreUtilsProvider { return mapped; } - /** - * Given an observable, convert it to a Promise that will resolve with the first received value. - * - * @param {Observable} obs The observable to convert. - * @return {Promise} Promise. - */ - observableToPromise(obs: Observable): Promise { - return new Promise((resolve, reject): void => { - const subscription = obs.subscribe((data) => { - // Data received, unsubscribe. - subscription.unsubscribe(); - resolve(data); - }, (error) => { - // Data received, unsubscribe. - subscription.unsubscribe(); - reject(error); - }); - }); - } - /** * Similar to AngularJS $q.defer(). * diff --git a/src/providers/ws.ts b/src/providers/ws.ts index 660887a06..267ac509d 100644 --- a/src/providers/ws.ts +++ b/src/providers/ws.ts @@ -238,9 +238,9 @@ export class CoreWSProvider { siteUrl = preSets.siteUrl + '/lib/ajax/service.php'; - const observable = this.http.post(siteUrl, JSON.stringify(ajaxData)).timeout(CoreConstants.WS_TIMEOUT); + const promise = this.http.post(siteUrl, JSON.stringify(ajaxData)).timeout(CoreConstants.WS_TIMEOUT).toPromise(); - return this.utils.observableToPromise(observable).then((data: any) => { + return promise.then((data: any) => { // Some moodle web services return null. // If the responseExpected value is set then so long as no data is returned, we create a blank object. if (!data && !preSets.responseExpected) { @@ -486,7 +486,7 @@ export class CoreWSProvider { let promise = this.getPromiseHttp('head', url); if (!promise) { - promise = this.utils.observableToPromise(this.commonHttp.head(url).timeout(CoreConstants.WS_TIMEOUT)); + promise = this.commonHttp.head(url).timeout(CoreConstants.WS_TIMEOUT).toPromise(); promise = this.setPromiseHttp(promise, 'head', url); } @@ -503,11 +503,18 @@ export class CoreWSProvider { * @return {Promise} Promise resolved with the response data in success and rejected with CoreWSError if it fails. */ performPost(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise { - // Perform the post request. - const observable = this.http.post(siteUrl, ajaxData).timeout(CoreConstants.WS_TIMEOUT); - let promise; + const options = {}; - promise = this.utils.observableToPromise(observable).then((data: any) => { + // This is done because some returned values like 0 are treated as null if responseType is json. + if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') { + // Avalaible values are: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType + options['responseType'] = 'text'; + } + + // Perform the post request. + let promise = this.http.post(siteUrl, ajaxData, options).timeout(CoreConstants.WS_TIMEOUT).toPromise(); + + promise = promise.then((data: any) => { // Some moodle web services return null. // If the responseExpected value is set to false, we create a blank object if the response is null. if (!data && !preSets.responseExpected) { @@ -517,9 +524,35 @@ export class CoreWSProvider { if (!data) { return Promise.reject(this.utils.createFakeWSError('core.serverconnection', true)); } else if (typeof data != preSets.typeExpected) { - this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); + // If responseType is text an string will be returned, parse before returning. + if (typeof data == 'string') { + if (preSets.typeExpected == 'number') { + data = Number(data); + if (isNaN(data)) { + this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); - return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); + return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); + } + } else if (preSets.typeExpected == 'boolean') { + if (data === 'true') { + data = true; + } else if (data === 'false') { + data = false; + } else { + this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); + + return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); + } + } else { + this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); + + return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); + } + } else { + this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); + + return Promise.reject(this.utils.createFakeWSError('core.errorinvalidresponse', true)); + } } if (typeof data.exception !== 'undefined') {