MOBILE-3980 book: Add index page for book
parent
c91f24245b
commit
f4cb040aa0
|
@ -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",
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons slot="end">
|
||||
<ion-button (click)="showToc()" [attr.aria-label]="'addon.mod_book.toc' | translate" aria-haspopup="true" *ngIf="loaded">
|
||||
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl"
|
||||
iconAction="fas-external-link-alt" [showBrowserWarning]="false"></core-context-menu-item>
|
||||
|
@ -28,31 +25,30 @@
|
|||
[courseId]="courseId">
|
||||
</core-course-module-info>
|
||||
|
||||
<ion-card class="core-warning-card" *ngIf="warning">
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label><span [innerHTML]="warning"></span></ion-label>
|
||||
<ion-label>
|
||||
<h2>{{ 'addon.mod_book.toc' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div class="safe-area-padding-horizontal">
|
||||
<core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" [showTitles]="displayTitlesInNavBar"
|
||||
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle" (action)="changeChapter($event.id)">
|
||||
</core-navigation-bar>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let chapter of chapters" [class.item-dimmed]="chapter.hidden" button detail="true"
|
||||
(click)="openBook(chapter.id)">
|
||||
<ion-label>
|
||||
<p [class.ion-padding-start]="addPadding && chapter.level == 1 ? true : null">
|
||||
<span *ngIf="showNumbers" class="addon-mod-book-number">{{chapter.indexNumber}} </span>
|
||||
<span *ngIf="showBullets" class="addon-mod-book-bullet">• </span>
|
||||
<core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
|
||||
<ng-template let-chapter="item">
|
||||
<div class="ion-padding">
|
||||
<core-format-text [component]="component" [componentId]="componentId" [text]="chapter.content" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId"></core-format-text>
|
||||
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
|
||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||
<core-tag-list [tags]="chapter.tags"></core-tag-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-swipe-slides>
|
||||
</div>
|
||||
<ion-button class="ion-margin" expand="block" (click)="openBook()">
|
||||
{{ 'addon.mod_book.book:read' | translate }}
|
||||
</ion-button>
|
||||
|
||||
</ion-list>
|
||||
|
||||
</core-loading>
|
||||
|
||||
|
|
|
@ -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<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
|
||||
warning = '';
|
||||
displayNavBar = true;
|
||||
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// Create the toc modal.
|
||||
const visibleChapter = this.manager?.getSelectedItem();
|
||||
|
||||
const modalData = await CoreDomUtils.openSideModal<number>({
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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<AddonModBookTocChapter>[] {
|
||||
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<LoadedChapter> {
|
||||
|
||||
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<AddonModBookBookWSData> {
|
||||
this.book = await AddonModBook.getBook(this.COURSE_ID, this.MODULE.id);
|
||||
|
||||
return this.book;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load module contents.
|
||||
*/
|
||||
async loadContents(): Promise<void> {
|
||||
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<LoadedChapter[]> {
|
||||
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<void> {
|
||||
return AddonModBook.invalidateContent(this.MODULE.id, this.COURSE_ID);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<h1>
|
||||
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="cmId" [courseId]="courseId">
|
||||
</core-format-text>
|
||||
</h1>
|
||||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="showToc()" [attr.aria-label]="'addon.mod_book.toc' | translate" aria-haspopup="true" *ngIf="loaded">
|
||||
<ion-icon name="fas-bookmark" slot="icon-only" aria-hidden="true"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="doRefresh($event.target)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="loaded">
|
||||
|
||||
<ion-card class="core-warning-card" *ngIf="warning">
|
||||
<ion-item>
|
||||
<ion-icon name="fas-exclamation-triangle" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label><span [innerHTML]="warning"></span></ion-label>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<div class="safe-area-padding-horizontal">
|
||||
<core-navigation-bar *ngIf="displayNavBar" [items]="navigationItems" [showTitles]="displayTitlesInNavBar"
|
||||
previousTranslate="addon.mod_book.navprevtitle" nextTranslate="addon.mod_book.navnexttitle"
|
||||
(action)="changeChapter($event.id)">
|
||||
</core-navigation-bar>
|
||||
|
||||
<core-swipe-slides [manager]="manager" [options]="slidesOpts">
|
||||
<ng-template let-chapter="item">
|
||||
<div class="ion-padding">
|
||||
<core-format-text [component]="component" [componentId]="cmId" [text]="chapter.content" contextLevel="module"
|
||||
[contextInstanceId]="cmId" [courseId]="courseId"></core-format-text>
|
||||
<div class="ion-margin-top" *ngIf="chapter.tags?.length > 0">
|
||||
<strong>{{ 'core.tag.tags' | translate }}: </strong>
|
||||
<core-tag-list [tags]="chapter.tags"></core-tag-list>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</core-swipe-slides>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -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 {}
|
|
@ -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<LoadedChapter, AddonModBookSlidesItemsManagerSource>;
|
||||
warning = '';
|
||||
displayNavBar = true;
|
||||
navigationItems: CoreNavigationBarItem<AddonModBookTocChapter>[] = [];
|
||||
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<void> {
|
||||
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<CoreCourseResourceDownloadResult> {
|
||||
|
||||
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<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 modalData = await CoreDomUtils.openSideModal<number>({
|
||||
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<void> {
|
||||
// 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<AddonModBookTocChapter>[] {
|
||||
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<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.
|
||||
*
|
||||
* @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<void> {
|
||||
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<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.
|
||||
*
|
||||
* @return Resolved when done.
|
||||
*/
|
||||
invalidateContent(): Promise<void> {
|
||||
return AddonModBook.invalidateContent(this.CM_ID, this.COURSE_ID);
|
||||
}
|
||||
|
||||
}
|
|
@ -19,6 +19,6 @@
|
|||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId" (dataRetrieved)="updateData($event)">
|
||||
<addon-mod-book-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
|
||||
</addon-mod-book-index>
|
||||
</ion-content>
|
||||
|
|
|
@ -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<AddonModBookIndexComponent> implements OnInit {
|
||||
export class AddonModBookIndexPage extends CoreCourseModuleMainActivityPage<AddonModBookIndexComponent> {
|
||||
|
||||
@ViewChild(AddonModBookIndexComponent) activityComponent?: AddonModBookIndexComponent;
|
||||
|
||||
chapterId?: number;
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
this.chapterId = CoreNavigator.getRouteNumberParam('chapterId');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue