diff --git a/src/addons/mod/book/book-lazy.module.ts b/src/addons/mod/book/book-lazy.module.ts new file mode 100644 index 000000000..cdaec7474 --- /dev/null +++ b/src/addons/mod/book/book-lazy.module.ts @@ -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 {} diff --git a/src/addons/mod/book/book.module.ts b/src/addons/mod/book/book.module.ts new file mode 100644 index 000000000..af8ec5278 --- /dev/null +++ b/src/addons/mod/book/book.module.ts @@ -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 {} diff --git a/src/addons/mod/book/components/components.module.ts b/src/addons/mod/book/components/components.module.ts new file mode 100644 index 000000000..4d59d249d --- /dev/null +++ b/src/addons/mod/book/components/components.module.ts @@ -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 {} diff --git a/src/addons/mod/book/components/index/addon-mod-book-index.html b/src/addons/mod/book/components/index/addon-mod-book-index.html new file mode 100644 index 000000000..0aaef2eb6 --- /dev/null +++ b/src/addons/mod/book/components/index/addon-mod-book-index.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ {{ 'core.tag.tags' | translate }}: + +
+ + +
+ +
diff --git a/src/addons/mod/book/components/index/index.ts b/src/addons/mod/book/components/index/index.ts new file mode 100644 index 000000000..3951b9242 --- /dev/null +++ b/src/addons/mod/book/components/index/index.ts @@ -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 { + super.ngOnInit(); + + this.tagsEnabled = CoreTag.instance.areTagsAvailableInSite(); + this.loadContent(); + } + + /** + * Show the TOC. + */ + async showToc(): Promise { + // 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 { + 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 { + const promises: Promise[] = []; + 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 { + 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; + } + } + +} diff --git a/src/addons/mod/book/components/toc/toc.html b/src/addons/mod/book/components/toc/toc.html new file mode 100644 index 000000000..b54fbbf32 --- /dev/null +++ b/src/addons/mod/book/components/toc/toc.html @@ -0,0 +1,32 @@ + + + + + + {{ 'addon.mod_book.toc' | translate }} + + + + + + + + + + diff --git a/src/addons/mod/book/components/toc/toc.scss b/src/addons/mod/book/components/toc/toc.scss new file mode 100644 index 000000000..199375cef --- /dev/null +++ b/src/addons/mod/book/components/toc/toc.scss @@ -0,0 +1,5 @@ +.addon-mod-book-bullet { + font-weight: bold; + font-size: 1.5em; + margin-right: 3px; +} diff --git a/src/addons/mod/book/components/toc/toc.ts b/src/addons/mod/book/components/toc/toc.ts new file mode 100644 index 000000000..d83c59e25 --- /dev/null +++ b/src/addons/mod/book/components/toc/toc.ts @@ -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(); + } + +} diff --git a/src/addons/mod/book/lang.json b/src/addons/mod/book/lang.json new file mode 100644 index 000000000..200e96ce1 --- /dev/null +++ b/src/addons/mod/book/lang.json @@ -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" +} \ No newline at end of file diff --git a/src/addons/mod/book/pages/index/index.html b/src/addons/mod/book/pages/index/index.html new file mode 100644 index 000000000..afea617fb --- /dev/null +++ b/src/addons/mod/book/pages/index/index.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/addons/mod/book/pages/index/index.module.ts b/src/addons/mod/book/pages/index/index.module.ts new file mode 100644 index 000000000..cc22a2160 --- /dev/null +++ b/src/addons/mod/book/pages/index/index.module.ts @@ -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 {} diff --git a/src/addons/mod/book/pages/index/index.ts b/src/addons/mod/book/pages/index/index.ts new file mode 100644 index 000000000..102fd5a94 --- /dev/null +++ b/src/addons/mod/book/pages/index/index.ts @@ -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; + } + +} diff --git a/src/addons/mod/book/services/book.ts b/src/addons/mod/book/services/book.ts new file mode 100644 index 000000000..bc27c60b2 --- /dev/null +++ b/src/addons/mod/book/services/book.ts @@ -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 { + 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 { + + 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 { + + 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 { + 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 { + siteId = siteId || CoreSites.instance.getCurrentSiteId(); + + const promises: Promise[] = []; + + 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 { + 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 { + 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. +}; diff --git a/src/addons/mod/book/services/handlers/index-link.ts b/src/addons/mod/book/services/handlers/index-link.ts new file mode 100644 index 000000000..cb1d39657 --- /dev/null +++ b/src/addons/mod/book/services/handlers/index-link.ts @@ -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): 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 { + return AddonModBook.instance.isPluginEnabled(siteId); + } + +} + +export class AddonModBookIndexLinkHandler extends makeSingleton(AddonModBookIndexLinkHandlerService) {} diff --git a/src/addons/mod/book/services/handlers/list-link.ts b/src/addons/mod/book/services/handlers/list-link.ts new file mode 100644 index 000000000..96903dc74 --- /dev/null +++ b/src/addons/mod/book/services/handlers/list-link.ts @@ -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 { + return AddonModBook.instance.isPluginEnabled(); + } + +} + +export class AddonModBookListLinkHandler extends makeSingleton(AddonModBookListLinkHandlerService) {} diff --git a/src/addons/mod/book/services/handlers/module.ts b/src/addons/mod/book/services/handlers/module.ts new file mode 100644 index 000000000..224fb766f --- /dev/null +++ b/src/addons/mod/book/services/handlers/module.ts @@ -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 { + 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 | undefined> { + return AddonModBookIndexComponent; + } + +} +export class AddonModBookModuleHandler extends makeSingleton(AddonModBookModuleHandlerService) {} diff --git a/src/addons/mod/book/services/handlers/prefetch.ts b/src/addons/mod/book/services/handlers/prefetch.ts new file mode 100644 index 000000000..bc8475fe5 --- /dev/null +++ b/src/addons/mod/book/services/handlers/prefetch.ts @@ -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 { + const promises: Promise[] = []; + + 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 { + 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 { + 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 { + return AddonModBook.instance.isPluginEnabled(); + } + +} + +export class AddonModBookPrefetchHandler extends makeSingleton(AddonModBookPrefetchHandlerService) {} diff --git a/src/addons/mod/book/services/handlers/tag-area.ts b/src/addons/mod/book/services/handlers/tag-area.ts new file mode 100644 index 000000000..e416707ee --- /dev/null +++ b/src/addons/mod/book/services/handlers/tag-area.ts @@ -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 { + 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 { + 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 | Promise> { + return CoreTagFeedComponent; + } + +} + +export class AddonModBookTagAreaHandler extends makeSingleton(AddonModBookTagAreaHandlerService) {} diff --git a/src/addons/mod/mod.module.ts b/src/addons/mod/mod.module.ts index fad22f4d3..92edb902e 100644 --- a/src/addons/mod/mod.module.ts +++ b/src/addons/mod/mod.module.ts @@ -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: [], diff --git a/src/core/constants.ts b/src/core/constants.ts index 0f43c62e3..f1e6723fc 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -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. diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts index b6647d3a3..92fc58eb9 100644 --- a/src/core/features/course/classes/main-resource-component.ts +++ b/src/core/features/course/classes/main-resource-component.ts @@ -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; } } diff --git a/src/core/features/h5p/classes/framework.ts b/src/core/features/h5p/classes/framework.ts index d10eeafcd..0e966a259 100644 --- a/src/core/features/h5p/classes/framework.ts +++ b/src/core/features/h5p/classes/framework.ts @@ -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 & { addTo: string; }; - diff --git a/src/core/services/filepool.ts b/src/core/services/filepool.ts index 211bc7107..46cf7ce2e 100644 --- a/src/core/services/filepool.ts +++ b/src/core/services/filepool.ts @@ -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); } /**