From 223fd19f1dbfda33ba0d439237039bc3df5ea620 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 25 Feb 2022 10:25:24 +0100 Subject: [PATCH] MOBILE-3998 imscp: Add entry page to IMSCP --- src/addons/mod/book/components/index/index.ts | 2 +- .../mod/book/pages/contents/contents.ts | 4 +- .../index/addon-mod-imscp-index.html | 37 ++- .../mod/imscp/components/index/index.ts | 113 +++----- src/addons/mod/imscp/imscp-lazy.module.ts | 4 + src/addons/mod/imscp/pages/view/view.html | 39 +++ .../mod/imscp/pages/view/view.module.ts | 38 +++ src/addons/mod/imscp/pages/view/view.ts | 273 ++++++++++++++++++ 8 files changed, 424 insertions(+), 86 deletions(-) create mode 100644 src/addons/mod/imscp/pages/view/view.html create mode 100644 src/addons/mod/imscp/pages/view/view.module.ts create mode 100644 src/addons/mod/imscp/pages/view/view.ts diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index b8d7f4bea..09263edda 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -92,7 +92,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp /** * Open the book in a certain chapter. * - * @param chapterId Chapter to open, undefined for first chapter. + * @param chapterId Chapter to open, undefined for last chapter viewed. */ openBook(chapterId?: number): void { CoreNavigator.navigate('contents', { diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index 21436d557..e32661914 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -53,7 +53,7 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; - title!: string; + title = ''; cmId!: number; courseId!: number; initialChapterId?: number; @@ -147,6 +147,8 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { } else { this.warning = ''; } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); } finally { this.loaded = true; } diff --git a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html index e4cfff579..0f43d70d2 100644 --- a/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html +++ b/src/addons/mod/imscp/components/index/addon-mod-imscp-index.html @@ -1,9 +1,5 @@ - - - - @@ -13,24 +9,35 @@ - + - + - - + +

{{ 'addon.mod_imscp.toc' | translate }}

+
-
-
- -
+ + +

+ + {{item.title}} +

+
+
+
- - - +
+ + {{ 'core.start' | translate }} + {{ 'core.resume' | translate }} + +
+
diff --git a/src/addons/mod/imscp/components/index/index.ts b/src/addons/mod/imscp/components/index/index.ts index 78df65bac..8538b3e54 100644 --- a/src/addons/mod/imscp/components/index/index.ts +++ b/src/addons/mod/imscp/components/index/index.ts @@ -13,14 +13,11 @@ // limitations under the License. import { Component, OnInit, Optional } from '@angular/core'; -import { CoreSilentError } from '@classes/errors/silenterror'; -import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; import { CoreCourse } from '@features/course/services/course'; -import { CoreDomUtils } from '@services/utils/dom'; +import { CoreNavigator } from '@services/navigator'; import { AddonModImscpProvider, AddonModImscp, AddonModImscpTocItem } from '../../services/imscp'; -import { AddonModImscpTocComponent } from '../toc/toc'; /** * Component that displays a IMSCP. @@ -33,13 +30,9 @@ import { AddonModImscpTocComponent } from '../toc/toc'; export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit { component = AddonModImscpProvider.COMPONENT; - src = ''; - warning = ''; - navigationItems: CoreNavigationBarItem[] = []; - protected items: AddonModImscpTocItem[] = []; - protected currentHref?: string; - protected displayDescription = false; + items: AddonModImscpTocItem[] = []; + hasStarted = false; constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModImscpIndexComponent', courseContentsPage); @@ -73,85 +66,67 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom /** * @inheritdoc */ - protected async fetchContent(refresh = false): Promise { - const downloadResult = await this.downloadResourceIfNeeded(refresh); - - const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); - this.description = imscp.intro; - this.dataRetrieved.emit(imscp); - - // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. - const contents = await CoreCourse.getModuleContents(this.module); - - this.items = AddonModImscp.createItemList(contents); - - if (this.items.length && this.currentHref === undefined) { - this.currentHref = this.items[0].href; - } - - try { - await this.loadItemHref(this.currentHref); - } catch (error) { - CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); - - throw new CoreSilentError(error); - } - - this.warning = downloadResult.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : ''; + protected async fetchContent(): Promise { + await Promise.all([ + this.loadImscp(), + this.loadTOC(), + ]); } /** - * Loads an item. + * Load IMSCP data. * - * @param itemHref Item Href. * @return Promise resolved when done. */ - async loadItemHref(itemHref?: string): Promise { - const src = await AddonModImscp.getIframeSrc(this.module, itemHref); - this.currentHref = itemHref; + protected async loadImscp(): Promise { + const imscp = await AddonModImscp.getImscp(this.courseId, this.module.id); - this.navigationItems = this.items.map((item) => ({ - item: item, - current: item.href == this.currentHref, - enabled: !!item.href, - })); + this.dataRetrieved.emit(imscp); - if (this.src && src == this.src) { - // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed. - this.src = ''; - setTimeout(() => { - this.src = src; - }); - } else { - this.src = src; - } + this.dataRetrieved.emit(imscp); + + this.description = imscp.intro; + + // @todo: Check if user already started the IMSCP. } /** - * Loads an item. + * Load book TOC. * - * @param item Item. + * @return Promise resolved when done. */ - loadItem(item: AddonModImscpTocItem): void { - this.loadItemHref(item.href); + protected async loadTOC(): Promise { + // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. + const contents = await CoreCourse.getModuleContents(this.module, this.courseId); + + this.items = AddonModImscp.createItemList(contents); } /** - * Show the TOC. + * Open IMSCP book with a certain item. + * + * @param href Item href to open, undefined for last item seen. */ - async showToc(): Promise { - // Create the toc modal. - const modalData = await CoreDomUtils.openSideModal({ - component: AddonModImscpTocComponent, - componentProps: { - items: this.items, - selected: this.currentHref, + openImscp(href?: string): void { + CoreNavigator.navigate('view', { + params: { + cmId: this.module.id, + courseId: this.courseId, + initialHref: href, }, }); - if (modalData) { - this.loadItemHref(modalData); - } + this.hasStarted = true; + } + + /** + * Get dummy array for padding. + * + * @param n Array length. + * @return Dummy array with n elements. + */ + getNumberForPadding(n: number): number[] { + return new Array(n); } } diff --git a/src/addons/mod/imscp/imscp-lazy.module.ts b/src/addons/mod/imscp/imscp-lazy.module.ts index 2fe070b1f..1fb94112d 100644 --- a/src/addons/mod/imscp/imscp-lazy.module.ts +++ b/src/addons/mod/imscp/imscp-lazy.module.ts @@ -24,6 +24,10 @@ const routes: Routes = [ path: ':courseId/:cmId', component: AddonModImscpIndexPage, }, + { + path: ':courseId/:cmId/view', + loadChildren: () => import('./pages/view/view.module').then(m => m.AddonModImscpViewPageModule), + }, ]; @NgModule({ diff --git a/src/addons/mod/imscp/pages/view/view.html b/src/addons/mod/imscp/pages/view/view.html new file mode 100644 index 000000000..309b6b94a --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.html @@ -0,0 +1,39 @@ + + + + + + +

+ + +

+
+ + + + + + +
+
+ + + + + + + + + + + +
+ +
+
+ + + +
diff --git a/src/addons/mod/imscp/pages/view/view.module.ts b/src/addons/mod/imscp/pages/view/view.module.ts new file mode 100644 index 000000000..a4232dac5 --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModImscpViewPage } from './view'; + +const routes: Routes = [ + { + path: '', + component: AddonModImscpViewPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + ], + declarations: [ + AddonModImscpViewPage, + ], + exports: [RouterModule], +}) +export class AddonModImscpViewPageModule {} diff --git a/src/addons/mod/imscp/pages/view/view.ts b/src/addons/mod/imscp/pages/view/view.ts new file mode 100644 index 000000000..58123db94 --- /dev/null +++ b/src/addons/mod/imscp/pages/view/view.ts @@ -0,0 +1,273 @@ +// (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 { Component, OnInit } from '@angular/core'; +import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; +import { CoreCourseResourceDownloadResult } from '@features/course/classes/main-resource-component'; +import { CoreCourse } from '@features/course/services/course'; +import { CoreCourseModuleData } from '@features/course/services/course-helper'; +import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; +import { IonRefresher } from '@ionic/angular'; +import { CoreApp } from '@services/app'; +import { CoreNavigator } from '@services/navigator'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreTextUtils } from '@services/utils/text'; +import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; +import { AddonModImscpTocComponent } from '../../components/toc/toc'; +import { AddonModImscp, AddonModImscpImscp, AddonModImscpTocItem } from '../../services/imscp'; + +/** + * Page that displays a IMSCP content. + */ +@Component({ + selector: 'page-addon-mod-imscp-view', + templateUrl: 'view.html', +}) +export class AddonModImscpViewPage implements OnInit { + + title = ''; + cmId!: number; + courseId!: number; + initialItemHref?: string; + src = ''; + warning = ''; + navigationItems: CoreNavigationBarItem[] = []; + loaded = false; + + protected module?: CoreCourseModuleData; + protected imscp?: AddonModImscpImscp; + protected items: AddonModImscpTocItem[] = []; + protected currentHref?: string; + + /** + * @inheritdoc + */ + ngOnInit(): void { + try { + this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + this.initialItemHref = CoreNavigator.getRouteParam('initialHref'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } + + this.fetchContent(); + } + + /** + * Download IMSCP contents and load the current item. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh = false): Promise { + try { + const { module, imscp } = await this.loadImscpData(); + + this.title = imscp.name; + + const downloadResult = await this.downloadResourceIfNeeded(module, refresh); + + // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. + const contents = await CoreCourse.getModuleContents(module, this.courseId); + + this.items = AddonModImscp.createItemList(contents); + + if (this.items.length) { + if (this.initialItemHref) { + // Check it's valid. + if (this.items.some(item => item.href === this.initialItemHref)) { + this.currentHref = this.initialItemHref; + } + } + + if (this.currentHref === undefined) { + // @todo: Use last item viewed. + this.currentHref = this.items[0].href; + } + } + + try { + await this.loadItemHref(this.currentHref); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'addon.mod_imscp.deploymenterror', true); + + return; + } + + if (downloadResult?.failed) { + const error = CoreTextUtils.getErrorMessageFromError(downloadResult.error) || downloadResult.error; + this.warning = Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); + } else { + this.warning = ''; + } + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); + } finally { + this.loaded = true; + } + } + + /** + * Load IMSCP data from WS. + * + * @return Promise resolved when done. + */ + async loadImscpData(): Promise<{ module: CoreCourseModuleData; imscp: AddonModImscpImscp }> { + this.module = await CoreCourse.getModule(this.cmId, this.courseId); + this.imscp = await AddonModImscp.getImscp(this.courseId, this.cmId); + + return { + module: this.module, + imscp: this.imscp, + }; + } + + /** + * Download a resource if needed. + * If the download call fails the promise won't be rejected, but the error will be included in the returned object. + * If module.contents cannot be loaded then the Promise will be rejected. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async downloadResourceIfNeeded( + module: CoreCourseModuleData, + refresh = false, + ): Promise { + + const result: CoreCourseResourceDownloadResult = { + failed: false, + }; + let contentsAlreadyLoaded = false; + + // Get module status to determine if it needs to be downloaded. + const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId, undefined, refresh); + + if (status !== CoreConstants.DOWNLOADED) { + // Download content. This function also loads module contents if needed. + try { + await CoreCourseModulePrefetchDelegate.downloadModule(module, this.courseId); + + // If we reach here it means the download process already loaded the contents, no need to do it again. + contentsAlreadyLoaded = true; + } catch (error) { + // Mark download as failed but go on since the main files could have been downloaded. + result.failed = true; + result.error = error; + } + } + + if (!module.contents?.length || (refresh && !contentsAlreadyLoaded)) { + // Try to load the contents. + const ignoreCache = refresh && CoreApp.isOnline(); + + try { + await CoreCourse.loadModuleContents(module, undefined, undefined, false, ignoreCache); + } catch (error) { + // Error loading contents. If we ignored cache, try to get the cached value. + if (ignoreCache && !module.contents) { + await CoreCourse.loadModuleContents(module); + } else if (!module.contents) { + // Not able to load contents, throw the error. + throw error; + } + } + } + + return result; + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + await CoreUtils.ignoreErrors(Promise.all([ + AddonModImscp.invalidateContent(this.cmId, this.courseId), + CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(this.courseId), // To detect if IMSCP was updated. + ])); + + await CoreUtils.ignoreErrors(this.fetchContent(true)); + + refresher?.complete(); + } + + /** + * Loads an item. + * + * @param itemHref Item Href. + * @return Promise resolved when done. + */ + async loadItemHref(itemHref?: string): Promise { + if (!this.module) { + return; + } + + const src = await AddonModImscp.getIframeSrc(this.module, itemHref); + this.currentHref = itemHref; + + this.navigationItems = this.items.map((item) => ({ + item: item, + current: item.href == this.currentHref, + enabled: !!item.href, + })); + + if (this.src && src == this.src) { + // Re-loading same page. Set it to empty and then re-set the src in the next digest so it detects it has changed. + this.src = ''; + setTimeout(() => { + this.src = src; + }); + } else { + this.src = src; + } + } + + /** + * Loads an item. + * + * @param item Item. + */ + loadItem(item: AddonModImscpTocItem): void { + this.loadItemHref(item.href); + } + + /** + * Show the TOC. + */ + async showToc(): Promise { + // Create the toc modal. + const modalData = await CoreDomUtils.openSideModal({ + component: AddonModImscpTocComponent, + componentProps: { + items: this.items, + selected: this.currentHref, + }, + }); + + if (modalData) { + this.loadItemHref(modalData); + } + } + +}