MOBILE-3637 book: Implement book activity

main
Pau Ferrer Ocaña 2021-02-08 14:29:11 +01:00
parent 5a15fca0a9
commit c98fa810fa
23 changed files with 1527 additions and 10 deletions

View File

@ -0,0 +1,28 @@
// (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';
const routes: Routes = [
{
path: ':courseId/:cmdId',
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModBookIndexPageModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class AddonModBookLazyModule {}

View File

@ -0,0 +1,57 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
import { AddonModBookComponentsModule } from './components/components.module';
import { AddonModBookModuleHandler, AddonModBookModuleHandlerService } from './services/handlers/module';
import { AddonModBookIndexLinkHandler } from './services/handlers/index-link';
import { AddonModBookListLinkHandler } from './services/handlers/list-link';
import { AddonModBookPrefetchHandler } from './services/handlers/prefetch';
import { AddonModBookTagAreaHandler } from './services/handlers/tag-area';
const routes: Routes = [
{
path: AddonModBookModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./book-lazy.module').then(m => m.AddonModBookLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModBookComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.instance.registerHandler(AddonModBookModuleHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModBookIndexLinkHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModBookListLinkHandler.instance);
CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModBookPrefetchHandler.instance);
CoreTagAreaDelegate.instance.registerHandler(AddonModBookTagAreaHandler.instance);
},
},
],
})
export class AddonModBookModule {}

View File

@ -0,0 +1,47 @@
// (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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/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';
@NgModule({
declarations: [
AddonModBookIndexComponent,
AddonModBookTocComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
FormsModule,
CoreSharedModule,
CoreCourseComponentsModule,
CoreTagComponentsModule,
],
exports: [
AddonModBookIndexComponent,
AddonModBookTocComponent,
],
})
export class AddonModBookComponentsModule {}

View File

@ -0,0 +1,52 @@
<!-- 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"></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"></core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right"></core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()"></core-context-menu-item>
<core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)"
[iconAction]="refreshIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center">
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId"></core-course-module-description>
<ion-card class="core-warning-card" *ngIf="warning">
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<span [innerHTML]="warning"></span>
</ion-card>
<div class="ion-padding safe-padding-horizontal">
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id"
[previousTitle]="previousNavBarTitle" [next]="nextChapter?.id" [nextTitle]="nextNavBarTitle"
(action)="changeChapter($event)">
</core-navigation-bar>
<core-format-text [component]="component" [componentId]="componentId" [text]="chapterContent" contextLevel="module"
[contextInstanceId]="module?.id" [courseId]="courseId"></core-format-text>
<div class="ion-margin-top" *ngIf="tagsEnabled && tags?.length > 0">
<strong>{{ 'core.tag.tags' | translate }}: </strong>
<core-tag-list [tags]="tags"></core-tag-list>
</div>
<core-navigation-bar *ngIf="displayNavBar" [previous]="previousChapter?.id"
[previousTitle]="previousNavBarTitle" [next]="nextChapter?.id" [nextTitle]="nextNavBarTitle"
(action)="changeChapter($event)"></core-navigation-bar>
</div>
</core-loading>

View File

@ -0,0 +1,251 @@
// (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, Optional, Input, OnInit } from '@angular/core';
import { IonContent } from '@ionic/angular';
import {
CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult,
} 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 { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { ModalController, Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import { AddonModBookTocComponent } from '../toc/toc';
import { CoreConstants } from '@/core/constants';
/**
* Component that displays a book.
*/
@Component({
selector: 'addon-mod-book-index',
templateUrl: 'addon-mod-book-index.html',
})
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
@Input() initialChapterId?: number; // The initial chapter ID to load.
component = AddonModBookProvider.COMPONENT;
chapterContent?: string;
previousChapter?: AddonModBookTocChapter;
nextChapter?: AddonModBookTocChapter;
tagsEnabled = false;
displayNavBar = true;
previousNavBarTitle?: string;
nextNavBarTitle?: string;
warning = '';
tags?: CoreTagItem[];
protected chapters: AddonModBookTocChapter[] = [];
protected currentChapter?: number;
protected book?: AddonModBookBookWSData;
protected displayTitlesInNavBar = false;
protected contentsMap: AddonModBookContentsMap = {};
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModBookIndexComponent', courseContentsPage);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.tagsEnabled = CoreTag.instance.areTagsAvailableInSite();
this.loadContent();
}
/**
* Show the TOC.
*/
async showToc(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.instance.create({
component: AddonModBookTocComponent,
componentProps: {
moduleId: this.module!.id,
chapters: this.chapters,
selected: this.currentChapter,
courseId: this.courseId,
book: this.book,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
this.changeChapter(result.data);
}
}
/**
* Change the current chapter.
*
* @param chapterId Chapter to load.
* @return Promise resolved when done.
*/
changeChapter(chapterId: number): void {
if (chapterId && chapterId != this.currentChapter) {
this.loaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.loadChapter(chapterId, true);
}
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected invalidateContent(): Promise<void> {
return AddonModBook.instance.invalidateContent(this.module!.id, this.courseId!);
}
/**
* 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> {
const promises: Promise<void>[] = [];
let downloadResult: CoreCourseResourceDownloadResult | undefined;
// Try to get the book data. Ignore errors since this WS isn't available in some Moodle versions.
promises.push(CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(this.courseId!, this.module!.id))
.then((book) => {
if (!book) {
return;
}
this.book = book;
this.dataRetrieved.emit(book);
this.description = book.intro;
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
return;
}));
// Get module status to determine if it needs to be downloaded.
promises.push(this.downloadResourceIfNeeded(refresh).then((result) => {
downloadResult = result;
return;
}));
try {
await Promise.all(promises);
this.contentsMap = AddonModBook.instance.getContentsMap(this.module!.contents);
this.chapters = AddonModBook.instance.getTocList(this.module!.contents);
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
// Initial chapter set. Validate that the chapter exists.
const chapter = this.chapters.find((chapter) => chapter.id == this.initialChapterId);
if (chapter) {
this.currentChapter = this.initialChapterId;
}
}
if (typeof this.currentChapter == 'undefined') {
// Load the first chapter.
this.currentChapter = AddonModBook.instance.getFirstChapter(this.chapters);
}
// Show chapter.
try {
await this.loadChapter(this.currentChapter!, refresh);
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
} catch {
// Ignore errors, they're handled inside the loadChapter function.
}
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Load a book chapter.
*
* @param chapterId Chapter to load.
* @param logChapterId Whether chapter ID should be passed to the log view function.
* @return Promise resolved when done.
*/
protected async loadChapter(chapterId: number, logChapterId: boolean): Promise<void> {
this.currentChapter = chapterId;
this.content?.scrollToTop();
try {
const content = await AddonModBook.instance.getChapterContent(this.contentsMap, chapterId, this.module!.id);
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
this.chapterContent = content;
this.previousChapter = AddonModBook.instance.getPreviousChapter(this.chapters, chapterId);
this.nextChapter = AddonModBook.instance.getNextChapter(this.chapters, chapterId);
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar
? Translate.instance.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title })
: '';
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar
? Translate.instance.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title })
: '';
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
await CoreUtils.instance.ignoreErrors(AddonModBook.instance.logView(
this.module!.instance!,
logChapterId ? chapterId : undefined,
this.module!.name,
));
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
if (!this.nextChapter) {
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);
throw error;
} finally {
this.loaded = true;
this.refreshIcon = CoreConstants.ICON_REFRESH;
}
}
}

View File

@ -0,0 +1,32 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'addon.mod_book.toc' | translate }}</ion-title>
<ion-buttons slot="end">
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<nav>
<ion-list>
<ion-item class="ion-text-wrap" *ngFor="let chapter of chapters" (click)="loadChapter(chapter.id)"
[class.core-nav-item-selected]="selected == chapter.id"
[class.item-dimmed]="chapter.hidden">
<ion-label>
<p [class.ion-padding-left]="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">&bull;</span>
<core-format-text [text]="chapter.title" contextLevel="module" [contextInstanceId]="moduleId"
[courseId]="courseId">
</core-format-text>
</p>
</ion-label>
</ion-item>
</ion-list>
</nav>
</ion-content>

View File

@ -0,0 +1,5 @@
.addon-mod-book-bullet {
font-weight: bold;
font-size: 1.5em;
margin-right: 3px;
}

View File

@ -0,0 +1,66 @@
// (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, Input, OnInit } from '@angular/core';
import { ModalController } from '@singletons';
import { AddonModBookTocChapter, AddonModBookBookWSData, AddonModBookNumbering } from '../../services/book';
/**
* Modal to display the TOC of a book.
*/
@Component({
selector: 'addon-mod-book-toc',
templateUrl: 'toc.html',
styleUrls: ['toc.scss'],
})
export class AddonModBookTocComponent implements OnInit {
@Input() moduleId?: number;
@Input() chapters: AddonModBookTocChapter[] = [];
@Input() selected?: number;
@Input() courseId?: number;
showNumbers = true;
addPadding = true;
showBullets = false;
@Input() protected book?: AddonModBookBookWSData;
/**
* Component loaded.
*/
ngOnInit(): void {
if (this.book) {
this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS;
this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS;
this.addPadding = this.book.numbering != AddonModBookNumbering.NONE;
}
}
/**
* Function called when a course is clicked.
*
* @param id ID of the clicked chapter.
*/
loadChapter(id: number): void {
ModalController.instance.dismiss(id);
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.instance.dismiss();
}
}

View File

@ -0,0 +1,8 @@
{
"errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books",
"navnexttitle": "Next: {{$a}}",
"navprevtitle": "Previous: {{$a}}",
"tagarea_book_chapters": "Book chapters",
"toc": "Table of contents"
}

View File

@ -0,0 +1,23 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!bookComponent?.loaded" (ionRefresh)="bookComponent?.doRefresh($event)">
<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>
</ion-content>

View File

@ -0,0 +1,46 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModBookComponentsModule } from '../../components/components.module';
import { AddonModBookIndexPage } from './index';
const routes: Routes = [
{
path: '',
component: AddonModBookIndexPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
AddonModBookComponentsModule,
],
declarations: [
AddonModBookIndexPage,
],
exports: [RouterModule],
})
export class AddonModBookIndexPageModule {}

View File

@ -0,0 +1,57 @@
// (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, OnInit, ViewChild } from '@angular/core';
import { CoreCourseWSModule } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { AddonModBookIndexComponent } from '../../components/index/index';
import { AddonModBookBookWSData } from '../../services/book';
/**
* Page that displays a book.
*/
@Component({
selector: 'page-addon-mod-book-index',
templateUrl: 'index.html',
})
export class AddonModBookIndexPage implements OnInit {
@ViewChild(AddonModBookIndexComponent) bookComponent?: AddonModBookIndexComponent;
title?: string;
module?: CoreCourseWSModule;
courseId?: number;
chapterId?: number;
/**
* Component being initialized.
*/
ngOnInit(): void {
this.module = CoreNavigator.instance.getRouteParam('module');
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
this.chapterId = CoreNavigator.instance.getRouteNumberParam('chapterId');
this.title = this.module?.name;
}
/**
* Update some data based on the book instance.
*
* @param book Book instance.
*/
updateData(book: AddonModBookBookWSData): void {
this.title = book.name || this.title;
}
}

View File

@ -0,0 +1,479 @@
// (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 { Injectable } from '@angular/core';
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreTagItem } from '@features/tag/services/tag';
import { CoreWSExternalWarning, CoreWSExternalFile, CoreWS } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreCourse, CoreCourseModuleContentFile } from '@features/course/services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreFilepool } from '@services/filepool';
import { CoreTextUtils } from '@services/utils/text';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreFile } from '@services/file';
import { CoreWSError } from '@classes/errors/wserror';
/**
* Constants to define how the chapters and subchapters of a book should be displayed in that table of contents.
*/
export const enum AddonModBookNumbering {
NONE = 0,
NUMBERS = 1,
BULLETS = 2,
INDENTED = 3,
}
/**
* Constants to define the navigation style used within a book.
*/
export const enum AddonModBookNavStyle {
TOC_ONLY = 0,
IMAGE = 1,
TEXT = 2,
}
const ROOT_CACHE_KEY = 'mmaModBook:';
/**
* Service that provides some features for books.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookProvider {
static readonly COMPONENT = 'mmaModBook';
/**
* Get a book by course module ID.
*
* @param courseId Course ID.
* @param cmId Course module ID.
* @param options Other options.
* @return Promise resolved when the book is retrieved.
*/
getBook(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModBookBookWSData> {
return this.getBookByField(courseId, 'coursemodule', cmId, options);
}
/**
* Get a book with key=value. If more than one is found, only the first will be returned.
*
* @param courseId Course ID.
* @param key Name of the property to check.
* @param value Value to search.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the book is retrieved.
*/
protected async getBookByField(
courseId: number,
key: string,
value: number,
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModBookBookWSData> {
const site = await CoreSites.instance.getSite(options.siteId);
const params: AddonModBookGetBooksByCoursesWSParams = {
courseids: [courseId],
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getBookDataCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModBookProvider.COMPONENT,
...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy),
};
const response: AddonModBookGetBooksByCoursesWSResponse = await site.read('mod_book_get_books_by_courses', params, preSets);
// Search the book.
const book = response.books.find((book) => book[key] == value);
if (book) {
return book;
}
throw new CoreWSError('Book not found');
}
/**
* Get cache key for get book data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getBookDataCacheKey(courseId: number): string {
return ROOT_CACHE_KEY + 'book:' + courseId;
}
/**
* Gets a chapter contents.
*
* @param contentsMap Contents map returned by getContentsMap.
* @param chapterId Chapter to retrieve.
* @param moduleId The module ID.
* @return Promise resolved with the contents.
*/
async getChapterContent(contentsMap: AddonModBookContentsMap, chapterId: number, moduleId: number): Promise<string> {
const indexUrl = contentsMap[chapterId] ? contentsMap[chapterId].indexUrl : undefined;
if (!indexUrl) {
// It shouldn't happen.
throw new CoreWSError('Could not locate the index chapter.');
}
if (!CoreFile.instance.isAvailable()) {
// We return the live URL.
return CoreSites.instance.getCurrentSite()!.checkAndFixPluginfileURL(indexUrl);
}
const siteId = CoreSites.instance.getCurrentSiteId();
const url = await CoreFilepool.instance.downloadUrl(siteId, indexUrl, false, AddonModBookProvider.COMPONENT, moduleId);
const content = await CoreWS.instance.getText(url);
// Now that we have the content, we update the SRC to point back to the external resource.
return CoreDomUtils.instance.restoreSourcesInHtml(content, contentsMap[chapterId].paths);
}
/**
* Convert an array of book contents into an object where contents are organized in chapters.
* Each chapter has an indexUrl and the list of contents in that chapter.
*
* @param contents The module contents.
* @return Contents map.
*/
getContentsMap(contents: CoreCourseModuleContentFile[]): AddonModBookContentsMap {
const map: AddonModBookContentsMap = {};
if (!contents) {
return map;
}
contents.forEach((content) => {
if (!this.isFileDownloadable(content)) {
return;
}
// Search the chapter number in the filepath.
const matches = content.filepath.match(/\/(\d+)\//);
if (!matches || !matches[1]) {
return;
}
let key: string;
const chapter: string = matches[1];
const filepathIsChapter = content.filepath == '/' + chapter + '/';
// Init the chapter if it's not defined yet.
map[chapter] = map[chapter] || { paths: {} };
if (content.filename == 'index.html' && filepathIsChapter) {
// Index of the chapter, set indexUrl and tags of the chapter.
map[chapter].indexUrl = content.fileurl;
map[chapter].tags = content.tags;
return;
}
if (filepathIsChapter) {
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).
// Try to get the path to the file from the URL.
const split = content.fileurl.split('mod_book/chapter' + content.filepath);
key = split[1] || content.filename; // Use filename if we couldn't find the path.
} else {
// Remove the chapter folder from the path and add the filename.
key = content.filepath.replace('/' + chapter + '/', '') + content.filename;
}
map[chapter].paths[CoreTextUtils.instance.decodeURIComponent(key)] = content.fileurl;
});
return map;
}
/**
* Get the first chapter of a book.
*
* @param chapters The chapters list.
* @return The chapter id.
*/
getFirstChapter(chapters: AddonModBookTocChapter[]): number | undefined {
if (!chapters || !chapters.length) {
return;
}
return chapters[0].id;
}
/**
* Get the next chapter to the given one.
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter.
*/
getNextChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
if (currentChapterIndex >= 0 && typeof chapters[currentChapterIndex + 1] != 'undefined') {
return chapters[currentChapterIndex + 1];
}
}
/**
* Get the previous chapter to the given one.
*
* @param chapters The chapters list.
* @param chapterId The current chapter.
* @return The next chapter.
*/
getPreviousChapter(chapters: AddonModBookTocChapter[], chapterId: number): AddonModBookTocChapter | undefined {
const currentChapterIndex = chapters.findIndex((chapter) => chapter.id == chapterId);
if (currentChapterIndex > 0) {
return chapters[currentChapterIndex - 1];
}
}
/**
* Get the book toc as an array.
*
* @param contents The module contents.
* @return The toc.
*/
getToc(contents: CoreCourseModuleContentFile[]): AddonModBookTocChapterParsed[] {
if (!contents || !contents.length || typeof contents[0].content == 'undefined') {
return [];
}
return CoreTextUtils.instance.parseJSON(contents[0].content, []);
}
/**
* Get the book toc as an array of chapters (not nested).
*
* @param contents The module contents.
* @return The toc as a list.
*/
getTocList(contents: CoreCourseModuleContentFile[]): AddonModBookTocChapter[] {
// Convenience function to get chapter info.
const getChapterInfo = (
chapter: AddonModBookTocChapterParsed,
chapterNumber: number,
previousNumber: string = '',
): AddonModBookTocChapter => {
const hidden = !!parseInt(chapter.hidden, 10);
const fullChapterNumber = previousNumber + (hidden ? 'x.' : chapterNumber + '.');
return {
id: parseInt(chapter.href.replace('/index.html', ''), 10),
title: chapter.title,
level: chapter.level,
indexNumber: fullChapterNumber,
hidden: hidden,
};
};
const chapters: AddonModBookTocChapter[] = [];
const toc = this.getToc(contents);
let chapterNumber = 1;
toc.forEach((chapter) => {
const tocChapter = getChapterInfo(chapter, chapterNumber);
// Add the chapter to the list.
chapters.push(tocChapter);
if (chapter.subitems) {
let subChapterNumber = 1;
// Add all the subchapters to the list.
chapter.subitems.forEach((subChapter) => {
chapters.push(getChapterInfo(subChapter, subChapterNumber, tocChapter.indexNumber));
subChapterNumber++;
});
}
chapterNumber++;
});
return chapters;
}
/**
* Invalidates book data.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidateBookData(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getBookDataCacheKey(courseId));
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId Course ID of the module.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const promises: Promise<void>[] = [];
promises.push(this.invalidateBookData(courseId, siteId));
promises.push(CoreFilepool.instance.invalidateFilesByComponent(siteId, AddonModBookProvider.COMPONENT, moduleId));
promises.push(CoreCourse.instance.invalidateModule(moduleId, siteId));
return CoreUtils.instance.allPromises(promises);
}
/**
* Check if a file is downloadable. The file param must have a 'type' attribute like in core_course_get_contents response.
*
* @param file File to check.
* @return Whether it's downloadable.
*/
isFileDownloadable(file: CoreCourseModuleContentFile): boolean {
return file.type === 'file';
}
/**
* Return whether or not the plugin is enabled.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
*/
async isPluginEnabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return site.canDownloadFiles();
}
/**
* Report a book as being viewed.
*
* @param id Module ID.
* @param chapterId Chapter ID.
* @param name Name of the book.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModBookViewBookWSParams = {
bookid: id,
chapterid: chapterId,
};
return CoreCourseLogHelper.instance.logSingle(
'mod_book_view_book',
params,
AddonModBookProvider.COMPONENT,
id,
name,
'book',
{ chapterid: chapterId },
siteId,
);
}
}
export class AddonModBook extends makeSingleton(AddonModBookProvider) {}
/**
* A book chapter inside the toc list.
*/
export type AddonModBookTocChapter = {
id: number; // ID to identify the chapter.
title: string; // Chapter's title.
level: number; // The chapter's level.
hidden: boolean; // The chapter is hidden.
indexNumber: string; // The chapter's number'.
};
/**
* A book chapter parsed from JSON.
*/
type AddonModBookTocChapterParsed = {
title: string; // Chapter's title.
level: number; // The chapter's level.
hidden: string; // The chapter is hidden.
href: string;
subitems: AddonModBookTocChapterParsed[];
};
/**
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
* is identified by the relative path in the book, and the value is the URL of the file.
*/
export type AddonModBookContentsMap = {
[chapter: string]: {
indexUrl?: string;
paths: {[path: string]: string};
tags?: CoreTagItem[];
};
};
/**
* Book returned by mod_book_get_books_by_courses.
*/
export type AddonModBookBookWSData = {
id: number; // Book id.
coursemodule: number; // Course module id.
course: number; // Course id.
name: string; // Book name.
intro: string; // The Book intro.
introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles?: CoreWSExternalFile[]; // @since 3.2.
numbering: number; // Book numbering configuration.
navstyle: number; // Book navigation style configuration.
customtitles: number; // Book custom titles type.
revision?: number; // Book revision.
timecreated?: number; // Time of creation.
timemodified?: number; // Time of last modification.
section?: number; // Course section id.
visible?: boolean; // Visible.
groupmode?: number; // Group mode.
groupingid?: number; // Group id.
};
/**
* Params of mod_book_get_books_by_courses WS.
*/
type AddonModBookGetBooksByCoursesWSParams = {
courseids?: number[]; // Array of course ids.
};
/**
* Data returned by mod_book_get_books_by_courses WS.
*/
type AddonModBookGetBooksByCoursesWSResponse = {
books: AddonModBookBookWSData[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_book_view_book WS.
*/
type AddonModBookViewBookWSParams = {
bookid: number; // Book instance id.
chapterid?: number; // Chapter id.
};

View File

@ -0,0 +1,55 @@
// (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 { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to treat links to book.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModBookLinkHandler';
constructor() {
super('AddonModBook', 'book', 'b');
}
/**
* Get the mod params necessary to open an activity.
*
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @return List of params to pass to navigateToModule / navigateToModuleByInstance.
*/
getPageParams(url: string, params: Record<string, string>): Params {
return params.chapterid ? { chapterId: parseInt(params.chapterid, 10) } : {};
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
*
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled(siteId);
}
}
export class AddonModBookIndexLinkHandler extends makeSingleton(AddonModBookIndexLinkHandlerService) {}

View File

@ -0,0 +1,44 @@
// (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 { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to treat links to book list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModBookListLinkHandler';
constructor() {
super('AddonModBook', 'book');
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
}
export class AddonModBookListLinkHandler extends makeSingleton(AddonModBookListLinkHandlerService) {}

View File

@ -0,0 +1,94 @@
// (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 { Injectable, Type } from '@angular/core';
import { AddonModBookIndexComponent } from '../../components/index';
import { AddonModBook } from '../book';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreConstants } from '@/core/constants';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { makeSingleton } from '@singletons';
/**
* Handler to support book modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_book';
name = 'AddonModBook';
modName = 'book';
supportedFeatures = {
[CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
};
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
/**
* Get the data required to display the module in the course contents view.
*
* @param module The module object.
* @param courseId The course ID.
* @param sectionId The section ID.
* @return Data to render the module.
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_book-handler',
showDownloadButton: true,
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.instance.navigateToSitePath(AddonModBookModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
async getMainComponent(): Promise<Type<unknown> | undefined> {
return AddonModBookIndexComponent;
}
}
export class AddonModBookModuleHandler extends makeSingleton(AddonModBookModuleHandlerService) {}

View File

@ -0,0 +1,86 @@
// (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 { Injectable } from '@angular/core';
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { AddonModBook, AddonModBookProvider } from '../book';
/**
* Handler to prefetch books.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
name = 'AddonModBook';
modName = 'book';
component = AddonModBookProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$/;
/**
* Download or prefetch the content.
*
* @param module The module object returned by WS.
* @param courseId Course ID.
* @param prefetch True to prefetch, false to download right away.
* @return Promise resolved when all content is downloaded. Data returned is not reliable.
*/
async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise<void> {
const promises: Promise<unknown>[] = [];
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
// Ignore errors since this WS isn't available in some Moodle versions.
promises.push(CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(courseId, module.id)));
await Promise.all(promises);
}
/**
* Returns module intro files.
*
* @param module The module object returned by WS.
* @param courseId Course ID.
* @return Promise resolved with list of intro files.
*/
async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
const book = await CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(courseId, module.id));
return this.getIntroFilesFromInstance(module, book);
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when the data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModBook.instance.invalidateContent(moduleId, courseId);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
}
export class AddonModBookPrefetchHandler extends makeSingleton(AddonModBookPrefetchHandlerService) {}

View File

@ -0,0 +1,79 @@
// (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 { Injectable, Type } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreTagFeedComponent } from '@features/tag/components/feed/feed';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper';
import { CoreUrlUtils } from '@services/utils/url';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to support tags.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookTagAreaHandlerService implements CoreTagAreaHandler {
name = 'AddonModBookTagAreaHandler';
type = 'mod_book/book_chapters';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param content Rendered content.
* @return Area items (or promise resolved with the items).
*/
async parseContent(content: string): Promise<CoreTagFeedElement[]> {
const items = CoreTagHelper.instance.parseFeedContent(content);
// Find module ids of the returned books, they are needed by the link delegate.
await Promise.all(items.map((item) => {
const params = item.url ? CoreUrlUtils.instance.extractUrlParams(item.url) : {};
if (params.b && !params.id) {
const bookId = parseInt(params.b, 10);
return CoreCourse.instance.getModuleBasicInfoByInstance(bookId, 'book').then((module) => {
item.url += '&id=' + module.id;
return;
});
}
}));
return items;
}
/**
* Get the component to use to display items.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return CoreTagFeedComponent;
}
}
export class AddonModBookTagAreaHandler extends makeSingleton(AddonModBookTagAreaHandlerService) {}

View File

@ -14,11 +14,13 @@
import { NgModule } from '@angular/core';
import { AddonModBookModule } from './book/book.module';
import { AddonModLessonModule } from './lesson/lesson.module';
@NgModule({
declarations: [],
imports: [
AddonModBookModule,
AddonModLessonModule,
],
providers: [],

View File

@ -77,12 +77,18 @@ export class CoreConstants {
static readonly OUTDATED = 'outdated';
static readonly NOT_DOWNLOADABLE = 'notdownloadable';
// Download / prefetch status icon. @todo
static readonly DOWNLOADED_ICON = 'cloud-done';
static readonly DOWNLOADING_ICON = 'spinner';
static readonly NOT_DOWNLOADED_ICON = 'cloud-download';
static readonly OUTDATED_ICON = 'fas-redo-alt';
static readonly NOT_DOWNLOADABLE_ICON = '';
// General download and sync icons.
static readonly ICON_LOADING = 'spinner';
static readonly ICON_REFRESH = 'fas-redo-alt';
static readonly ICON_SYNC = 'fas-sync-alt';
// Constants from Moodle's resourcelib.
static readonly RESOURCELIB_DISPLAY_AUTO = 0; // Try the best way.
static readonly RESOURCELIB_DISPLAY_EMBED = 1; // Display using object tag.

View File

@ -58,7 +58,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
// Data for context menu.
externalUrl?: string; // External URL to open in browser.
description?: string; // Module description.
refreshIcon = 'spinner'; // Refresh icon, normally spinner or refresh.
refreshIcon = CoreConstants.ICON_LOADING; // Refresh icon, normally spinner or refresh.
prefetchStatusIcon?: string; // Used when calling fillContextMenu.
prefetchStatus?: string; // Used when calling fillContextMenu.
prefetchText?: string; // Used when calling fillContextMenu.
@ -132,14 +132,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
return;
}
this.refreshIcon = 'spinner';
this.refreshIcon = CoreConstants.ICON_LOADING;
try {
await CoreUtils.instance.ignoreErrors(this.invalidateContent());
await this.loadContent(true);
} finally {
this.refreshIcon = 'fas-redo';
this.refreshIcon = CoreConstants.ICON_REFRESH;
}
}
@ -181,7 +181,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
} finally {
this.loaded = true;
this.refreshIcon = 'fas-redo';
this.refreshIcon = CoreConstants.ICON_REFRESH;
}
}

View File

@ -64,9 +64,9 @@ export class CoreH5PFramework {
const db = await CoreSites.instance.getSiteDb(siteId);
const whereAndParams = db.getInOrEqual(libraryIds);
whereAndParams[0] = 'mainlibraryid ' + whereAndParams[0];
whereAndParams.sql = 'mainlibraryid ' + whereAndParams.sql;
await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams[0], whereAndParams[1]);
await db.updateRecordsWhere(CONTENT_TABLE_NAME, { filtered: null }, whereAndParams.sql, whereAndParams.params);
}
/**
@ -919,4 +919,3 @@ type LibraryDependency = {
type LibraryAddonDBData = Omit<CoreH5PLibraryAddonData, 'addTo'> & {
addTo: string;
};

View File

@ -2196,15 +2196,16 @@ export class CoreFilepoolProvider {
}
const fileIds = items.map((item) => item.fileId);
const whereAndParams = db.getInOrEqual(fileIds);
whereAndParams[0] = 'fileId ' + whereAndParams[0];
whereAndParams.sql = 'fileId ' + whereAndParams.sql;
if (onlyUnknown) {
whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
whereAndParams.sql += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
}
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams[0], whereAndParams[1]);
await db.updateRecordsWhere(FILES_TABLE_NAME, { stale: 1 }, whereAndParams.sql, whereAndParams.params);
}
/**