diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index c467b8a8d..382292c28 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -22,6 +22,7 @@ import { AddonModLabelModule } from './label/label.module'; import { AddonModLessonModule } from './lesson/lesson.module'; import { AddonModPageModule } from './page/page.module'; import { AddonModQuizModule } from './quiz/quiz.module'; +import { AddonModResourceModule } from './resource/resource.module'; import { AddonModUrlModule } from './url/url.module'; @NgModule({ @@ -35,6 +36,7 @@ import { AddonModUrlModule } from './url/url.module'; AddonModQuizModule, AddonModUrlModule, AddonModLabelModule, + AddonModResourceModule, AddonModFolderModule, ], providers: [], diff --git a/src/addons/mod/resource/components/components.module.ts b/src/addons/mod/resource/components/components.module.ts new file mode 100644 index 000000000..a4e217b90 --- /dev/null +++ b/src/addons/mod/resource/components/components.module.ts @@ -0,0 +1,36 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSharedModule } from '@/core/shared.module'; +import { CoreCourseComponentsModule } from '@features/course/components/components.module'; + +import { AddonModResourceIndexComponent } from './index/index'; + +@NgModule({ + declarations: [ + AddonModResourceIndexComponent, + ], + imports: [ + CoreSharedModule, + CoreCourseComponentsModule, + ], + providers: [ + ], + exports: [ + AddonModResourceIndexComponent, + ], +}) +export class AddonModResourceComponentsModule {} diff --git a/src/addons/mod/resource/components/index/addon-mod-resource-index.html b/src/addons/mod/resource/components/index/addon-mod-resource-index.html new file mode 100644 index 000000000..56757e8d1 --- /dev/null +++ b/src/addons/mod/resource/components/index/addon-mod-resource-index.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + {{ 'addon.mod_resource.openthefile' | translate }} + +
+ +
diff --git a/src/addons/mod/resource/components/index/index.ts b/src/addons/mod/resource/components/index/index.ts new file mode 100644 index 000000000..4b1c9b8cf --- /dev/null +++ b/src/addons/mod/resource/components/index/index.ts @@ -0,0 +1,177 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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, Optional } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { + CoreCourseModuleMainResourceComponent, +} from '@features/course/classes/main-resource-component'; +import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; +import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreSites } from '@services/sites'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { + AddonModResource, + AddonModResourceCustomData, + AddonModResourceProvider, + AddonModResourceResource, +} from '../../services/resource'; +import { AddonModResourceHelper } from '../../services/resource-helper'; + +/** + * Component that displays a resource. + */ +@Component({ + selector: 'addon-mod-resource-index', + templateUrl: 'addon-mod-resource-index.html', +}) +export class AddonModResourceIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { + + component = AddonModResourceProvider.COMPONENT; + + canGetResource = false; + mode = ''; + src = ''; + contentText = ''; + displayDescription = true; + warning = ''; + + constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { + super('AddonModResourceIndexComponent', courseContentsPage); + } + + /** + * Component being initialized. + */ + async ngOnInit(): Promise { + super.ngOnInit(); + + this.canGetResource = AddonModResource.isGetResourceWSAvailable(); + + await this.loadContent(); + try { + await AddonModResource.logView(this.module!.instance!, this.module!.name); + CoreCourse.checkModuleCompletion(this.courseId!, this.module!.completiondata); + } catch { + // Ignore errors. + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + protected async invalidateContent(): Promise { + return AddonModResource.invalidateContent(this.module!.id, this.courseId!); + } + + /** + * Download resource contents. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh?: boolean): Promise { + // Load module contents if needed. Passing refresh is needed to force reloading contents. + await CoreCourse.loadModuleContents(this.module!, this.courseId, undefined, false, refresh); + + if (!this.module!.contents || !this.module!.contents.length) { + throw new CoreError(Translate.instant('core.filenotfound')); + } + + let resource: AddonModResourceResource | CoreCourseWSModule | undefined; + let options: AddonModResourceCustomData = {}; + + // Get the resource instance to get the latest name/description and to know if it's embedded. + if (this.canGetResource) { + resource = await CoreUtils.ignoreErrors(AddonModResource.getResourceData(this.courseId!, this.module!.id)); + this.description = resource?.intro || ''; + options = resource?.displayoptions ? CoreTextUtils.unserialize(resource.displayoptions) : {}; + } else { + resource = await CoreUtils.ignoreErrors(CoreCourse.getModule(this.module!.id, this.courseId)); + this.description = resource?.description || ''; + options = resource?.customdata ? CoreTextUtils.unserialize(CoreTextUtils.parseJSON(resource.customdata)) : {}; + } + + try { + if (resource) { + this.displayDescription = typeof options.printintro == 'undefined' || !!options.printintro; + this.dataRetrieved.emit(resource); + } + + if (AddonModResourceHelper.isDisplayedInIframe(this.module!)) { + const downloadResult = await this.downloadResourceIfNeeded(refresh, true); + const src = await AddonModResourceHelper.getIframeSrc(this.module!); + 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; + } + + this.warning = downloadResult.failed + ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) + : ''; + + return; + } + + if (resource && 'display' in resource && AddonModResourceHelper.isDisplayedEmbedded(this.module!, resource.display)) { + this.mode = 'embedded'; + this.warning = ''; + + this.contentText = await AddonModResourceHelper.getEmbeddedHtml(this.module!, this.courseId!); + this.mode = this.contentText.length > 0 ? 'embedded' : 'external'; + } else { + this.mode = 'external'; + this.warning = ''; + } + } finally { + this.fillContextMenu(refresh); + } + } + + /** + * Opens a file. + * + * @return Promise resolved when done. + */ + async open(): Promise { + let downloadable = await CoreCourseModulePrefetchDelegate.isModuleDownloadable(this.module!, this.courseId!); + + if (downloadable) { + // Check if the main file is downloadle. + // This isn't done in "isDownloadable" to prevent extra WS calls in the course page. + downloadable = await AddonModResourceHelper.isMainFileDownloadable(this.module!); + + if (downloadable) { + return AddonModResourceHelper.openModuleFile(this.module!, this.courseId!); + } + } + + // The resource cannot be downloaded, open the activity in browser. + await CoreSites.getCurrentSite()?.openInBrowserWithAutoLoginIfSameSite(this.module!.url!); + } + +} diff --git a/src/addons/mod/resource/lang.json b/src/addons/mod/resource/lang.json new file mode 100644 index 000000000..bd7e9cefb --- /dev/null +++ b/src/addons/mod/resource/lang.json @@ -0,0 +1,7 @@ +{ + "errorwhileloadingthecontent": "Error while loading the content.", + "modifieddate": "Modified {{$a}}", + "modulenameplural": "Files", + "openthefile": "Open the file", + "uploadeddate": "Uploaded {{$a}}" +} \ No newline at end of file diff --git a/src/addons/mod/resource/pages/index/index.html b/src/addons/mod/resource/pages/index/index.html new file mode 100644 index 000000000..d8f0497a8 --- /dev/null +++ b/src/addons/mod/resource/pages/index/index.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/resource/pages/index/index.page.ts b/src/addons/mod/resource/pages/index/index.page.ts new file mode 100644 index 000000000..23f01ad08 --- /dev/null +++ b/src/addons/mod/resource/pages/index/index.page.ts @@ -0,0 +1,30 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; +import { AddonModResourceIndexComponent } from '../../components/index/index'; + +/** + * Page that displays a resource. + */ +@Component({ + selector: 'page-addon-mod-resource-index', + templateUrl: 'index.html', +}) +export class AddonModResourceIndexPage extends CoreCourseModuleMainActivityPage { + + @ViewChild(AddonModResourceIndexComponent) activityComponent?: AddonModResourceIndexComponent; + +} diff --git a/src/addons/mod/resource/resource-lazy.module.ts b/src/addons/mod/resource/resource-lazy.module.ts new file mode 100644 index 000000000..49354852a --- /dev/null +++ b/src/addons/mod/resource/resource-lazy.module.ts @@ -0,0 +1,38 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreSharedModule } from '@/core/shared.module'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { AddonModResourceComponentsModule } from './components/components.module'; +import { AddonModResourceIndexPage } from './pages/index/index.page'; + +const routes: Routes = [ + { + path: ':courseId/:cmId', + component: AddonModResourceIndexPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + AddonModResourceComponentsModule, + ], + declarations: [ + AddonModResourceIndexPage, + ], +}) +export class AddonModResourceLazyModule {} diff --git a/src/addons/mod/resource/resource.module.ts b/src/addons/mod/resource/resource.module.ts new file mode 100644 index 000000000..a546a08f6 --- /dev/null +++ b/src/addons/mod/resource/resource.module.ts @@ -0,0 +1,56 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { Routes } from '@angular/router'; +import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; +import { CorePluginFileDelegate } from '@services/plugin-file-delegate'; +import { AddonModResourceComponentsModule } from './components/components.module'; +import { AddonModResourceIndexLinkHandler } from './services/handlers/index-link'; +import { AddonModResourceListLinkHandler } from './services/handlers/list-link'; +import { AddonModResourceModuleHandlerService, AddonModResourceModuleHandler } from './services/handlers/module'; +import { AddonModResourcePluginFileHandler } from './services/handlers/pluginfile'; +import { AddonModResourcePrefetchHandler } from './services/handlers/prefetch'; + +const routes: Routes = [ + { + path: AddonModResourceModuleHandlerService.PAGE_NAME, + loadChildren: () => import('./resource-lazy.module').then(m => m.AddonModResourceLazyModule), + }, +]; + +@NgModule({ + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + AddonModResourceComponentsModule, + ], + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + deps: [], + useFactory: () => () => { + CoreCourseModuleDelegate.registerHandler(AddonModResourceModuleHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModResourceIndexLinkHandler.instance); + CoreContentLinksDelegate.registerHandler(AddonModResourceListLinkHandler.instance); + CoreCourseModulePrefetchDelegate.registerHandler(AddonModResourcePrefetchHandler.instance); + CorePluginFileDelegate.registerHandler(AddonModResourcePluginFileHandler.instance); + }, + }, + ], +}) +export class AddonModResourceModule {} diff --git a/src/addons/mod/resource/services/handlers/index-link.ts b/src/addons/mod/resource/services/handlers/index-link.ts new file mode 100644 index 000000000..6c6e46ef2 --- /dev/null +++ b/src/addons/mod/resource/services/handlers/index-link.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 '@features/contentlinks/classes/module-index-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModResource } from '../resource'; + +/** + * Handler to treat links to resource. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourceIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler { + + name = 'AddonModResourceLinkHandler'; + + constructor() { + super('AddonModResource', 'resource', 'r'); + } + + /** + * @inheritdoc + */ + isEnabled(siteId: string): Promise { + return AddonModResource.isPluginEnabled(siteId); + } + +} +export const AddonModResourceIndexLinkHandler = makeSingleton(AddonModResourceIndexLinkHandlerService); diff --git a/src/addons/mod/resource/services/handlers/list-link.ts b/src/addons/mod/resource/services/handlers/list-link.ts new file mode 100644 index 000000000..2a262ff0a --- /dev/null +++ b/src/addons/mod/resource/services/handlers/list-link.ts @@ -0,0 +1,40 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler'; +import { makeSingleton } from '@singletons'; +import { AddonModResource } from '../resource'; + +/** + * Handler to treat links to resource list page. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourceListLinkHandlerService extends CoreContentLinksModuleListHandler { + + name = 'AddonModResourceListLinkHandler'; + + constructor() { + super('AddonModResource', 'resource'); + } + + /** + * @inheritdoc + */ + isEnabled(siteId: string): Promise { + return AddonModResource.isPluginEnabled(siteId); + } + +} +export const AddonModResourceListLinkHandler = makeSingleton(AddonModResourceListLinkHandlerService); diff --git a/src/addons/mod/resource/services/handlers/module.ts b/src/addons/mod/resource/services/handlers/module.ts new file mode 100644 index 000000000..cf47524d7 --- /dev/null +++ b/src/addons/mod/resource/services/handlers/module.ts @@ -0,0 +1,259 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreConstants } from '@/core/constants'; +import { Injectable, Type } from '@angular/core'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseModuleContentFile } from '@features/course/services/course'; +import { CoreCourseModule } from '@features/course/services/course-helper'; +import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreTimeUtils } from '@services/utils/time'; +import { CoreWSExternalFile } from '@services/ws'; +import { makeSingleton, Translate } from '@singletons'; +import { AddonModResourceIndexComponent } from '../../components/index'; +import { AddonModResource, AddonModResourceCustomData } from '../resource'; +import { AddonModResourceHelper } from '../resource-helper'; + +/** + * Handler to support resource modules. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourceModuleHandlerService implements CoreCourseModuleHandler { + + static readonly PAGE_NAME = 'mod_resource'; + + name = 'AddonModResource'; + modName = 'resource'; + + supportedFeatures = { + [CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE, + [CoreConstants.FEATURE_GROUPS]: false, + [CoreConstants.FEATURE_GROUPINGS]: false, + [CoreConstants.FEATURE_MOD_INTRO]: true, + [CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true, + [CoreConstants.FEATURE_GRADE_HAS_GRADE]: false, + [CoreConstants.FEATURE_GRADE_OUTCOMES]: false, + [CoreConstants.FEATURE_BACKUP_MOODLE2]: true, + [CoreConstants.FEATURE_SHOW_DESCRIPTION]: true, + }; + + /** + * @inheritdoc + */ + isEnabled(): Promise { + return AddonModResource.isPluginEnabled(); + } + + /** + * Get the data required to display the module in the course contents view. + * + * @param module The module object. + * @param courseId The course ID. + * @param sectionId The section ID. + * @return Data to render the module. + */ + getData(module: CoreCourseAnyModuleData, courseId: number): CoreCourseModuleHandlerData { + const updateStatus = (status: string): void => { + handlerData.buttons![0].hidden = status !== CoreConstants.DOWNLOADED || + AddonModResourceHelper.isDisplayedInIframe(module); + }; + + const handlerData: CoreCourseModuleHandlerData = { + icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined), + title: module.name, + class: 'addon-mod_resource-handler', + showDownloadButton: true, + action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void { + options = options || {}; + options.params = options.params || {}; + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; + + CoreNavigator.navigateToSitePath(AddonModResourceModuleHandlerService.PAGE_NAME + routeParams, options); + }, + updateStatus: updateStatus.bind(this), + buttons: [{ + hidden: true, + icon: 'document', + label: 'addon.mod_resource.openthefile', + action: async (event: Event, module: CoreCourseModule, courseId: number): Promise => { + const hide = await this.hideOpenButton(module, courseId); + if (!hide) { + AddonModResourceHelper.openModuleFile(module, courseId); + } + }, + }], + }; + + this.getResourceData(module, courseId, handlerData).then((data) => { + handlerData.icon = data.icon; + handlerData.extraBadge = data.extra; + handlerData.extraBadgeColor = 'light'; + + return; + }).catch(() => { + // Ignore errors. + }); + + return handlerData; + } + + /** + * Returns if contents are loaded to show open button. + * + * @param module The module object. + * @param courseId The course ID. + * @return Resolved when done. + */ + protected async hideOpenButton(module: CoreCourseAnyModuleData, courseId: number): Promise { + if (!('contentsinfo' in module) || !module.contentsinfo) { + await CoreCourse.loadModuleContents(module, courseId, undefined, false, false, undefined, this.modName); + } + + const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, courseId); + + return status !== CoreConstants.DOWNLOADED || AddonModResourceHelper.isDisplayedInIframe(module); + } + + /** + * Returns the activity icon and data. + * + * @param module The module object. + * @param courseId The course ID. + * @return Resource data. + */ + protected async getResourceData( + module: CoreCourseAnyModuleData, + courseId: number, + handlerData: CoreCourseModuleHandlerData, + ): Promise { + const promises: Promise[] = []; + let infoFiles: CoreWSExternalFile[] = []; + let options: AddonModResourceCustomData = {}; + + // Check if the button needs to be shown or not. + promises.push(this.hideOpenButton(module, courseId).then((hideOpenButton) => { + handlerData.buttons![0].hidden = hideOpenButton; + + return; + })); + + if ('customdata' in module && typeof module.customdata != 'undefined') { + options = CoreTextUtils.unserialize(CoreTextUtils.parseJSON(module.customdata)); + } else if (AddonModResource.isGetResourceWSAvailable()) { + // Get the resource data. + promises.push(AddonModResource.getResourceData(courseId, module.id).then((info) => { + infoFiles = info.contentfiles; + options = CoreTextUtils.unserialize(info.displayoptions); + + return; + })); + } + + await Promise.all(promises); + + const files: (CoreCourseModuleContentFile | CoreWSExternalFile)[] = module.contents && module.contents.length + ? module.contents + : infoFiles; + + const resourceData: AddonResourceHandlerData = { + icon: '', + extra: '', + }; + const extra: string[] = []; + + if ('contentsinfo' in module && module.contentsinfo) { + // No need to use the list of files. + const mimetype = module.contentsinfo.mimetypes[0]; + if (mimetype) { + resourceData.icon = CoreMimetypeUtils.getMimetypeIcon(mimetype); + } + resourceData.extra = CoreTextUtils.cleanTags(module.afterlink); + + } else if (files && files.length) { + const file = files[0]; + + resourceData.icon = CoreMimetypeUtils.getFileIcon(file.filename || ''); + + if (options.showsize) { + const size = options.filedetails + ? options.filedetails.size + : files.reduce((result, file) => result + (file.filesize || 0), 0); + + extra.push(CoreTextUtils.bytesToSize(size, 1)); + } + + if (options.showtype) { + // We should take it from options.filedetails.size if avalaible but it's already translated. + extra.push(CoreMimetypeUtils.getMimetypeDescription(file)); + } + + if (options.showdate) { + const timecreated = 'timecreated' in file ? file.timecreated : 0; + + if (options.filedetails && options.filedetails.modifieddate) { + extra.push(Translate.instant( + 'addon.mod_resource.modifieddate', + { $a: CoreTimeUtils.userDate(options.filedetails.modifieddate * 1000, 'core.strftimedatetimeshort') }, + )); + } else if (options.filedetails && options.filedetails.uploadeddate) { + extra.push(Translate.instant( + 'addon.mod_resource.uploadeddate', + { $a: CoreTimeUtils.userDate(options.filedetails.uploadeddate * 1000, 'core.strftimedatetimeshort') }, + )); + } else if ((file.timemodified || 0) > timecreated + CoreConstants.SECONDS_MINUTE * 5) { + /* Modified date may be up to several minutes later than uploaded date just because + teacher did not submit the form promptly. Give teacher up to 5 minutes to do it. */ + extra.push(Translate.instant( + 'addon.mod_resource.modifieddate', + { $a: CoreTimeUtils.userDate((file.timemodified || 0) * 1000, 'core.strftimedatetimeshort') }, + )); + } else { + extra.push(Translate.instant( + 'addon.mod_resource.uploadeddate', + { $a: CoreTimeUtils.userDate(timecreated * 1000, 'core.strftimedatetimeshort') }, + )); + } + } + + resourceData.extra += extra.join(' '); + } + + // No previously set, just set the icon. + if (resourceData.icon == '') { + resourceData.icon = CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined); + } + + return resourceData; + } + + /** + * @inheritdoc + */ + async getMainComponent(): Promise | undefined> { + return AddonModResourceIndexComponent; + } + +} +export const AddonModResourceModuleHandler = makeSingleton(AddonModResourceModuleHandlerService); + + +type AddonResourceHandlerData = { + icon: string; + extra: string; +} +; diff --git a/src/addons/mod/resource/services/handlers/pluginfile.ts b/src/addons/mod/resource/services/handlers/pluginfile.ts new file mode 100644 index 000000000..96a134ca4 --- /dev/null +++ b/src/addons/mod/resource/services/handlers/pluginfile.ts @@ -0,0 +1,55 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 '@services/plugin-file-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Handler to treat links to resource. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourcePluginFileHandlerService implements CorePluginFileHandler { + + name = 'AddonModResourcePluginFileHandler'; + component = 'mod_resource'; + + /** + * @inheritdoc + */ + getComponentRevisionRegExp(args: string[]): RegExp | undefined { + // Check filearea. + if (args[2] == 'content') { + // Component + Filearea + Revision + return new RegExp('/mod_resource/content/([0-9]+)/'); + } + } + + /** + * @inheritdoc + */ + getComponentRevisionReplace(): string { + // Component + Filearea + Revision + return '/mod_resource/content/0/'; + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + +} +export const AddonModResourcePluginFileHandler = makeSingleton(AddonModResourcePluginFileHandlerService); diff --git a/src/addons/mod/resource/services/handlers/prefetch.ts b/src/addons/mod/resource/services/handlers/prefetch.ts new file mode 100644 index 000000000..15886949e --- /dev/null +++ b/src/addons/mod/resource/services/handlers/prefetch.ts @@ -0,0 +1,120 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreConstants } from '@/core/constants'; +import { Injectable } from '@angular/core'; +import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites } from '@services/sites'; +import { makeSingleton } from '@singletons'; +import { AddonModResource, AddonModResourceProvider } from '../resource'; +import { AddonModResourceHelper } from '../resource-helper'; + +/** + * Handler to prefetch resources. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourcePrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase { + + name = 'AddonModResource'; + modName = 'resource'; + component = AddonModResourceProvider.COMPONENT; + + /** + * @inheritdoc + */ + determineStatus(module: CoreCourseAnyModuleData, status: string): string { + if (status == CoreConstants.DOWNLOADED && module) { + // If the main file is an external file, always display the module as outdated. + if ('contentsinfo' in module && module.contentsinfo) { + if (module.contentsinfo.repositorytype) { + // It's an external file. + return CoreConstants.OUTDATED; + } + } else if (module.contents) { + const mainFile = module.contents[0]; + if (mainFile && mainFile.isexternalfile) { + return CoreConstants.OUTDATED; + } + } + } + + return status; + } + + /** + * @inheritdoc + */ + async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise { + let dirPath: string | undefined; + + if (AddonModResourceHelper.isDisplayedInIframe(module)) { + dirPath = await CoreFilepool.getPackageDirPathByUrl(CoreSites.getCurrentSiteId(), module.url!); + } + + const promises: Promise[] = []; + + promises.push(super.downloadOrPrefetch(module, courseId, prefetch, dirPath)); + + if (AddonModResource.isGetResourceWSAvailable()) { + promises.push(AddonModResource.getResourceData(courseId, module.id)); + } + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + async invalidateContent(moduleId: number, courseId: number): Promise { + await AddonModResource.invalidateContent(moduleId, courseId); + } + + /** + * @inheritdoc + */ + async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise { + const promises: Promise[] = []; + + promises.push(AddonModResource.invalidateResourceData(courseId)); + promises.push(CoreCourse.invalidateModule(module.id, undefined, this.modName)); + + await Promise.all(promises); + } + + /** + * @inheritdoc + */ + async isDownloadable(module: CoreCourseAnyModuleData, courseId: number): Promise { + if (CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { + // Nextcloud files are downloadable from 3.7 onwards. + return true; + } + + // Don't allow downloading Nextcloud files in older sites. + await this.loadContents(module, courseId, false); + + return !AddonModResourceHelper.isNextcloudFile(module); + } + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return AddonModResource.isPluginEnabled(); + } + +} +export const AddonModResourcePrefetchHandler = makeSingleton(AddonModResourcePrefetchHandlerService); diff --git a/src/addons/mod/resource/services/resource-helper.ts b/src/addons/mod/resource/services/resource-helper.ts new file mode 100644 index 000000000..33c37bfe7 --- /dev/null +++ b/src/addons/mod/resource/services/resource-helper.ts @@ -0,0 +1,203 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreConstants } from '@/core/constants'; +import { Injectable } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course'; +import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreApp } from '@services/app'; +import { CoreFile } from '@services/file'; +import { CoreFileHelper } from '@services/file-helper'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreMimetypeUtils } from '@services/utils/mimetype'; +import { CoreTextUtils } from '@services/utils/text'; +import { makeSingleton } from '@singletons'; +import { AddonModResource, AddonModResourceProvider } from './resource'; + +/** + * Service that provides helper functions for resources. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourceHelperProvider { + + /** + * Get the HTML to display an embedded resource. + * + * @param module The module object. + * @param courseId The course ID. + * @return Promise resolved with the HTML. + */ + async getEmbeddedHtml(module: CoreCourseWSModule, courseId: number): Promise { + const result = await CoreCourseHelper.downloadModuleWithMainFileIfNeeded( + module, + courseId, + AddonModResourceProvider.COMPONENT, + module.id, + module.contents, + ); + + return CoreMimetypeUtils.getEmbeddedHtml(module.contents[0], result.path); + } + + /** + * Download all the files needed and returns the src of the iframe. + * + * @param module The module object. + * @return Promise resolved with the iframe src. + */ + async getIframeSrc(module: CoreCourseWSModule): Promise { + if (!module.contents.length) { + throw new CoreError('No contents available in module'); + } + + const mainFile = module.contents[0]; + let mainFilePath = mainFile.filename; + + if (mainFile.filepath !== '/') { + mainFilePath = mainFile.filepath.substr(1) + mainFilePath; + } + + try { + const dirPath = await CoreFilepool.getPackageDirUrlByUrl(CoreSites.getCurrentSiteId(), module.url!); + + // This URL is going to be injected in an iframe, we need trustAsResourceUrl to make it work in a browser. + return CoreTextUtils.concatenatePaths(dirPath, mainFilePath); + } catch (e) { + // Error getting directory, there was an error downloading or we're in browser. Return online URL. + if (CoreApp.isOnline() && mainFile.fileurl) { + // This URL is going to be injected in an iframe, we need this to make it work. + return CoreSites.getCurrentSite()!.checkAndFixPluginfileURL(mainFile.fileurl); + } + + throw e; + } + } + + /** + * Whether the resource has to be displayed embedded. + * + * @param module The module object. + * @param display The display mode (if available). + * @return Whether the resource should be displayed embeded. + */ + isDisplayedEmbedded(module: CoreCourseWSModule, display: number): boolean { + const currentSite = CoreSites.getCurrentSite(); + + if ((!module.contents.length && !module.contentsinfo) || + !CoreFile.isAvailable() || + (currentSite && !currentSite.isVersionGreaterEqualThan('3.7') && this.isNextcloudFile(module))) { + return false; + } + + const ext = module.contentsinfo + ? CoreMimetypeUtils.getExtension(module.contentsinfo.mimetypes[0]) + : CoreMimetypeUtils.getFileExtension(module.contents[0].filename); + + return (display == CoreConstants.RESOURCELIB_DISPLAY_EMBED || display == CoreConstants.RESOURCELIB_DISPLAY_AUTO) && + CoreMimetypeUtils.canBeEmbedded(ext); + } + + /** + * Whether the resource has to be displayed in an iframe. + * + * @param module The module object. + * @return Whether the resource should be displayed in an iframe. + */ + isDisplayedInIframe(module: CoreCourseAnyModuleData): boolean { + if (!CoreFile.isAvailable()) { + return false; + } + + let mimetype: string | undefined; + + if ('contentsinfo' in module && module.contentsinfo) { + mimetype = module.contentsinfo.mimetypes[0]; + } else if (module.contents) { + const ext = CoreMimetypeUtils.getFileExtension(module.contents[0].filename); + mimetype = CoreMimetypeUtils.getMimeType(ext); + } else { + return false; + } + + return mimetype == 'text/html'; + } + + /** + * Check if main file of resource is downloadable. + * + * @param module Module instance. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with boolean: whether main file is downloadable. + */ + isMainFileDownloadable(module: CoreCourseWSModule, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const mainFile = module.contents[0]; + const timemodified = CoreFileHelper.getFileTimemodified(mainFile); + + return CoreFilepool.isFileDownloadable(siteId, mainFile.fileurl, timemodified); + } + + /** + * Check if the resource is a Nextcloud file. + * + * @param module Module to check. + * @return Whether it's a Nextcloud file. + */ + isNextcloudFile(module: CoreCourseAnyModuleData): boolean { + if ('contentsinfo' in module && module.contentsinfo) { + return module.contentsinfo.repositorytype == 'nextcloud'; + } + + return !!(module.contents && module.contents[0] && module.contents[0].repositorytype == 'nextcloud'); + } + + /** + * Opens a file of the resource activity. + * + * @param module Module where to get the contents. + * @param courseId Course Id, used for completion purposes. + * @return Resolved when done. + */ + async openModuleFile(module: CoreCourseWSModule, courseId: number): Promise { + const modal = await CoreDomUtils.showModalLoading(); + + try { + // Download and open the file from the resource contents. + await CoreCourseHelper.downloadModuleAndOpenFile( + module, + courseId, + AddonModResourceProvider.COMPONENT, + module.id, + module.contents, + ); + + try { + await AddonModResource.logView(module.instance!, module.name); + CoreCourse.checkModuleCompletion(courseId, module.completiondata); + } catch { + // Ignore errors. + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_resource.errorwhileloadingthecontent', true); + } finally { + modal.dismiss(); + } + } + +} +export const AddonModResourceHelper = makeSingleton(AddonModResourceHelperProvider); diff --git a/src/addons/mod/resource/services/resource.ts b/src/addons/mod/resource/services/resource.ts new file mode 100644 index 000000000..e99552ac1 --- /dev/null +++ b/src/addons/mod/resource/services/resource.ts @@ -0,0 +1,241 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// 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 { CoreError } from '@classes/errors/error'; +import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseLogHelper } from '@features/course/services/log-helper'; +import { CoreFilepool } from '@services/filepool'; +import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +const ROOT_CACHE_KEY = 'mmaModResource:'; + +/** + * Service that provides some features for resources. + */ +@Injectable({ providedIn: 'root' }) +export class AddonModResourceProvider { + + static readonly COMPONENT = 'mmaModResource'; + + /** + * Get cache key for resource data WS calls. + * + * @param courseId Course ID. + * @return Cache key. + */ + protected getResourceCacheKey(courseId: number): string { + return ROOT_CACHE_KEY + 'resource:' + courseId; + } + + /** + * Get a resource data. + * + * @param courseId Course ID. + * @param key Name of the property to check. + * @param value Value to search. + * @param options Other options. + * @return Promise resolved when the resource is retrieved. + */ + protected async getResourceDataByKey( + courseId: number, + key: string, + value: number, + options: CoreSitesCommonWSOptions = {}, + ): Promise { + const site = await CoreSites.getSite(options.siteId); + + const params: AddonModResourceGetResourcesByCoursesWSParams = { + courseids: [courseId], + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getResourceCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + component: AddonModResourceProvider.COMPONENT, + ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), + }; + + const response = await site.read( + 'mod_resource_get_resources_by_courses', + params, + preSets, + ); + + const currentResource = response.resources.find((resource) => resource[key] == value); + if (currentResource) { + return currentResource; + } + + throw new CoreError('Resource not found'); + } + + /** + * Get a resource by course module ID. + * + * @param courseId Course ID. + * @param cmId Course module ID. + * @param options Other options. + * @return Promise resolved when the resource is retrieved. + */ + getResourceData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { + return this.getResourceDataByKey(courseId, 'coursemodule', cmId, options); + } + + /** + * Invalidate the prefetched content. + * + * @param moduleId The module ID. + * @param courseId Course ID of the module. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise { + siteId = siteId || CoreSites.getCurrentSiteId(); + + const promises: Promise[] = []; + + promises.push(this.invalidateResourceData(courseId, siteId)); + promises.push(CoreFilepool.invalidateFilesByComponent(siteId, AddonModResourceProvider.COMPONENT, moduleId)); + promises.push(CoreCourse.invalidateModule(moduleId, siteId, 'resource')); + + await CoreUtils.allPromises(promises); + } + + /** + * Invalidates resource data. + * + * @param courseid Course ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the data is invalidated. + */ + async invalidateResourceData(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getResourceCacheKey(courseId)); + } + + /** + * Returns whether or not getResource WS available or not. + * + * @return If WS is abalaible. + * @since 3.3 + */ + isGetResourceWSAvailable(): boolean { + return CoreSites.wsAvailableInCurrentSite('mod_resource_get_resources_by_courses'); + } + + /** + * Return whether or not the plugin is enabled. + * + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. + */ + async isPluginEnabled(siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + return site.canDownloadFiles(); + } + + /** + * Report the resource as being viewed. + * + * @param id Module ID. + * @param name Name of the resource. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved when the WS call is successful. + */ + async logView(id: number, name?: string, siteId?: string): Promise { + const params: AddonModResourceViewResourceWSParams = { + resourceid: id, + }; + + await CoreCourseLogHelper.logSingle( + 'mod_resource_view_resource', + params, + AddonModResourceProvider.COMPONENT, + id, + name, + 'resource', + {}, + siteId, + ); + } + +} +export const AddonModResource = makeSingleton(AddonModResourceProvider); + +/** + * Params of mod_resource_view_resource WS. + */ +type AddonModResourceViewResourceWSParams = { + resourceid: number; // Resource instance id. +}; + +/** + * Resource returned by mod_resource_get_resources_by_courses. + */ +export type AddonModResourceResource = { + id: number; // Module id. + coursemodule: number; // Course module id. + course: number; // Course id. + name: string; // Page name. + intro: string; // Summary. + introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). + introfiles: CoreWSExternalFile[]; + contentfiles: CoreWSExternalFile[]; + tobemigrated: number; // Whether this resource was migrated. + legacyfiles: number; // Legacy files flag. + legacyfileslast: number; // Legacy files last control flag. + display: number; // How to display the resource. + displayoptions: string; // Display options (width, height). + filterfiles: number; // If filters should be applied to the resource content. + revision: number; // Incremented when after each file changes, to avoid cache. + timemodified: number; // Last time the resource was modified. + section: number; // Course section id. + visible: number; // Module visibility. + groupmode: number; // Group mode. + groupingid: number; // Grouping id. +}; + +export type AddonModResourceCustomData = { + showsize?: boolean; + filedetails?: { + size: number; + modifieddate: number; + uploadeddate: number; + }; + showtype?: boolean; + showdate?: boolean; + printintro?: boolean; +}; + +/** + * Params of mod_resource_get_resources_by_courses WS. + */ +type AddonModResourceGetResourcesByCoursesWSParams = { + courseids?: number[]; // Array of course ids. +}; + +/** + * Data returned by mod_resource_get_resources_by_courses WS. + */ +type AddonModResourceGetResourcesByCoursesWSResponse = { + resources: AddonModResourceResource[]; + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/core/features/course/classes/resource-prefetch-handler.ts b/src/core/features/course/classes/resource-prefetch-handler.ts index d55fd69c8..b12025a2d 100644 --- a/src/core/features/course/classes/resource-prefetch-handler.ts +++ b/src/core/features/course/classes/resource-prefetch-handler.ts @@ -19,7 +19,7 @@ import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; import { CoreSites } from '@services/sites'; import { CoreWSExternalFile } from '@services/ws'; -import { CoreCourse, CoreCourseWSModule } from '../services/course'; +import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '../services/course'; import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler'; /** @@ -178,7 +178,7 @@ export class CoreCourseResourcePrefetchHandlerBase extends CoreCourseModulePrefe * @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down). * @return Promise resolved when loaded. */ - loadContents(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise { + loadContents(module: CoreCourseAnyModuleData, courseId: number, ignoreCache?: boolean): Promise { return CoreCourse.loadModuleContents(module, courseId, undefined, false, ignoreCache); } diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index c1dc0cf0a..c57d50745 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -1439,18 +1439,20 @@ export type CoreCourseModuleWSCompletionData = { }; export type CoreCourseModuleContentFile = { - type: string; // A file or a folder or external link. + // Common properties with CoreWSExternalFile. filename: string; // Filename. filepath: string; // Filepath. filesize: number; // Filesize. fileurl: string; // Downloadable file url. - content?: string; // Raw content, will be used when type is content. - timecreated: number; // Time created. timemodified: number; // Time modified. - sortorder: number; // Content sort order. mimetype?: string; // File mime type. isexternalfile?: number; // Whether is an external file. repositorytype?: string; // The repository type for external files. + + type: string; // A file or a folder or external link. + content?: string; // Raw content, will be used when type is content. + timecreated: number; // Time created. + sortorder: number; // Content sort order. userid: number; // User who added this content to moodle. author: string; // Content owner. license: string; // Content license. diff --git a/src/core/services/utils/mimetype.ts b/src/core/services/utils/mimetype.ts index 3985facdb..3296d5d43 100644 --- a/src/core/services/utils/mimetype.ts +++ b/src/core/services/utils/mimetype.ts @@ -65,7 +65,7 @@ export class CoreMimetypeUtilsProvider { * @param extension Extension. * @return Whether it can be embedded. */ - canBeEmbedded(extension: string): boolean { + canBeEmbedded(extension?: string): boolean { return this.isExtensionInGroup(extension, ['web_image', 'web_video', 'web_audio']); } diff --git a/src/core/services/utils/text.ts b/src/core/services/utils/text.ts index 70d30bb15..6b351523d 100644 --- a/src/core/services/utils/text.ts +++ b/src/core/services/utils/text.ts @@ -238,11 +238,7 @@ export class CoreTextUtilsProvider { * @param singleLine True if new lines should be removed (all the text in a single line). * @return Clean text. */ - cleanTags(text: string, singleLine?: boolean): string { - if (typeof text != 'string') { - return text; - } - + cleanTags(text: string | undefined, singleLine?: boolean): string { if (!text) { return ''; } diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts index 72ea027b5..49edf5b2b 100644 --- a/src/core/services/ws.ts +++ b/src/core/services/ws.ts @@ -1077,46 +1077,14 @@ export type CoreWarningsWSResponse = { * Structure of files returned by WS. */ export type CoreWSExternalFile = { - /** - * Downloadable file url. - */ - fileurl: string; - - /** - * File name. - */ - filename?: string; - - /** - * File path. - */ - filepath?: string; - - /** - * File size. - */ - filesize?: number; - - /** - * Time modified. - */ - timemodified?: number; - - /** - * File mime type. - */ - mimetype?: string; - - /** - * Whether is an external file. - */ - isexternalfile?: number; - - /** - * The repository type for external files. - */ - repositorytype?: string; - + fileurl: string; // Downloadable file url. + filename?: string; // File name. + filepath?: string; // File path. + filesize?: number; // File size. + timemodified?: number; // Time modified. + mimetype?: string; // File mime type. + isexternalfile?: number; // Whether is an external file. + repositorytype?: string; // The repository type for external files. }; /**