From f4cb040aa00727aa4de5f9c9fd3fb0bc0e5837d5 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 15 Feb 2022 08:40:48 +0100 Subject: [PATCH 1/4] MOBILE-3980 book: Add index page for book --- scripts/langindex.json | 1 + src/addons/mod/book/book-lazy.module.ts | 4 + .../mod/book/components/components.module.ts | 2 - .../index/addon-mod-book-index.html | 44 +- src/addons/mod/book/components/index/index.ts | 319 ++----------- src/addons/mod/book/lang.json | 3 +- .../mod/book/pages/contents/contents.html | 53 +++ .../book/pages/contents/contents.module.ts | 40 ++ .../mod/book/pages/contents/contents.ts | 428 ++++++++++++++++++ src/addons/mod/book/pages/index/index.html | 2 +- src/addons/mod/book/pages/index/index.page.ts | 17 +- 11 files changed, 592 insertions(+), 321 deletions(-) create mode 100644 src/addons/mod/book/pages/contents/contents.html create mode 100644 src/addons/mod/book/pages/contents/contents.module.ts create mode 100644 src/addons/mod/book/pages/contents/contents.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 4edb31980..5a733c08e 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -450,6 +450,7 @@ "addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn", + "addon.mod_book.book:read": "book", "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", "addon.mod_book.navnexttitle": "book", diff --git a/src/addons/mod/book/book-lazy.module.ts b/src/addons/mod/book/book-lazy.module.ts index 974dce981..0c81566fc 100644 --- a/src/addons/mod/book/book-lazy.module.ts +++ b/src/addons/mod/book/book-lazy.module.ts @@ -23,6 +23,10 @@ const routes: Routes = [ path: ':courseId/:cmId', component: AddonModBookIndexPage, }, + { + path: ':courseId/:cmId/contents', + loadChildren: () => import('./pages/contents/contents.module').then(m => m.AddonModBookContentsPageModule), + }, ]; @NgModule({ diff --git a/src/addons/mod/book/components/components.module.ts b/src/addons/mod/book/components/components.module.ts index 0254605d5..d31d09782 100644 --- a/src/addons/mod/book/components/components.module.ts +++ b/src/addons/mod/book/components/components.module.ts @@ -16,7 +16,6 @@ import { NgModule } from '@angular/core'; import { CoreSharedModule } from '@/core/shared.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; -import { CoreTagComponentsModule } from '@features/tag/components/components.module'; import { AddonModBookIndexComponent } from './index/index'; import { AddonModBookTocComponent } from './toc/toc'; @@ -29,7 +28,6 @@ import { AddonModBookTocComponent } from './toc/toc'; imports: [ CoreSharedModule, CoreCourseComponentsModule, - CoreTagComponentsModule, ], exports: [ AddonModBookIndexComponent, diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index 0789f53c5..a87c06a88 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -1,8 +1,5 @@ - - - @@ -28,31 +25,30 @@ [courseId]="courseId"> - + - - + +

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

+
-
-
- - + + +

+ {{chapter.indexNumber}}  + •  + + +

+
+
- - -
- -
- {{ 'core.tag.tags' | translate }}: - -
-
-
-
-
+ + {{ 'addon.mod_book.book:read' | translate }} + + + diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index a984af095..35f24bbd2 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -12,33 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Optional, Input, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; +import { Component, Optional, OnInit, OnDestroy } from '@angular/core'; import { CoreCourseModuleMainResourceComponent } from '@features/course/classes/main-resource-component'; -import { - AddonModBookProvider, - AddonModBookContentsMap, - AddonModBookTocChapter, - AddonModBookNavStyle, - AddonModBook, - AddonModBookBookWSData, -} from '../../services/book'; -import { CoreTag, CoreTagItem } from '@features/tag/services/tag'; -import { CoreDomUtils } from '@services/utils/dom'; +import { AddonModBook, AddonModBookBookWSData, AddonModBookNumbering, AddonModBookTocChapter } from '../../services/book'; import { CoreCourseContentsPage } from '@features/course/pages/contents/contents'; -import { CoreUtils } from '@services/utils/utils'; import { CoreCourse } from '@features/course/services/course'; -import { AddonModBookTocComponent } from '../toc/toc'; -import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; -import { CoreError } from '@classes/errors/error'; -import { Translate } from '@singletons'; -import { CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides'; -import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/swipe-slides-items-manager-source'; -import { CoreCourseModule } from '@features/course/services/course-helper'; -import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager'; -import { CoreTextUtils } from '@services/utils/text'; +import { CoreNavigator } from '@services/navigator'; /** - * Component that displays a book. + * Component that displays a book entry page. */ @Component({ selector: 'addon-mod-book-index', @@ -46,191 +28,63 @@ import { CoreTextUtils } from '@services/utils/text'; }) export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy { - @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; + showNumbers = true; + addPadding = true; + showBullets = false; + chapters: AddonModBookTocChapter[] = []; - @Input() initialChapterId?: number; // The initial chapter ID to load. + protected book?: AddonModBookBookWSData; - component = AddonModBookProvider.COMPONENT; - manager?: CoreSwipeSlidesItemsManager; - warning = ''; - displayNavBar = true; - navigationItems: CoreNavigationBarItem[] = []; - displayTitlesInNavBar = false; - slidesOpts: CoreSwipeSlidesOptions = { - autoHeight: true, - scrollOnChange: 'top', - }; - - protected firstLoad = true; - protected element: HTMLElement; - protected managerUnsubscribe?: () => void; - - constructor( - elementRef: ElementRef, - @Optional() courseContentsPage?: CoreCourseContentsPage, - ) { + constructor( @Optional() courseContentsPage?: CoreCourseContentsPage) { super('AddonModBookIndexComponent', courseContentsPage); - - this.element = elementRef.nativeElement; } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { super.ngOnInit(); - const source = new AddonModBookSlidesItemsManagerSource( - this.courseId, - this.module, - CoreTag.areTagsAvailableInSite(), - this.initialChapterId, - ); - this.manager = new CoreSwipeSlidesItemsManager(source); - this.managerUnsubscribe = this.manager.addListener({ - onSelectedItemUpdated: (item) => { - this.onChapterViewed(item.id); - }, - }); - this.loadContent(); } - get book(): AddonModBookBookWSData | undefined { - return this.manager?.getSource().book; - } + /** + * @inheritdoc + */ + protected async fetchContent(refresh?: boolean): Promise { + try { + this.book = await AddonModBook.getBook(this.courseId, this.module.id); - get chapters(): AddonModBookTocChapter[] { - return this.manager?.getSource().chapters || []; + if (this.book) { + this.dataRetrieved.emit(this.book); + + this.description = this.book.intro; + this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS; + this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS; + this.addPadding = this.book.numbering != AddonModBookNumbering.NONE; + } + + const contents = await CoreCourse.getModuleContents(this.module, this.courseId); + + this.chapters = AddonModBook.getTocList(contents); + } finally { + this.fillContextMenu(refresh); + } } /** - * Show the TOC. + * Open the book in a certain chapter. + * + * @param chapterId Chapter to open, undefined for first chapter. */ - async showToc(): Promise { - // Create the toc modal. - const visibleChapter = this.manager?.getSelectedItem(); - - const modalData = await CoreDomUtils.openSideModal({ - component: AddonModBookTocComponent, - componentProps: { - moduleId: this.module.id, - chapters: this.chapters, - selected: visibleChapter, + openBook(chapterId?: number): void { + CoreNavigator.navigate('contents', { + params: { + cmId: this.module.id, courseId: this.courseId, - book: this.book, + chapterId, }, }); - - if (modalData) { - this.changeChapter(modalData); - } - } - - /** - * Change the current chapter. - * - * @param chapterId Chapter to load. - * @return Promise resolved when done. - */ - changeChapter(chapterId: number): void { - if (!chapterId) { - return; - } - - this.slides?.slideToItem({ id: chapterId }); - } - - /** - * Perform the invalidate content function. - * - * @return Resolved when done. - */ - protected async invalidateContent(): Promise { - await this.manager?.getSource().invalidateContent(); - } - - /** - * Download book contents and load the current chapter. - * - * @param refresh Whether we're refreshing data. - * @return Promise resolved when done. - */ - protected async fetchContent(refresh = false): Promise { - try { - const source = this.manager?.getSource(); - if (!source) { - return; - } - - const downloadResult = await this.downloadResourceIfNeeded(refresh); - - const book = await source.loadBookData(); - - if (book) { - this.dataRetrieved.emit(book); - - this.description = book.intro; - this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; - this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT; - } - - // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. - await source.loadContents(); - - await source.load(); - - this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error || '') : ''; - } finally { - // Pass false because downloadResourceIfNeeded already invalidates and refresh data if refresh=true. - this.fillContextMenu(false); - } - } - - /** - * Update data related to chapter being viewed. - * - * @param chapterId Chapter viewed. - * @return Promise resolved when done. - */ - protected async onChapterViewed(chapterId: number): Promise { - // Don't log the chapter ID when the user has just opened the book. - const logChapterId = this.firstLoad; - this.firstLoad = false; - - if (this.displayNavBar) { - this.navigationItems = this.getNavigationItems(chapterId); - } - - // Chapter loaded, log view. - await CoreUtils.ignoreErrors(AddonModBook.logView( - this.module.instance, - logChapterId ? chapterId : undefined, - this.module.name, - )); - - const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); - const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; - - // Module is completed when last chapter is viewed, so we only check completion if the last is reached. - if (isLastChapter) { - CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); - } - } - - /** - * Converts chapters to navigation items. - * - * @param chapterId Current chapter Id. - * @return Navigation items. - */ - protected getNavigationItems(chapterId: number): CoreNavigationBarItem[] { - return this.chapters.map((chapter) => ({ - item: chapter, - title: chapter.title, - current: chapter.id == chapterId, - enabled: true, - })); } /** @@ -238,99 +92,6 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp */ ngOnDestroy(): void { super.ngOnDestroy(); - - this.managerUnsubscribe && this.managerUnsubscribe(); - } - -} - -type LoadedChapter = { - id: number; - content?: string; - tags?: CoreTagItem[]; -}; - -/** - * Helper to manage swiping within a collection of chapters. - */ -class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSource { - - readonly COURSE_ID: number; - readonly MODULE: CoreCourseModule; - readonly TAGS_ENABLED: boolean; - - book?: AddonModBookBookWSData; - chapters: AddonModBookTocChapter[] = []; - contentsMap: AddonModBookContentsMap = {}; - - constructor(courseId: number, module: CoreCourseModule, tagsEnabled: boolean, initialChapterId?: number) { - super(initialChapterId ? { id: initialChapterId } : undefined); - - this.COURSE_ID = courseId; - this.MODULE = module; - this.TAGS_ENABLED = tagsEnabled; - } - - /** - * @inheritdoc - */ - getItemId(item: LoadedChapter): string | number { - return item.id; - } - - /** - * Load book data from WS. - * - * @return Promise resolved when done. - */ - async loadBookData(): Promise { - this.book = await AddonModBook.getBook(this.COURSE_ID, this.MODULE.id); - - return this.book; - } - - /** - * Load module contents. - */ - async loadContents(): Promise { - const contents = await CoreCourse.getModuleContents(this.MODULE, this.COURSE_ID); - - this.contentsMap = AddonModBook.getContentsMap(contents); - this.chapters = AddonModBook.getTocList(contents); - } - - /** - * @inheritdoc - */ - protected async loadItems(): Promise { - try { - const newChapters = await Promise.all(this.chapters.map(async (chapter) => { - const content = await AddonModBook.getChapterContent(this.contentsMap, chapter.id, this.MODULE.id); - - return { - id: chapter.id, - content, - tags: this.TAGS_ENABLED ? this.contentsMap[chapter.id].tags : [], - }; - })); - - return newChapters; - } catch (error) { - if (!CoreTextUtils.getErrorMessageFromError(error)) { - throw new CoreError(Translate.instant('addon.mod_book.errorchapter')); - } - - throw error; - } - } - - /** - * Perform the invalidate content function. - * - * @return Resolved when done. - */ - invalidateContent(): Promise { - return AddonModBook.invalidateContent(this.MODULE.id, this.COURSE_ID); } } diff --git a/src/addons/mod/book/lang.json b/src/addons/mod/book/lang.json index 200e96ce1..8b6812eb9 100644 --- a/src/addons/mod/book/lang.json +++ b/src/addons/mod/book/lang.json @@ -1,8 +1,9 @@ { + "book:read": "View book", "errorchapter": "Error reading chapter of book.", "modulenameplural": "Books", "navnexttitle": "Next: {{$a}}", "navprevtitle": "Previous: {{$a}}", "tagarea_book_chapters": "Book chapters", "toc": "Table of contents" -} \ No newline at end of file +} diff --git a/src/addons/mod/book/pages/contents/contents.html b/src/addons/mod/book/pages/contents/contents.html new file mode 100644 index 000000000..f5a6d6f4a --- /dev/null +++ b/src/addons/mod/book/pages/contents/contents.html @@ -0,0 +1,53 @@ + + + + + + +

+ + +

+
+ + + + + +
+
+ + + + + + + + + + + + + + +
+ + + + + +
+ +
+ {{ 'core.tag.tags' | translate }}: + +
+
+
+
+
+
+
diff --git a/src/addons/mod/book/pages/contents/contents.module.ts b/src/addons/mod/book/pages/contents/contents.module.ts new file mode 100644 index 000000000..2c2bea53c --- /dev/null +++ b/src/addons/mod/book/pages/contents/contents.module.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 { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { CoreSharedModule } from '@/core/shared.module'; +import { AddonModBookContentsPage } from './contents'; +import { CoreTagComponentsModule } from '@features/tag/components/components.module'; + +const routes: Routes = [ + { + path: '', + component: AddonModBookContentsPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CoreSharedModule, + CoreTagComponentsModule, + ], + declarations: [ + AddonModBookContentsPage, + ], + exports: [RouterModule], +}) +export class AddonModBookContentsPageModule {} diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts new file mode 100644 index 000000000..de3de0c6f --- /dev/null +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -0,0 +1,428 @@ +// (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, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { CoreError } from '@classes/errors/error'; +import { CoreSwipeSlidesItemsManager } from '@classes/items-management/swipe-slides-items-manager'; +import { CoreSwipeSlidesItemsManagerSource } from '@classes/items-management/swipe-slides-items-manager-source'; +import { CoreNavigationBarItem } from '@components/navigation-bar/navigation-bar'; +import { CoreSwipeSlidesComponent, CoreSwipeSlidesOptions } from '@components/swipe-slides/swipe-slides'; +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 { CoreTag, CoreTagItem } from '@features/tag/services/tag'; +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 { AddonModBookTocComponent } from '../../components/toc/toc'; +import { + AddonModBook, + AddonModBookBookWSData, + AddonModBookContentsMap, + AddonModBookNavStyle, + AddonModBookProvider, + AddonModBookTocChapter, +} from '../../services/book'; + +/** + * Page that displays a book contents. + */ +@Component({ + selector: 'page-addon-mod-book-contents', + templateUrl: 'contents.html', +}) +export class AddonModBookContentsPage implements OnInit, OnDestroy { + + @ViewChild(CoreSwipeSlidesComponent) slides?: CoreSwipeSlidesComponent; + + title!: string; + cmId!: number; + courseId!: number; + initialChapterId?: number; + component = AddonModBookProvider.COMPONENT; + manager?: CoreSwipeSlidesItemsManager; + warning = ''; + displayNavBar = true; + navigationItems: CoreNavigationBarItem[] = []; + displayTitlesInNavBar = false; + slidesOpts: CoreSwipeSlidesOptions = { + autoHeight: true, + scrollOnChange: 'top', + }; + + loaded = false; + + protected firstLoad = true; + protected managerUnsubscribe?: () => void; + + /** + * @inheritdoc + */ + ngOnInit(): void { + try { + this.cmId = CoreNavigator.getRequiredRouteNumberParam('cmId'); + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + this.initialChapterId = CoreNavigator.getRouteNumberParam('chapterId'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + + CoreNavigator.back(); + + return; + } + + const source = new AddonModBookSlidesItemsManagerSource( + this.courseId, + this.cmId, + CoreTag.areTagsAvailableInSite(), + this.initialChapterId, + ); + this.manager = new CoreSwipeSlidesItemsManager(source); + this.managerUnsubscribe = this.manager.addListener({ + onSelectedItemUpdated: (item) => { + this.onChapterViewed(item.id); + }, + }); + + this.fetchContent(); + } + + get module(): CoreCourseModuleData | undefined { + return this.manager?.getSource().module; + } + + get book(): AddonModBookBookWSData | undefined { + return this.manager?.getSource().book; + } + + get chapters(): AddonModBookTocChapter[] { + return this.manager?.getSource().chapters || []; + } + + /** + * Download book contents and load the current chapter. + * + * @param refresh Whether we're refreshing data. + * @return Promise resolved when done. + */ + protected async fetchContent(refresh = false): Promise { + try { + const source = this.manager?.getSource(); + if (!source) { + return; + } + + const { module, book } = await source.loadBookData(); + + const downloadResult = await this.downloadResourceIfNeeded(module, refresh); + + if (book) { + this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; + this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT; + this.title = book.name; + } else { + this.title = this.title || Translate.instant('addon.mod_book.book:read'); + } + + // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. + await source.loadContents(); + + await source.load(); + + if (downloadResult?.failed) { + const error = CoreTextUtils.getErrorMessageFromError(downloadResult.error) || downloadResult.error; + this.warning = Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); + } else { + this.warning = ''; + } + } finally { + this.loaded = true; + } + } + + /** + * 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; + } + + /** + * Change the current chapter. + * + * @param chapterId Chapter to load. + * @return Promise resolved when done. + */ + changeChapter(chapterId: number): void { + if (!chapterId) { + return; + } + + this.slides?.slideToItem({ id: chapterId }); + } + + /** + * Refresh the data. + * + * @param refresher Refresher. + * @return Promise resolved when done. + */ + async doRefresh(refresher?: IonRefresher): Promise { + if (this.manager) { + await CoreUtils.ignoreErrors(Promise.all([ + this.manager.getSource().invalidateContent(), + CoreCourseModulePrefetchDelegate.invalidateCourseUpdates(this.courseId), // To detect if book was updated. + ])); + } + + await CoreUtils.ignoreErrors(this.fetchContent(true)); + + refresher?.complete(); + } + + /** + * Show the TOC. + */ + async showToc(): Promise { + // Create the toc modal. + const visibleChapter = this.manager?.getSelectedItem(); + + const modalData = await CoreDomUtils.openSideModal({ + component: AddonModBookTocComponent, + componentProps: { + moduleId: this.cmId, + chapters: this.chapters, + selected: visibleChapter, + courseId: this.courseId, + book: this.book, + }, + }); + + if (modalData) { + this.changeChapter(modalData); + } + } + + /** + * Update data related to chapter being viewed. + * + * @param chapterId Chapter viewed. + * @return Promise resolved when done. + */ + protected async onChapterViewed(chapterId: number): Promise { + // Don't log the chapter ID when the user has just opened the book. + const logChapterId = this.firstLoad; + this.firstLoad = false; + + if (this.displayNavBar) { + this.navigationItems = this.getNavigationItems(chapterId); + } + + if (!this.module) { + return; + } + + // Chapter loaded, log view. + await CoreUtils.ignoreErrors(AddonModBook.logView( + this.module.instance, + logChapterId ? chapterId : undefined, + this.module.name, + )); + + const currentChapterIndex = this.chapters.findIndex((chapter) => chapter.id == chapterId); + const isLastChapter = currentChapterIndex < 0 || this.chapters[currentChapterIndex + 1] === undefined; + + // Module is completed when last chapter is viewed, so we only check completion if the last is reached. + if (isLastChapter) { + CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata); + } + } + + /** + * Converts chapters to navigation items. + * + * @param chapterId Current chapter Id. + * @return Navigation items. + */ + protected getNavigationItems(chapterId: number): CoreNavigationBarItem[] { + return this.chapters.map((chapter) => ({ + item: chapter, + title: chapter.title, + current: chapter.id == chapterId, + enabled: true, + })); + } + + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.managerUnsubscribe && this.managerUnsubscribe(); + } + +} + +type LoadedChapter = { + id: number; + content?: string; + tags?: CoreTagItem[]; +}; + +/** + * Helper to manage swiping within a collection of chapters. + */ +class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSource { + + readonly COURSE_ID: number; + readonly CM_ID: number; + readonly TAGS_ENABLED: boolean; + + module?: CoreCourseModuleData; + book?: AddonModBookBookWSData; + chapters: AddonModBookTocChapter[] = []; + contentsMap: AddonModBookContentsMap = {}; + + constructor(courseId: number, cmId: number, tagsEnabled: boolean, initialChapterId?: number) { + super(initialChapterId ? { id: initialChapterId } : undefined); + + this.COURSE_ID = courseId; + this.CM_ID = cmId; + this.TAGS_ENABLED = tagsEnabled; + } + + /** + * @inheritdoc + */ + getItemId(item: LoadedChapter): string | number { + return item.id; + } + + /** + * Load book data from WS. + * + * @return Promise resolved when done. + */ + async loadBookData(): Promise<{ module: CoreCourseModuleData; book: AddonModBookBookWSData }> { + this.module = await CoreCourse.getModule(this.CM_ID, this.COURSE_ID); + this.book = await AddonModBook.getBook(this.COURSE_ID, this.CM_ID); + + return { + module: this.module, + book: this.book, + }; + } + + /** + * Load module contents. + */ + async loadContents(): Promise { + if (!this.module) { + return; + } + + const contents = await CoreCourse.getModuleContents(this.module, this.COURSE_ID); + + this.contentsMap = AddonModBook.getContentsMap(contents); + this.chapters = AddonModBook.getTocList(contents); + } + + /** + * @inheritdoc + */ + protected async loadItems(): Promise { + try { + const newChapters = await Promise.all(this.chapters.map(async (chapter) => { + const content = await AddonModBook.getChapterContent(this.contentsMap, chapter.id, this.CM_ID); + + return { + id: chapter.id, + content, + tags: this.TAGS_ENABLED ? this.contentsMap[chapter.id].tags : [], + }; + })); + + return newChapters; + } catch (error) { + if (!CoreTextUtils.getErrorMessageFromError(error)) { + throw new CoreError(Translate.instant('addon.mod_book.errorchapter')); + } + + throw error; + } + } + + /** + * Perform the invalidate content function. + * + * @return Resolved when done. + */ + invalidateContent(): Promise { + return AddonModBook.invalidateContent(this.CM_ID, this.COURSE_ID); + } + +} diff --git a/src/addons/mod/book/pages/index/index.html b/src/addons/mod/book/pages/index/index.html index 2c06606d2..b09bfab96 100644 --- a/src/addons/mod/book/pages/index/index.html +++ b/src/addons/mod/book/pages/index/index.html @@ -19,6 +19,6 @@ - + diff --git a/src/addons/mod/book/pages/index/index.page.ts b/src/addons/mod/book/pages/index/index.page.ts index e361ca8ba..81fc1fa66 100644 --- a/src/addons/mod/book/pages/index/index.page.ts +++ b/src/addons/mod/book/pages/index/index.page.ts @@ -12,30 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page'; -import { CoreNavigator } from '@services/navigator'; import { AddonModBookIndexComponent } from '../../components/index/index'; /** - * Page that displays a book. + * Page that displays a book entry page. */ @Component({ selector: 'page-addon-mod-book-index', templateUrl: 'index.html', }) -export class AddonModBookIndexPage extends CoreCourseModuleMainActivityPage implements OnInit { +export class AddonModBookIndexPage extends CoreCourseModuleMainActivityPage { @ViewChild(AddonModBookIndexComponent) activityComponent?: AddonModBookIndexComponent; - chapterId?: number; - - /** - * Component being initialized. - */ - ngOnInit(): void { - super.ngOnInit(); - this.chapterId = CoreNavigator.getRouteNumberParam('chapterId'); - } - } From 52e10ea15dad9239d6664fefa1278f99c489861c Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 15 Feb 2022 12:45:54 +0100 Subject: [PATCH 2/4] MOBILE-3980 book: Open book contents when clicking link --- .../assign/services/handlers/push-click.ts | 5 +- .../mod/book/services/handlers/index-link.ts | 29 ++++-- .../feedback/services/handlers/push-click.ts | 5 +- .../lesson/services/handlers/grade-link.ts | 6 +- .../lesson/services/handlers/index-link.ts | 16 +++- .../mod/quiz/services/handlers/push-click.ts | 5 +- .../classes/module-grade-handler.ts | 9 +- .../classes/module-index-handler.ts | 51 +++++++--- src/core/features/course/pages/index/index.ts | 18 +++- .../features/course/services/course-helper.ts | 96 +++++++++++-------- .../features/sitehome/pages/index/index.ts | 13 ++- upgrade.txt | 1 + 12 files changed, 178 insertions(+), 76 deletions(-) diff --git a/src/addons/mod/assign/services/handlers/push-click.ts b/src/addons/mod/assign/services/handlers/push-click.ts index 1973435e2..1b5714c8c 100644 --- a/src/addons/mod/assign/services/handlers/push-click.ts +++ b/src/addons/mod/assign/services/handlers/push-click.ts @@ -54,7 +54,10 @@ export class AddonModAssignPushClickHandlerService implements CorePushNotificati const moduleId = Number(contextUrlParams.id); await CoreUtils.ignoreErrors(AddonModAssign.invalidateContent(moduleId, courseId, notification.site)); - await CoreCourseHelper.navigateToModule(moduleId, notification.site, courseId); + await CoreCourseHelper.navigateToModule(moduleId, { + courseId, + siteId: notification.site, + }); } } diff --git a/src/addons/mod/book/services/handlers/index-link.ts b/src/addons/mod/book/services/handlers/index-link.ts index c0b5389ce..83015b7ef 100644 --- a/src/addons/mod/book/services/handlers/index-link.ts +++ b/src/addons/mod/book/services/handlers/index-link.ts @@ -15,6 +15,7 @@ import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; +import { CoreNavigationOptions } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { AddonModBook } from '../book'; @@ -31,20 +32,32 @@ export class AddonModBookIndexLinkHandlerService extends CoreContentLinksModuleI } /** - * Get the mod params necessary to open an activity. - * - * @param url The URL to treat. - * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} - * @return List of params to pass to navigateToModule / navigateToModuleByInstance. + * @inheritdoc + */ + getModNavOptions(url: string, params: Record): CoreNavigationOptions { + const chapterId = params.chapterid ? parseInt(params.chapterid, 10) : undefined; + + return { + nextNavigation: { + path: 'contents', + options: { + params: { + chapterId, + }, + }, + }, + }; + } + + /** + * @inheritdoc */ getPageParams(url: string, params: Record): Params { return params.chapterid ? { chapterId: parseInt(params.chapterid, 10) } : {}; } /** - * Check if the handler is enabled for a certain site (site + user) and a URL. - * - * @return Whether the handler is enabled for the URL and site. + * @inheritdoc */ isEnabled(siteId: string): Promise { return AddonModBook.isPluginEnabled(siteId); diff --git a/src/addons/mod/feedback/services/handlers/push-click.ts b/src/addons/mod/feedback/services/handlers/push-click.ts index e736768db..02ad4aedb 100644 --- a/src/addons/mod/feedback/services/handlers/push-click.ts +++ b/src/addons/mod/feedback/services/handlers/push-click.ts @@ -55,7 +55,10 @@ export class AddonModFeedbackPushClickHandlerService implements CorePushNotifica if (notification.name == 'submission') { return AddonModFeedbackHelper.handleShowEntriesLink(contextUrlParams, notification.site); } else { - return CoreCourseHelper.navigateToModule(moduleId, notification.site, courseId); + return CoreCourseHelper.navigateToModule(moduleId, { + courseId, + siteId: notification.site, + }); } } diff --git a/src/addons/mod/lesson/services/handlers/grade-link.ts b/src/addons/mod/lesson/services/handlers/grade-link.ts index bc16a1ef5..c1da1506d 100644 --- a/src/addons/mod/lesson/services/handlers/grade-link.ts +++ b/src/addons/mod/lesson/services/handlers/grade-link.ts @@ -76,7 +76,11 @@ export class AddonModLessonGradeLinkHandlerService extends CoreContentLinksModul ); } else { // User cannot view the report, go to lesson index. - CoreCourseHelper.navigateToModule(moduleId, siteId, module.course, module.section); + CoreCourseHelper.navigateToModule(moduleId, { + courseId: module.course, + sectionId: module.section, + siteId, + }); } } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); diff --git a/src/addons/mod/lesson/services/handlers/index-link.ts b/src/addons/mod/lesson/services/handlers/index-link.ts index 21be65658..5c0f88f6a 100644 --- a/src/addons/mod/lesson/services/handlers/index-link.ts +++ b/src/addons/mod/lesson/services/handlers/index-link.ts @@ -61,7 +61,10 @@ export class AddonModLessonIndexLinkHandlerService extends CoreContentLinksModul if (params.userpassword) { this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId!, params.userpassword, siteId); } else { - CoreCourseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId); + CoreCourseHelper.navigateToModule(parseInt(params.id, 10), { + courseId, + siteId, + }); } }, }]; @@ -94,10 +97,17 @@ export class AddonModLessonIndexLinkHandlerService extends CoreContentLinksModul // Store the password so it's automatically used. await CoreUtils.ignoreErrors(AddonModLesson.storePassword(module.instance, password, siteId)); - await CoreCourseHelper.navigateToModule(moduleId, siteId, module.course, module.section); + await CoreCourseHelper.navigateToModule(moduleId, { + courseId: module.course, + sectionId: module.section, + siteId, + }); } catch { // Error, go to index page. - await CoreCourseHelper.navigateToModule(moduleId, siteId, courseId); + await CoreCourseHelper.navigateToModule(moduleId, { + courseId, + siteId, + }); } finally { modal.dismiss(); } diff --git a/src/addons/mod/quiz/services/handlers/push-click.ts b/src/addons/mod/quiz/services/handlers/push-click.ts index 7144d7a03..05492395a 100644 --- a/src/addons/mod/quiz/services/handlers/push-click.ts +++ b/src/addons/mod/quiz/services/handlers/push-click.ts @@ -72,7 +72,10 @@ export class AddonModQuizPushClickHandlerService implements CorePushNotification await CoreUtils.ignoreErrors(AddonModQuiz.invalidateContent(moduleId, courseId, notification.site)); - return CoreCourseHelper.navigateToModule(moduleId, notification.site, courseId); + return CoreCourseHelper.navigateToModule(moduleId, { + courseId, + siteId: notification.site, + }); } } diff --git a/src/core/features/contentlinks/classes/module-grade-handler.ts b/src/core/features/contentlinks/classes/module-grade-handler.ts index 1b3b8f29b..cce3bad41 100644 --- a/src/core/features/contentlinks/classes/module-grade-handler.ts +++ b/src/core/features/contentlinks/classes/module-grade-handler.ts @@ -78,10 +78,11 @@ export class CoreContentLinksModuleGradeHandler extends CoreContentLinksHandlerB // No user specified or current user. Navigate to module. CoreCourseHelper.navigateToModule( Number(params.id), - siteId, - courseId, - undefined, - this.useModNameToGetModule ? this.modName : undefined, + { + courseId, + modName: this.useModNameToGetModule ? this.modName : undefined, + siteId, + }, ); } else if (this.canReview) { // Use the goToReview function. diff --git a/src/core/features/contentlinks/classes/module-index-handler.ts b/src/core/features/contentlinks/classes/module-index-handler.ts index 5efc4c83a..384d37155 100644 --- a/src/core/features/contentlinks/classes/module-index-handler.ts +++ b/src/core/features/contentlinks/classes/module-index-handler.ts @@ -16,6 +16,7 @@ import { CoreContentLinksHandlerBase } from './base-handler'; import { Params } from '@angular/router'; import { CoreContentLinksAction } from '../services/contentlinks-delegate'; import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreNavigationOptions } from '@services/navigator'; /** * Handler to handle URLs pointing to the index of a module. @@ -58,12 +59,27 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} * @param courseId Course ID related to the URL. Optional but recommended. * @return List of params to pass to navigateToModule / navigateToModuleByInstance. + * @deprecated since 4.0 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars getPageParams(url: string, params: Record, courseId?: number): Params { return {}; } + /** + * Get the navigation options to open the module. + * + * @param url The URL to treat. + * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1} + * @param siteId The site ID. + * @param courseId Course ID related to the URL. Optional but recommended. + * @return Navigation options to open the module. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getModNavOptions(url: string, params: Record, siteId: string, courseId?: number): CoreNavigationOptions { + return {}; + } + /** * Get the list of actions for a link (url). * @@ -81,7 +97,18 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB ): CoreContentLinksAction[] | Promise { courseId = Number(courseId || params.courseid || params.cid); - const pageParams = this.getPageParams(url, params, courseId); + const getModNavOptions = (siteId: string): CoreNavigationOptions => { + let modNavOptions = this.getModNavOptions(url, params, siteId, courseId); + if (!modNavOptions) { + // Use the old function, currently deprecated. + const pageParams = this.getPageParams(url, params, courseId); + if (pageParams && Object.keys(pageParams).length > 0) { + modNavOptions = { params: pageParams }; + } + } + + return modNavOptions; + }; if (this.instanceIdParam && params[this.instanceIdParam] !== undefined) { const instanceId = parseInt(params[this.instanceIdParam], 10); @@ -91,11 +118,12 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB CoreCourseHelper.navigateToModuleByInstance( instanceId, this.modName, - siteId, - courseId, - undefined, - this.useModNameToGetModule, - pageParams, + { + courseId, + useModNameToGetModule: this.useModNameToGetModule, + modNavOptions: getModNavOptions(siteId), + siteId, + }, ); }, }]; @@ -105,11 +133,12 @@ export class CoreContentLinksModuleIndexHandler extends CoreContentLinksHandlerB action: (siteId) => { CoreCourseHelper.navigateToModule( parseInt(params.id, 10), - siteId, - courseId, - undefined, - this.useModNameToGetModule ? this.modName : undefined, - pageParams, + { + courseId, + modName: this.useModNameToGetModule ? this.modName : undefined, + modNavOptions: getModNavOptions(siteId), + siteId, + }, ); }, }]; diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index 449058e33..9991c008d 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -24,7 +24,7 @@ import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseWSSection } fro import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; -import { CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header'; @@ -57,7 +57,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { protected sections: CoreCourseWSSection[] = []; // List of course sections. protected firstTabName?: string; protected module?: CoreCourseModuleData; - protected modParams?: Params; + protected modNavOptions?: CoreNavigationOptions; protected isGuest = false; protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = { page: CONTENTS_PAGE_NAME, @@ -136,8 +136,15 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); this.module = CoreNavigator.getRouteParam('module'); - this.modParams = CoreNavigator.getRouteParam('modParams'); this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); + this.modNavOptions = CoreNavigator.getRouteParam('modNavOptions'); + if (!this.modNavOptions) { + // Fallback to old way of passing params. @deprecated since 4.0. + const modParams = CoreNavigator.getRouteParam('modParams'); + if (modParams) { + this.modNavOptions = { params: modParams }; + } + } this.currentPagePath = CoreNavigator.getCurrentPath(); this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page); @@ -171,7 +178,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { return; } // Now that the first tab has been selected we can load the module. - CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams); + CoreCourseHelper.openModule(this.module, this.course.id, { + sectionId: this.contentsTab.pageParams.sectionId, + modNavOptions: this.modNavOptions, + }); delete this.module; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index d0e16d0c2..93c8b0ca6 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -1573,36 +1573,27 @@ export class CoreCourseHelperProvider { * * @param instanceId Activity instance ID. * @param modName Module name of the activity. - * @param siteId Site ID. If not defined, current site. - * @param courseId Course ID. If not defined we'll try to retrieve it from the site. - * @param sectionId Section the module belongs to. If not defined we'll try to retrieve it from the site. - * @param useModNameToGetModule If true, the app will retrieve all modules of this type with a single WS call. This reduces the - * number of WS calls, but it isn't recommended for modules that can return a lot of contents. - * @param modParams Params to pass to the module + * @param options Other options. * @return Promise resolved when done. */ async navigateToModuleByInstance( instanceId: number, modName: string, - siteId?: string, - courseId?: number, - sectionId?: number, - useModNameToGetModule: boolean = false, - modParams?: Params, + options: CoreCourseNavigateToModuleByInstanceOptions = {}, ): Promise { const modal = await CoreDomUtils.showModalLoading(); try { - const module = await CoreCourse.getModuleBasicInfoByInstance(instanceId, modName, { siteId }); + const module = await CoreCourse.getModuleBasicInfoByInstance(instanceId, modName, { siteId: options.siteId }); this.navigateToModule( module.id, - siteId, - module.course, - sectionId, - useModNameToGetModule ? modName : undefined, - modParams, + { + ...options, + courseId: module.course, + modName: options.useModNameToGetModule ? modName : undefined, + }, ); } catch (error) { CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true); @@ -1616,23 +1607,16 @@ export class CoreCourseHelperProvider { * Navigate to a module. * * @param moduleId Module's ID. - * @param siteId Site ID. If not defined, current site. - * @param courseId Course ID. If not defined we'll try to retrieve it from the site. - * @param sectionId Section the module belongs to. If not defined we'll try to retrieve it from the site. - * @param modName If set, the app will retrieve all modules of this type with a single WS call. This reduces the - * number of WS calls, but it isn't recommended for modules that can return a lot of contents. - * @param modParams Params to pass to the module + * @param options Other options. * @return Promise resolved when done. */ async navigateToModule( moduleId: number, - siteId?: string, - courseId?: number, - sectionId?: number, - modName?: string, - modParams?: Params, + options: CoreCourseNavigateToModuleOptions = {}, ): Promise { - siteId = siteId || CoreSites.getCurrentSiteId(); + const siteId = options.siteId || CoreSites.getCurrentSiteId(); + let courseId = options.courseId; + let sectionId = options.sectionId; const modal = await CoreDomUtils.showModalLoading(); @@ -1651,10 +1635,9 @@ export class CoreCourseHelperProvider { const site = await CoreSites.getSite(siteId); // Get the module. - const module = - await CoreCourse.getModule(moduleId, courseId, sectionId, false, false, siteId, modName); + const module = await CoreCourse.getModule(moduleId, courseId, sectionId, false, false, siteId, options.modName); - if (CoreSites.getCurrentSiteId() == site.getId()) { + if (CoreSites.getCurrentSiteId() === site.getId()) { // Try to use the module's handler to navigate cleanly. module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor( module.modname, @@ -1667,7 +1650,7 @@ export class CoreCourseHelperProvider { if (module.handlerData?.action) { modal.dismiss(); - return module.handlerData.action(new Event('click'), module, courseId, { params: modParams }); + return module.handlerData.action(new Event('click'), module, courseId, options.modNavOptions); } } @@ -1675,7 +1658,7 @@ export class CoreCourseHelperProvider { course: { id: courseId }, module, sectionId, - modParams, + modNavOptions: options.modNavOptions, }; if (courseId == site.getSiteHomeId()) { @@ -1704,23 +1687,25 @@ export class CoreCourseHelperProvider { * * @param module The module to open. * @param courseId The course ID of the module. - * @param sectionId The section ID of the module. - * @param modParams Params to pass to the module + * @param options Other options. * @param True if module can be opened, false otherwise. */ - async openModule(module: CoreCourseModuleData, courseId: number, sectionId?: number, modParams?: Params): Promise { + async openModule(module: CoreCourseModuleData, courseId: number, options: CoreCourseOpenModuleOptions = {}): Promise { if (!module.handlerData) { module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor( module.modname, module, courseId, - sectionId, + options.sectionId, false, ); } if (module.handlerData?.action) { - module.handlerData.action(new Event('click'), module, courseId, { animated: false, params: modParams }); + module.handlerData.action(new Event('click'), module, courseId, { + animated: false, + ...options.modNavOptions, + }); return true; } @@ -2206,6 +2191,39 @@ export type CoreCourseConfirmPrefetchCoursesOptions = CoreCoursePrefetchCoursesO onProgress?: (data: CoreCourseCoursesProgress) => void; }; +/** + * Common options for navigate to module functions. + */ +type CoreCourseNavigateToModuleCommonOptions = { + courseId?: number; // Course ID. If not defined we'll try to retrieve it from the site. + sectionId?: number; // Section the module belongs to. If not defined we'll try to retrieve it from the site. + modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module. + siteId?: string; // Site ID. If not defined, current site. +}; + +/** + * Options for navigate to module by instance function. + */ +export type CoreCourseNavigateToModuleByInstanceOptions = CoreCourseNavigateToModuleCommonOptions & { + // True to retrieve all instances with a single WS call. Not recommended if can return a lot of contents. + useModNameToGetModule?: boolean; +}; + +/** + * Options for navigate to module function. + */ +export type CoreCourseNavigateToModuleOptions = CoreCourseNavigateToModuleCommonOptions & { + modName?: string; // To retrieve all instances with a single WS call. Not recommended if can return a lot of contents. +}; + +/** + * Options for open module function. + */ +export type CoreCourseOpenModuleOptions = { + sectionId?: number; // Section the module belongs to. + modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module. +}; + type ComponentWithContextMenu = { prefetchStatusIcon?: string; isDestroyed?: boolean; diff --git a/src/core/features/sitehome/pages/index/index.ts b/src/core/features/sitehome/pages/index/index.ts index b58998e07..087589a5d 100644 --- a/src/core/features/sitehome/pages/index/index.ts +++ b/src/core/features/sitehome/pages/index/index.ts @@ -26,7 +26,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate'; -import { CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreUtils } from '@services/utils/utils'; @@ -73,8 +73,15 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy { const module = CoreNavigator.getRouteParam('module'); if (module) { - const modParams = CoreNavigator.getRouteParam('modParams'); - CoreCourseHelper.openModule(module, this.siteHomeId, undefined, modParams); + let modNavOptions = CoreNavigator.getRouteParam('modNavOptions'); + if (!modNavOptions) { + // Fallback to old way of passing params. @deprecated since 4.0. + const modParams = CoreNavigator.getRouteParam('modParams'); + if (modParams) { + modNavOptions = { params: modParams }; + } + } + CoreCourseHelper.openModule(module, this.siteHomeId, { modNavOptions }); } this.loadContent().finally(() => { diff --git a/upgrade.txt b/upgrade.txt index e943d0f93..4755f12ea 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -19,6 +19,7 @@ information provided here is intended especially for developers. - displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead. - Most of the functions or callbacks that handle redirects/deeplinks have been modified to accept an object instead of just path + options. E.g.: CoreLoginHelper.isSiteLoggedOut, CoreLoginHelper.openBrowserForSSOLogin, CoreLoginHelper.openBrowserForOAuthLogin, CoreLoginHelper.prepareForSSOLogin, CoreApp.storeRedirect, CoreSites.loadSite. - Course preview page route has changed from course/:courseId/preview to course/:courseId/summary to match with the page name and characteristics. +- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule. === 3.9.5 === From 5f3e7bb0b58c703d2d8aae8f82e9581b8f3f9c97 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 15 Feb 2022 14:42:23 +0100 Subject: [PATCH 3/4] MOBILE-3980 mod: Fix print [object...] in error download files --- src/core/features/course/classes/main-resource-component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index 24964fc75..625278153 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -295,7 +295,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, error, ]); } else { - error = CoreTextUtils.getErrorMessageFromError(error) || error; + error = CoreTextUtils.getErrorMessageFromError(error) || ''; return Translate.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : ''); } From 1b67036aca4fb4cc4dfe30e2a382c51b82527eab Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 16 Feb 2022 08:18:36 +0100 Subject: [PATCH 4/4] MOBILE-3980 book: Support resuming a book --- scripts/langindex.json | 4 +- .../calendar/services/database/calendar.ts | 2 +- src/addons/mod/book/book.module.ts | 7 +++ .../index/addon-mod-book-index.html | 3 +- src/addons/mod/book/components/index/index.ts | 51 ++++++++++++++----- src/addons/mod/book/lang.json | 1 - .../mod/book/pages/contents/contents.ts | 23 ++++++--- src/addons/mod/book/services/book.ts | 50 ++++++++++++++++++ src/addons/mod/book/services/database/book.ts | 46 +++++++++++++++++ src/core/lang.json | 1 + 10 files changed, 162 insertions(+), 26 deletions(-) create mode 100644 src/addons/mod/book/services/database/book.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5a733c08e..18d1bdd5c 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -450,7 +450,6 @@ "addon.mod_bigbluebuttonbn.view_message_session_started_at": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewer": "bigbluebuttonbn", "addon.mod_bigbluebuttonbn.view_message_viewers": "bigbluebuttonbn", - "addon.mod_book.book:read": "book", "addon.mod_book.errorchapter": "book", "addon.mod_book.modulenameplural": "book", "addon.mod_book.navnexttitle": "book", @@ -2127,6 +2126,7 @@ "core.resources": "moodle", "core.restore": "moodle", "core.restricted": "moodle", + "core.resume": "local_moodlemobileapp", "core.retry": "local_moodlemobileapp", "core.save": "moodle", "core.savechanges": "assign", @@ -2253,7 +2253,7 @@ "core.sorry": "local_moodlemobileapp", "core.sort": "moodle", "core.sortby": "moodle", - "core.start": "grouptool", + "core.start": "local_moodlemobileapp", "core.storingfiles": "local_moodlemobileapp", "core.strftimedate": "langconfig", "core.strftimedatefullshort": "langconfig", diff --git a/src/addons/calendar/services/database/calendar.ts b/src/addons/calendar/services/database/calendar.ts index 359f9ea4b..b21928140 100644 --- a/src/addons/calendar/services/database/calendar.ts +++ b/src/addons/calendar/services/database/calendar.ts @@ -19,7 +19,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AddonCalendar, AddonCalendarEventType, AddonCalendarProvider } from '../calendar'; /** - * Database variables for AddonDatabase service. + * Database variables for AddonCalendarProvider service. */ export const EVENTS_TABLE = 'addon_calendar_events_3'; export const REMINDERS_TABLE = 'addon_calendar_reminders'; diff --git a/src/addons/mod/book/book.module.ts b/src/addons/mod/book/book.module.ts index 3ac80f207..351e3ee18 100644 --- a/src/addons/mod/book/book.module.ts +++ b/src/addons/mod/book/book.module.ts @@ -26,6 +26,8 @@ import { AddonModBookListLinkHandler } from './services/handlers/list-link'; import { AddonModBookPrefetchHandler } from './services/handlers/prefetch'; import { AddonModBookTagAreaHandler } from './services/handlers/tag-area'; import { AddonModBookProvider } from './services/book'; +import { CORE_SITE_SCHEMAS } from '@services/sites'; +import { BOOK_SITE_SCHEMA } from './services/database/book'; export const ADDON_MOD_BOOK_SERVICES: Type[] = [ AddonModBookProvider, @@ -44,6 +46,11 @@ const routes: Routes = [ AddonModBookComponentsModule, ], providers: [ + { + provide: CORE_SITE_SCHEMAS, + useValue: [BOOK_SITE_SCHEMA], + multi: true, + }, { provide: APP_INITIALIZER, multi: true, diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html index a87c06a88..0ebdcb21f 100644 --- a/src/addons/mod/book/components/index/addon-mod-book-index.html +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -45,7 +45,8 @@ - {{ 'addon.mod_book.book:read' | translate }} + {{ 'core.start' | translate }} + {{ 'core.resume' | translate }} diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts index 35f24bbd2..4ef609091 100644 --- a/src/addons/mod/book/components/index/index.ts +++ b/src/addons/mod/book/components/index/index.ts @@ -32,6 +32,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp addPadding = true; showBullets = false; chapters: AddonModBookTocChapter[] = []; + hasStartedBook = false; protected book?: AddonModBookBookWSData; @@ -53,25 +54,45 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp */ protected async fetchContent(refresh?: boolean): Promise { try { - this.book = await AddonModBook.getBook(this.courseId, this.module.id); - - if (this.book) { - this.dataRetrieved.emit(this.book); - - this.description = this.book.intro; - this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS; - this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS; - this.addPadding = this.book.numbering != AddonModBookNumbering.NONE; - } - - const contents = await CoreCourse.getModuleContents(this.module, this.courseId); - - this.chapters = AddonModBook.getTocList(contents); + await Promise.all([ + this.loadBook(), + this.loadTOC(), + ]); } finally { this.fillContextMenu(refresh); } } + /** + * Load book data. + * + * @return Promise resolved when done. + */ + protected async loadBook(): Promise { + this.book = await AddonModBook.getBook(this.courseId, this.module.id); + + this.dataRetrieved.emit(this.book); + + this.description = this.book.intro; + this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS; + this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS; + this.addPadding = this.book.numbering != AddonModBookNumbering.NONE; + + const lastChapterViewed = await AddonModBook.getLastChapterViewed(this.book.id); + this.hasStartedBook = lastChapterViewed !== undefined; + } + + /** + * Load book TOC. + * + * @return Promise resolved when done. + */ + protected async loadTOC(): Promise { + const contents = await CoreCourse.getModuleContents(this.module, this.courseId); + + this.chapters = AddonModBook.getTocList(contents); + } + /** * Open the book in a certain chapter. * @@ -85,6 +106,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp chapterId, }, }); + + this.hasStartedBook = true; } /** diff --git a/src/addons/mod/book/lang.json b/src/addons/mod/book/lang.json index 8b6812eb9..64afa1cd7 100644 --- a/src/addons/mod/book/lang.json +++ b/src/addons/mod/book/lang.json @@ -1,5 +1,4 @@ { - "book:read": "View book", "errorchapter": "Error reading chapter of book.", "modulenameplural": "Books", "navnexttitle": "Next: {{$a}}", diff --git a/src/addons/mod/book/pages/contents/contents.ts b/src/addons/mod/book/pages/contents/contents.ts index de3de0c6f..0f2444198 100644 --- a/src/addons/mod/book/pages/contents/contents.ts +++ b/src/addons/mod/book/pages/contents/contents.ts @@ -133,13 +133,9 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { const downloadResult = await this.downloadResourceIfNeeded(module, refresh); - if (book) { - this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; - this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT; - this.title = book.name; - } else { - this.title = this.title || Translate.instant('addon.mod_book.book:read'); - } + this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY; + this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT; + this.title = book.name; // Get contents. No need to refresh, it has been done in downloadResourceIfNeeded. await source.loadContents(); @@ -283,6 +279,10 @@ export class AddonModBookContentsPage implements OnInit, OnDestroy { this.navigationItems = this.getNavigationItems(chapterId); } + if (this.book) { + AddonModBook.storeLastChapterViewed(this.book.id, chapterId); + } + if (!this.module) { return; } @@ -371,6 +371,15 @@ class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSo this.module = await CoreCourse.getModule(this.CM_ID, this.COURSE_ID); this.book = await AddonModBook.getBook(this.COURSE_ID, this.CM_ID); + if (!this.initialItem) { + // No chapter ID specified. Calculate last viewed. + const lastViewed = await AddonModBook.getLastChapterViewed(this.book.id); + + if (lastViewed) { + this.initialItem = { id: lastViewed }; + } + } + return { module: this.module, book: this.book, diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts index 1efa28d21..6b6c18a8f 100644 --- a/src/addons/mod/book/services/book.ts +++ b/src/addons/mod/book/services/book.ts @@ -26,6 +26,11 @@ import { CoreTextUtils } from '@services/utils/text'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreFile } from '@services/file'; import { CoreError } from '@classes/errors/error'; +import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; +import { asyncInstance, AsyncInstance } from '@/core/utils/async-instance'; +import { CoreDatabaseTable } from '@classes/database/database-table'; +import { AddonModBookLastChapterViewedDBRecord, LAST_CHAPTER_VIEWED_TABLE } from './database/book'; +import { CoreDatabaseCachingStrategy } from '@classes/database/database-table-proxy'; /** * Constants to define how the chapters and subchapters of a book should be displayed in that table of contents. @@ -56,6 +61,19 @@ export class AddonModBookProvider { static readonly COMPONENT = 'mmaModBook'; + protected lastChapterViewedTables: LazyMap>>; + + constructor() { + this.lastChapterViewedTables = lazyMap( + siteId => asyncInstance( + () => CoreSites.getSiteTable(LAST_CHAPTER_VIEWED_TABLE, { + siteId, + config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, + }), + ), + ); + } + /** * Get a book by course module ID. * @@ -216,6 +234,24 @@ export class AddonModBookProvider { return chapters[0].id; } + /** + * Get last chapter viewed in the app for a book. + * + * @param id Book instance ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with last chapter viewed, undefined if none. + */ + async getLastChapterViewed(id: number, siteId?: string): Promise { + try { + const site = await CoreSites.getSite(siteId); + const entry = await this.lastChapterViewedTables[site.getId()].getOneByPrimaryKey({ id }); + + return entry.chapterid; + } catch { + // No last chapter viewed. + } + } + /** * Get the book toc as an array. * @@ -363,6 +399,20 @@ export class AddonModBookProvider { ); } + /** + * Store last chapter viewed in the app for a book. + * + * @param id Book instance ID. + * @param chapterId Chapter ID. + * @param siteId Site ID. If not defined, current site. + * @return Promise resolved with last chapter viewed, undefined if none. + */ + async storeLastChapterViewed(id: number, chapterId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await this.lastChapterViewedTables[site.getId()].insert({ id, chapterid: chapterId }); + } + } export const AddonModBook = makeSingleton(AddonModBookProvider); diff --git a/src/addons/mod/book/services/database/book.ts b/src/addons/mod/book/services/database/book.ts new file mode 100644 index 000000000..38f526348 --- /dev/null +++ b/src/addons/mod/book/services/database/book.ts @@ -0,0 +1,46 @@ +// (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 { CoreSiteSchema } from '@services/sites'; + +/** + * Database variables for AddonModBookProvider service. + */ +export const LAST_CHAPTER_VIEWED_TABLE = 'addon_mod_book_last_chapter_viewed'; +export const BOOK_SITE_SCHEMA: CoreSiteSchema = { + name: 'AddonModBookProvider', + version: 1, + tables: [ + { + name: LAST_CHAPTER_VIEWED_TABLE, + columns: [ + { + name: 'id', + type: 'INTEGER', + primaryKey: true, + }, + { + name: 'chapterid', + type: 'INTEGER', + notNull: true, + }, + ], + }, + ], +}; + +export type AddonModBookLastChapterViewedDBRecord = { + id: number; + chapterid: number; +}; diff --git a/src/core/lang.json b/src/core/lang.json index 651d5fd54..5b58a7e07 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -248,6 +248,7 @@ "restore": "Restore", "restricted": "Restricted", "retry": "Retry", + "resume": "Resume", "save": "Save", "savechanges": "Save changes", "scanqr": "Scan QR code",