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