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