406 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			406 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| // (C) Copyright 2015 Moodle Pty Ltd.
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| import { Component, 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 { CoreCourse, CoreCourseModuleContentFile } 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 { CoreNetwork } from '@services/network';
 | |
| 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 {
 | |
|     AddonModBook,
 | |
|     AddonModBookBookWSData,
 | |
|     AddonModBookContentsMap,
 | |
|     AddonModBookTocChapter,
 | |
| } from '../../services/book';
 | |
| import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics';
 | |
| import { CoreUrl } from '@singletons/url';
 | |
| import { ADDON_MOD_BOOK_COMPONENT, AddonModBookNavStyle } from '../../constants';
 | |
| import { CoreModals } from '@services/modals';
 | |
| 
 | |
| /**
 | |
|  * Page that displays a book contents.
 | |
|  */
 | |
| @Component({
 | |
|     selector: 'page-addon-mod-book-contents',
 | |
|     templateUrl: 'contents.html',
 | |
|     styleUrls: ['contents.scss'],
 | |
| })
 | |
| export class AddonModBookContentsPage implements OnInit, OnDestroy {
 | |
| 
 | |
|     @ViewChild(CoreSwipeSlidesComponent) swipeSlidesComponent?: CoreSwipeSlidesComponent;
 | |
| 
 | |
|     title = '';
 | |
|     cmId!: number;
 | |
|     courseId!: number;
 | |
|     initialChapterId?: number;
 | |
|     component = ADDON_MOD_BOOK_COMPONENT;
 | |
|     manager?: CoreSwipeSlidesItemsManager<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
 | |
|     displayNavBar = true;
 | |
|     navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
 | |
|     swiperOpts: CoreSwipeSlidesOptions = {
 | |
|         autoHeight: true,
 | |
|         observer: true,
 | |
|         observeParents: true,
 | |
|         scrollOnChange: 'top',
 | |
|     };
 | |
| 
 | |
|     loaded = false;
 | |
| 
 | |
|     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.
 | |
|      * @returns Promise resolved when done.
 | |
|      */
 | |
|     protected async fetchContent(refresh = false): Promise<void> {
 | |
|         try {
 | |
|             const source = this.manager?.getSource();
 | |
|             if (!source) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             const { book } = await source.loadBookData();
 | |
| 
 | |
|             this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
 | |
|             this.title = book.name;
 | |
| 
 | |
|             await source.loadContents(refresh);
 | |
| 
 | |
|             await source.load();
 | |
|         } catch (error) {
 | |
|             CoreDomUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
 | |
|         } finally {
 | |
|             this.loaded = true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Change the current chapter.
 | |
|      *
 | |
|      * @param chapterId Chapter to load.
 | |
|      */
 | |
|     changeChapter(chapterId: number): void {
 | |
|         if (!chapterId) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         this.swipeSlidesComponent?.slideToItem({ id: chapterId });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Refresh the data.
 | |
|      *
 | |
|      * @param refresher Refresher.
 | |
|      * @returns Promise resolved when done.
 | |
|      */
 | |
|     async doRefresh(refresher?: HTMLIonRefresherElement): Promise<void> {
 | |
|         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<void> {
 | |
|         // Create the toc modal.
 | |
|         const visibleChapter = this.manager?.getSelectedItem();
 | |
| 
 | |
|         const { AddonModBookTocComponent } = await import('../../components/toc/toc');
 | |
| 
 | |
|         const modalData = await CoreModals.openSideModal<number>({
 | |
|             component: AddonModBookTocComponent,
 | |
|             componentProps: {
 | |
|                 moduleId: this.cmId,
 | |
|                 chapters: this.chapters,
 | |
|                 selected: visibleChapter?.id,
 | |
|                 courseId: this.courseId,
 | |
|                 book: this.book,
 | |
|             },
 | |
|         });
 | |
| 
 | |
|         if (modalData) {
 | |
|             this.changeChapter(modalData);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update data related to chapter being viewed.
 | |
|      *
 | |
|      * @param chapterId Chapter viewed.
 | |
|      * @returns Promise resolved when done.
 | |
|      */
 | |
|     protected async onChapterViewed(chapterId: number): Promise<void> {
 | |
|         if (this.displayNavBar) {
 | |
|             this.navigationItems = this.getNavigationItems(chapterId);
 | |
|         }
 | |
| 
 | |
|         if (this.book) {
 | |
|             AddonModBook.storeLastChapterViewed(this.book.id, chapterId, this.courseId);
 | |
|         }
 | |
| 
 | |
|         if (!this.module) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Chapter loaded, log view.
 | |
|         await CoreUtils.ignoreErrors(AddonModBook.logView(this.module.instance, chapterId));
 | |
| 
 | |
|         CoreAnalytics.logEvent({
 | |
|             type: CoreAnalyticsEventType.VIEW_ITEM,
 | |
|             ws: 'mod_book_view_book',
 | |
|             name: this.module.name,
 | |
|             data: { id: this.module.instance, category: 'book', chapterid: chapterId },
 | |
|             url: CoreUrl.addParamsToUrl(`/mod/book/view.php?id=${this.module.id}`, { chapterid: chapterId }),
 | |
|         });
 | |
| 
 | |
|         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.
 | |
|      * @returns Navigation items.
 | |
|      */
 | |
|     protected getNavigationItems(chapterId: number): CoreNavigationBarItem<AddonModBookTocChapter>[] {
 | |
|         return this.chapters.map((chapter) => ({
 | |
|             item: chapter,
 | |
|             title: chapter.title,
 | |
|             current: chapter.id == chapterId,
 | |
|             enabled: true,
 | |
|         }));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @inheritdoc
 | |
|      */
 | |
|     ngOnDestroy(): void {
 | |
|         this.manager?.destroy();
 | |
|         this.managerUnsubscribe?.();
 | |
| 
 | |
|         delete this.manager;
 | |
|     }
 | |
| 
 | |
| }
 | |
| 
 | |
| type LoadedChapter = {
 | |
|     id: number;
 | |
|     content?: string;
 | |
|     tags?: CoreTagItem[];
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Helper to manage swiping within a collection of chapters.
 | |
|  */
 | |
| class AddonModBookSlidesItemsManagerSource extends CoreSwipeSlidesItemsManagerSource<LoadedChapter> {
 | |
| 
 | |
|     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.
 | |
|      *
 | |
|      * @returns 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);
 | |
| 
 | |
|         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,
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Load module contents.
 | |
|      *
 | |
|      * @param refresh Whether we're refreshing data.
 | |
|      */
 | |
|     async loadContents(refresh = false): Promise<void> {
 | |
|         if (!this.module) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const contents = await this.getModuleContents(refresh);
 | |
| 
 | |
|         this.contentsMap = AddonModBook.getContentsMap(contents);
 | |
|         this.chapters = AddonModBook.getTocList(contents);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get module contents.
 | |
|      *
 | |
|      * @param refresh Whether we're refreshing data.
 | |
|      * @returns Module contents.
 | |
|      */
 | |
|     protected async getModuleContents(refresh = false): Promise<CoreCourseModuleContentFile[]> {
 | |
|         if (!this.module) {
 | |
|             return [];
 | |
|         }
 | |
| 
 | |
|         const ignoreCache = refresh && CoreNetwork.isOnline();
 | |
| 
 | |
|         try {
 | |
|             return await CoreCourse.getModuleContents(this.module, this.COURSE_ID, undefined, false, ignoreCache);
 | |
|         } catch (error) {
 | |
|             // Error loading contents. If we ignored cache, try to get the cached value.
 | |
|             if (ignoreCache && !this.module.contents) {
 | |
|                 return await CoreCourse.getModuleContents(this.module);
 | |
|             } else if (!this.module.contents) {
 | |
|                 // Not able to load contents, throw the error.
 | |
|                 throw error;
 | |
|             }
 | |
| 
 | |
|             return this.module.contents;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @inheritdoc
 | |
|      */
 | |
|     protected async loadItems(): Promise<LoadedChapter[]> {
 | |
|         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.
 | |
|      *
 | |
|      * @returns Resolved when done.
 | |
|      */
 | |
|     invalidateContent(): Promise<void> {
 | |
|         return AddonModBook.invalidateContent(this.CM_ID, this.COURSE_ID);
 | |
|     }
 | |
| 
 | |
| }
 |