From 5a15fca0a9acbf7cae2dd291da872b22c9411935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Feb 2021 15:45:55 +0100 Subject: [PATCH 1/5] MOBILE-3637 core: Add tag list and navigation bar components --- src/core/components/components.module.ts | 3 + .../navigation-bar/core-navigation-bar.html | 25 ++++++++ .../navigation-bar/navigation-bar.scss | 12 ++++ .../navigation-bar/navigation-bar.ts | 59 +++++++++++++++++++ .../tag/components/components.module.ts | 3 + .../tag/components/list/core-tag-list.html | 3 + .../features/tag/components/list/list.scss | 7 +++ src/core/features/tag/components/list/list.ts | 47 +++++++++++++++ 8 files changed, 159 insertions(+) create mode 100644 src/core/components/navigation-bar/core-navigation-bar.html create mode 100644 src/core/components/navigation-bar/navigation-bar.scss create mode 100644 src/core/components/navigation-bar/navigation-bar.ts create mode 100644 src/core/features/tag/components/list/core-tag-list.html create mode 100644 src/core/features/tag/components/list/list.scss create mode 100644 src/core/features/tag/components/list/list.ts diff --git a/src/core/components/components.module.ts b/src/core/components/components.module.ts index d99184c52..9b0bcd00c 100644 --- a/src/core/components/components.module.ts +++ b/src/core/components/components.module.ts @@ -44,6 +44,7 @@ import { CoreDynamicComponent } from './dynamic-component/dynamic-component'; import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons'; import { CoreSendMessageFormComponent } from './send-message-form/send-message-form'; import { CoreTimerComponent } from './timer/timer'; +import { CoreNavigationBarComponent } from './navigation-bar/navigation-bar'; import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; @@ -76,6 +77,7 @@ import { CorePipesModule } from '@pipes/pipes.module'; CoreDynamicComponent, CoreSendMessageFormComponent, CoreTimerComponent, + CoreNavigationBarComponent, ], imports: [ CommonModule, @@ -112,6 +114,7 @@ import { CorePipesModule } from '@pipes/pipes.module'; CoreDynamicComponent, CoreSendMessageFormComponent, CoreTimerComponent, + CoreNavigationBarComponent, ], }) export class CoreComponentsModule {} diff --git a/src/core/components/navigation-bar/core-navigation-bar.html b/src/core/components/navigation-bar/core-navigation-bar.html new file mode 100644 index 000000000..3fdb8a357 --- /dev/null +++ b/src/core/components/navigation-bar/core-navigation-bar.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/core/components/navigation-bar/navigation-bar.scss b/src/core/components/navigation-bar/navigation-bar.scss new file mode 100644 index 000000000..c0219d1ac --- /dev/null +++ b/src/core/components/navigation-bar/navigation-bar.scss @@ -0,0 +1,12 @@ +.core-navigation-bar-arrow { + text-transform: none; + max-width: 100%; + ion-icon { + flex-shrink: 0; + } + + core-format-text { + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/src/core/components/navigation-bar/navigation-bar.ts b/src/core/components/navigation-bar/navigation-bar.ts new file mode 100644 index 000000000..9d7eb2ddd --- /dev/null +++ b/src/core/components/navigation-bar/navigation-bar.ts @@ -0,0 +1,59 @@ +// (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, EventEmitter, Input, Output } from '@angular/core'; +import { CoreTextUtils } from '@services/utils/text'; + +/** + * Component to show a "bar" with arrows to navigate forward/backward and a "info" icon to display more data. + * + * This directive will show two arrows at the left and right of the screen to navigate to previous/next item when clicked. + * If no previous/next item is defined, that arrow won't be shown. It will also show a button to show more info. + * + * Example usage: + * + */ +@Component({ + selector: 'core-navigation-bar', + templateUrl: 'core-navigation-bar.html', + styleUrls: ['navigation-bar.scss'], +}) +export class CoreNavigationBarComponent { + + @Input() previous?: unknown; // Previous item. If not defined, the previous arrow won't be shown. + @Input() previousTitle?: string; // Previous item title. If not defined, only the arrow will be shown. + @Input() next?: unknown; // Next item. If not defined, the next arrow won't be shown. + @Input() nextTitle?: string; // Next item title. If not defined, only the arrow will be shown. + @Input() info = ''; // Info to show when clicking the info button. If not defined, the info button won't be shown. + @Input() title = ''; // Title to show when seeing the info (new page). + @Input() component?: string; // Component the bar belongs to. + @Input() componentId?: number; // Component ID. + @Input() contextLevel?: string; // The context level. + @Input() contextInstanceId?: number; // The instance ID related to the context. + @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. + @Output() action?: EventEmitter = + new EventEmitter(); // Function to call when arrow is clicked. Will receive as a param the item to load. + + showInfo(): void { + CoreTextUtils.instance.viewText(this.title, this.info, { + component: this.component, + componentId: this.componentId, + filter: true, + contextLevel: this.contextLevel, + instanceId: this.contextInstanceId, + courseId: this.courseId, + }); + } + +} diff --git a/src/core/features/tag/components/components.module.ts b/src/core/features/tag/components/components.module.ts index 595a5d3e3..9f3aa671e 100644 --- a/src/core/features/tag/components/components.module.ts +++ b/src/core/features/tag/components/components.module.ts @@ -19,10 +19,12 @@ import { TranslateModule } from '@ngx-translate/core'; import { CoreTagFeedComponent } from './feed/feed'; import { CoreSharedModule } from '@/core/shared.module'; +import { CoreTagListComponent } from './list/list'; @NgModule({ declarations: [ CoreTagFeedComponent, + CoreTagListComponent, ], imports: [ CommonModule, @@ -34,6 +36,7 @@ import { CoreSharedModule } from '@/core/shared.module'; ], exports: [ CoreTagFeedComponent, + CoreTagListComponent, ], }) export class CoreTagComponentsModule {} diff --git a/src/core/features/tag/components/list/core-tag-list.html b/src/core/features/tag/components/list/core-tag-list.html new file mode 100644 index 000000000..7e6372e20 --- /dev/null +++ b/src/core/features/tag/components/list/core-tag-list.html @@ -0,0 +1,3 @@ + + {{ tag.rawname }} + diff --git a/src/core/features/tag/components/list/list.scss b/src/core/features/tag/components/list/list.scss new file mode 100644 index 000000000..e73e7e8fc --- /dev/null +++ b/src/core/features/tag/components/list/list.scss @@ -0,0 +1,7 @@ +:host { + line-height: 1.6; + + ion-badge { + cursor: pointer; + } +} diff --git a/src/core/features/tag/components/list/list.ts b/src/core/features/tag/components/list/list.ts new file mode 100644 index 000000000..97d4ce83b --- /dev/null +++ b/src/core/features/tag/components/list/list.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 { Component, Input } from '@angular/core'; +import { CoreTagItem } from '@features/tag/services/tag'; +import { Params } from '@angular/router'; +import { CoreNavigator } from '@services/navigator'; + +/** + * Component that displays the list of tags of an item. + */ +@Component({ + selector: 'core-tag-list', + templateUrl: 'core-tag-list.html', + styleUrls: ['list.scss'], +}) +export class CoreTagListComponent { + + @Input() tags: CoreTagItem[] = []; + + /** + * Go to tag index page. + */ + openTag(tag: CoreTagItem): void { + const params: Params = { + tagId: tag.id, + tagName: tag.rawname, + collectionId: tag.tagcollid, + fromContextId: tag.taginstancecontextid, + }; + + // @todo: Check split view to navigate on the outlet if any. + CoreNavigator.instance.navigate('/tag/index', { params }); + } + +} From c98fa810fa70e02623d160765e20dbe74bac7134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Feb 2021 14:29:11 +0100 Subject: [PATCH 2/5] MOBILE-3637 book: Implement book activity --- src/addons/mod/book/book-lazy.module.ts | 28 + src/addons/mod/book/book.module.ts | 57 +++ .../mod/book/components/components.module.ts | 47 ++ .../index/addon-mod-book-index.html | 52 ++ src/addons/mod/book/components/index/index.ts | 251 +++++++++ src/addons/mod/book/components/toc/toc.html | 32 ++ src/addons/mod/book/components/toc/toc.scss | 5 + src/addons/mod/book/components/toc/toc.ts | 66 +++ src/addons/mod/book/lang.json | 8 + src/addons/mod/book/pages/index/index.html | 23 + .../mod/book/pages/index/index.module.ts | 46 ++ src/addons/mod/book/pages/index/index.ts | 57 +++ src/addons/mod/book/services/book.ts | 479 ++++++++++++++++++ .../mod/book/services/handlers/index-link.ts | 55 ++ .../mod/book/services/handlers/list-link.ts | 44 ++ .../mod/book/services/handlers/module.ts | 94 ++++ .../mod/book/services/handlers/prefetch.ts | 86 ++++ .../mod/book/services/handlers/tag-area.ts | 79 +++ src/addons/mod/mod.module.ts | 2 + src/core/constants.ts | 6 + .../course/classes/main-resource-component.ts | 8 +- src/core/features/h5p/classes/framework.ts | 5 +- src/core/services/filepool.ts | 7 +- 23 files changed, 1527 insertions(+), 10 deletions(-) create mode 100644 src/addons/mod/book/book-lazy.module.ts create mode 100644 src/addons/mod/book/book.module.ts create mode 100644 src/addons/mod/book/components/components.module.ts create mode 100644 src/addons/mod/book/components/index/addon-mod-book-index.html create mode 100644 src/addons/mod/book/components/index/index.ts create mode 100644 src/addons/mod/book/components/toc/toc.html create mode 100644 src/addons/mod/book/components/toc/toc.scss create mode 100644 src/addons/mod/book/components/toc/toc.ts create mode 100644 src/addons/mod/book/lang.json create mode 100644 src/addons/mod/book/pages/index/index.html create mode 100644 src/addons/mod/book/pages/index/index.module.ts create mode 100644 src/addons/mod/book/pages/index/index.ts create mode 100644 src/addons/mod/book/services/book.ts create mode 100644 src/addons/mod/book/services/handlers/index-link.ts create mode 100644 src/addons/mod/book/services/handlers/list-link.ts create mode 100644 src/addons/mod/book/services/handlers/module.ts create mode 100644 src/addons/mod/book/services/handlers/prefetch.ts create mode 100644 src/addons/mod/book/services/handlers/tag-area.ts 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); } /** From d3e8b969464b3bf1cfbd02121f5e8fbc47e90e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Feb 2021 14:50:09 +0100 Subject: [PATCH 3/5] MOBILE-3637 core: Unify refresh and sync icons --- src/addons/calendar/pages/day/day.page.ts | 9 +++--- src/addons/calendar/pages/event/event.page.ts | 9 +++--- src/addons/calendar/pages/index/index.page.ts | 9 +++--- src/addons/calendar/pages/list/list.page.ts | 8 +++--- .../pages/discussion/discussion.page.ts | 15 +++++----- .../index/addon-mod-lesson-index.html | 2 +- .../mod/lesson/components/index/index.ts | 8 +++--- .../context-menu/context-menu-popover.ts | 3 +- src/core/constants.ts | 12 ++++---- .../course/classes/main-activity-component.ts | 27 +++++++++--------- .../course/pages/contents/contents.ts | 5 ++-- .../features/course/services/course-helper.ts | 28 +++++++++---------- .../courses/pages/my-courses/my-courses.ts | 2 +- src/core/features/settings/constants.ts | 4 ++- 14 files changed, 75 insertions(+), 66 deletions(-) diff --git a/src/addons/calendar/pages/day/day.page.ts b/src/addons/calendar/pages/day/day.page.ts index 80d7657bd..c680e52fe 100644 --- a/src/addons/calendar/pages/day/day.page.ts +++ b/src/addons/calendar/pages/day/day.page.ts @@ -40,6 +40,7 @@ import { CoreNavigator } from '@services/navigator'; import { Params } from '@angular/router'; import { Subscription } from 'rxjs'; import { CoreUtils } from '@services/utils/utils'; +import { CoreConstants } from '@/core/constants'; /** * Page that displays the calendar events for a certain day. @@ -85,7 +86,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { loaded = false; hasOffline = false; isOnline = false; - syncIcon = 'spinner'; + syncIcon = CoreConstants.ICON_LOADING; isCurrentDay = false; isPastDay = false; currentMoment!: moment.Moment; @@ -260,7 +261,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { */ async fetchData(sync?: boolean): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; this.isOnline = CoreApp.instance.isOnline(); if (sync) { @@ -320,7 +321,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { } this.loaded = true; - this.syncIcon = 'fas-sync-alt'; + this.syncIcon = CoreConstants.ICON_SYNC; } /** @@ -450,7 +451,7 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async refreshData(sync?: boolean, afterChange?: boolean): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; const promises: Promise[] = []; diff --git a/src/addons/calendar/pages/event/event.page.ts b/src/addons/calendar/pages/event/event.page.ts index 3d3c9e7a2..eced37428 100644 --- a/src/addons/calendar/pages/event/event.page.ts +++ b/src/addons/calendar/pages/event/event.page.ts @@ -45,6 +45,7 @@ import { CoreUtils } from '@services/utils/utils'; import { AddonCalendarReminderDBRecord } from '../../services/database/calendar'; import { ActivatedRoute } from '@angular/router'; import { CoreScreen } from '@services/screen'; +import { CoreConstants } from '@/core/constants'; /** * Page that displays a single calendar event. @@ -84,7 +85,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { canDelete = false; hasOffline = false; isOnline = false; - syncIcon = 'spinner'; // Sync icon. + syncIcon = CoreConstants.ICON_LOADING; // Sync icon. isSplitViewOn = false; constructor( @@ -163,7 +164,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } this.eventId = eventId; - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; this.fetchEvent(); }); @@ -338,7 +339,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { } this.eventLoaded = true; - this.syncIcon = 'fas-sync-alt'; + this.syncIcon = CoreConstants.ICON_SYNC; } /** @@ -417,7 +418,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async refreshEvent(sync = false, showErrors = false): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; const promises: Promise[] = []; diff --git a/src/addons/calendar/pages/index/index.page.ts b/src/addons/calendar/pages/index/index.page.ts index c5fdf8e5f..600dc8369 100644 --- a/src/addons/calendar/pages/index/index.page.ts +++ b/src/addons/calendar/pages/index/index.page.ts @@ -32,6 +32,7 @@ import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming- import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter'; import { CoreNavigator } from '@services/navigator'; import { CoreLocalNotifications } from '@services/local-notifications'; +import { CoreConstants } from '@/core/constants'; /** @@ -68,7 +69,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { loaded = false; hasOffline = false; isOnline = false; - syncIcon = 'spinner'; + syncIcon = CoreConstants.ICON_LOADING; showCalendar = true; loadUpcoming = false; filter: AddonCalendarFilter = { @@ -194,7 +195,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { */ async fetchData(sync?: boolean, showErrors?: boolean): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; this.isOnline = CoreApp.instance.isOnline(); if (sync) { @@ -254,7 +255,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { } this.loaded = true; - this.syncIcon = 'fas-sync-alt'; + this.syncIcon = CoreConstants.ICON_SYNC; } /** @@ -285,7 +286,7 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async refreshData(sync = false, showErrors = false): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; const promises: Promise[] = []; diff --git a/src/addons/calendar/pages/list/list.page.ts b/src/addons/calendar/pages/list/list.page.ts index 3e2dbb71c..9eb4f0936 100644 --- a/src/addons/calendar/pages/list/list.page.ts +++ b/src/addons/calendar/pages/list/list.page.ts @@ -89,7 +89,7 @@ export class AddonCalendarListPage implements OnInit, OnDestroy { canCreate = false; hasOffline = false; isOnline = false; - syncIcon = 'spinner'; + syncIcon = CoreConstants.ICON_LOADING; filter: AddonCalendarFilter = { filtered: false, courseId: -1, @@ -251,7 +251,7 @@ export class AddonCalendarListPage implements OnInit, OnDestroy { this.gotoEvent(this.eventId); } - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; await this.fetchData(false, true, false); @@ -361,7 +361,7 @@ export class AddonCalendarListPage implements OnInit, OnDestroy { } this.eventsLoaded = true; - this.syncIcon = 'fas-sync-alt'; + this.syncIcon = CoreConstants.ICON_SYNC; } /** @@ -567,7 +567,7 @@ export class AddonCalendarListPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ async refreshEvents(sync?: boolean, showErrors?: boolean): Promise { - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; const promises: Promise[] = []; diff --git a/src/addons/messages/pages/discussion/discussion.page.ts b/src/addons/messages/pages/discussion/discussion.page.ts index ecbe47ef8..99c3de9fd 100644 --- a/src/addons/messages/pages/discussion/discussion.page.ts +++ b/src/addons/messages/pages/discussion/discussion.page.ts @@ -49,6 +49,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreIonLoadingElement } from '@classes/ion-loading'; import { ActivatedRoute } from '@angular/router'; import { AddonMessagesConversationInfoComponent } from '../../components/conversation-info/conversation-info'; +import { CoreConstants } from '@/core/constants'; /** * Page that displays a message discussion page. @@ -1352,7 +1353,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView return; } - this.favouriteIcon = 'spinner'; + this.favouriteIcon = CoreConstants.ICON_LOADING; try { await AddonMessages.instance.setFavouriteConversation(this.conversation.id, !this.conversation.isfavourite); @@ -1386,7 +1387,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView return; } - this.muteIcon = 'spinner'; + this.muteIcon = CoreConstants.ICON_LOADING; try { await AddonMessages.instance.muteConversation(this.conversation.id, !this.conversation.ismuted); @@ -1461,7 +1462,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView try { await CoreDomUtils.instance.showConfirm(template, undefined, okText); - this.blockIcon = 'spinner'; + this.blockIcon = CoreConstants.ICON_LOADING; const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); this.showLoadingModal = true; @@ -1497,7 +1498,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView try { await CoreDomUtils.instance.showDeleteConfirm(confirmMessage); - this.deleteIcon = 'spinner'; + this.deleteIcon = CoreConstants.ICON_LOADING; try { try { @@ -1543,7 +1544,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView try { await CoreDomUtils.instance.showConfirm(template, undefined, okText); - this.blockIcon = 'spinner'; + this.blockIcon = CoreConstants.ICON_LOADING; const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); this.showLoadingModal = true; @@ -1582,7 +1583,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView try { await CoreDomUtils.instance.showConfirm(template, undefined, okText); - this.addRemoveIcon = 'spinner'; + this.addRemoveIcon = CoreConstants.ICON_LOADING; const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); this.showLoadingModal = true; @@ -1673,7 +1674,7 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView try { await CoreDomUtils.instance.showConfirm(template, undefined, okText); - this.addRemoveIcon = 'spinner'; + this.addRemoveIcon = CoreConstants.ICON_LOADING; const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true); this.showLoadingModal = true; diff --git a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html index f2b99aa64..e82d0d9c3 100644 --- a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html @@ -20,7 +20,7 @@ [iconAction]="prefetchStatusIcon" [closeOnClick]="false"> + iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false"> diff --git a/src/addons/mod/lesson/components/index/index.ts b/src/addons/mod/lesson/components/index/index.ts index 5b670b6a0..e9b68dc7c 100644 --- a/src/addons/mod/lesson/components/index/index.ts +++ b/src/addons/mod/lesson/components/index/index.ts @@ -624,8 +624,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo } this.loaded = false; - this.refreshIcon = 'spinner'; - this.syncIcon = 'spinner'; + this.refreshIcon = CoreConstants.ICON_LOADING; + this.syncIcon = CoreConstants.ICON_LOADING; try { await this.validatePassword( password); @@ -643,8 +643,8 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo CoreDomUtils.instance.showErrorModal(error); } finally { this.loaded = true; - this.refreshIcon = 'refresh'; - this.syncIcon = 'sync'; + this.refreshIcon = CoreConstants.ICON_REFRESH; + this.syncIcon = CoreConstants.ICON_SYNC; CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true, this.siteId); } diff --git a/src/core/components/context-menu/context-menu-popover.ts b/src/core/components/context-menu/context-menu-popover.ts index e6e6c5e7e..3a8373981 100644 --- a/src/core/components/context-menu/context-menu-popover.ts +++ b/src/core/components/context-menu/context-menu-popover.ts @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreConstants } from '@/core/constants'; import { Component } from '@angular/core'; import { NavParams } from '@ionic/angular'; import { PopoverController } from '@singletons'; @@ -58,7 +59,7 @@ export class CoreContextMenuPopoverComponent { event.preventDefault(); event.stopPropagation(); - if (item.iconAction == 'spinner') { + if (item.iconAction == CoreConstants.ICON_LOADING) { return false; } diff --git a/src/core/constants.ts b/src/core/constants.ts index f1e6723fc..b70e2ee0d 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -77,12 +77,12 @@ 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 = ''; + // Download / prefetch status icon. + static readonly ICON_DOWNLOADED = 'cloud-done'; + static readonly ICON_DOWNLOADING = 'spinner'; + static readonly ICON_NOT_DOWNLOADED = 'cloud-download'; + static readonly ICON_OUTDATED = 'fas-redo-alt'; + static readonly ICON_NOT_DOWNLOADABLE = ''; // General download and sync icons. static readonly ICON_LOADING = 'spinner'; diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts index 197010660..6d234b2ec 100644 --- a/src/core/features/course/classes/main-activity-component.ts +++ b/src/core/features/course/classes/main-activity-component.ts @@ -25,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreCourseContentsPage } from '../pages/contents/contents'; +import { CoreConstants } from '@/core/constants'; /** * Template class to easily create CoreCourseModuleMainComponent of activities. @@ -70,7 +71,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR super.ngOnInit(); this.hasOffline = false; - this.syncIcon = 'spinner'; + this.syncIcon = CoreConstants.ICON_LOADING; this.moduleName = CoreCourse.instance.translateModuleName(this.moduleName || ''); if (this.syncEventName) { @@ -117,16 +118,16 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR return; } - this.refreshIcon = 'spinner'; - this.syncIcon = 'spinner'; + this.refreshIcon = CoreConstants.ICON_LOADING; + this.syncIcon = CoreConstants.ICON_LOADING; try { await CoreUtils.instance.ignoreErrors(this.invalidateContent()); await this.loadContent(true, sync, showErrors); } finally { - this.refreshIcon = 'fas-redo'; - this.syncIcon = 'fas-sync'; + this.refreshIcon = CoreConstants.ICON_REFRESH; + this.syncIcon = CoreConstants.ICON_SYNC; } } @@ -138,16 +139,16 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR * @return Resolved when done. */ protected async showLoadingAndFetch(sync: boolean = false, showErrors: boolean = false): Promise { - this.refreshIcon = 'spinner'; - this.syncIcon = 'spinner'; + this.refreshIcon = CoreConstants.ICON_LOADING; + this.syncIcon = CoreConstants.ICON_LOADING; this.loaded = false; this.content?.scrollToTop(); try { await this.loadContent(false, sync, showErrors); } finally { - this.refreshIcon = 'fas-redo'; - this.syncIcon = 'fas-sync'; + this.refreshIcon = CoreConstants.ICON_REFRESH; + this.syncIcon = CoreConstants.ICON_REFRESH; } } @@ -159,8 +160,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR * @return Resolved when done. */ protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise { - this.refreshIcon = 'spinner'; - this.syncIcon = 'spinner'; + this.refreshIcon = CoreConstants.ICON_LOADING; + this.syncIcon = CoreConstants.ICON_LOADING; this.loaded = false; this.content?.scrollToTop(); @@ -207,8 +208,8 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true); } finally { this.loaded = true; - this.refreshIcon = 'fas-redo'; - this.syncIcon = 'fas-sync'; + this.refreshIcon = CoreConstants.ICON_REFRESH; + this.syncIcon = CoreConstants.ICON_REFRESH; } } diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index c67cabec1..b44c95803 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -45,6 +45,7 @@ import { CoreEventCompletionModuleViewedData, } from '@singletons/events'; import { CoreNavigator } from '@services/navigator'; +import { CoreConstants } from '@/core/constants'; /** * Page that displays the contents of a course. @@ -71,7 +72,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { displayEnableDownload = false; displayRefresher = false; prefetchCourseData: CorePrefetchStatusInfo = { - icon: 'spinner', + icon: CoreConstants.ICON_LOADING, statusTranslatable: 'core.course.downloadcourse', status: '', loading: true, @@ -171,7 +172,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { // Determine the course prefetch status. await this.determineCoursePrefetchIcon(); - if (this.prefetchCourseData.icon != 'spinner') { + if (this.prefetchCourseData.icon != CoreConstants.ICON_LOADING) { return; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index ee3d56cf7..ceeeaf0e3 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -359,7 +359,7 @@ export class CoreCourseHelperProvider { const siteId = CoreSites.instance.getCurrentSiteId(); data.downloadSucceeded = false; - data.icon = 'spinner'; + data.icon = CoreConstants.ICON_DOWNLOADING; data.statusTranslatable = 'core.downloading'; // Get the sections first if needed. @@ -563,7 +563,7 @@ export class CoreCourseHelperProvider { done?: () => void, ): Promise { const initialIcon = instance.prefetchStatusIcon; - instance.prefetchStatusIcon = 'spinner'; // Show spinner since this operation might take a while. + instance.prefetchStatusIcon = CoreConstants.ICON_DOWNLOADING; // Show spinner since this operation might take a while. try { // We need to call getDownloadSize, the package might have been updated. @@ -1122,7 +1122,7 @@ export class CoreCourseHelperProvider { if (prefetch.loading) { // It seems all courses are being downloaded, show a download button instead. - prefetch.icon = CoreConstants.NOT_DOWNLOADED_ICON; + prefetch.icon = CoreConstants.ICON_NOT_DOWNLOADED; } return prefetch; @@ -1188,14 +1188,14 @@ export class CoreCourseHelperProvider { prefetch: CorePrefetchStatusInfo, ): Promise { prefetch.loading = true; - prefetch.icon = CoreConstants.DOWNLOADING_ICON; + prefetch.icon = CoreConstants.ICON_DOWNLOADING; prefetch.badge = ''; try { await this.confirmAndPrefetchCourses(courses, (progress) => { prefetch.badge = progress.count + ' / ' + progress.total; }); - prefetch.icon = CoreConstants.OUTDATED_ICON; + prefetch.icon = CoreConstants.ICON_OUTDATED; } finally { prefetch.loading = false; prefetch.badge = ''; @@ -1264,19 +1264,19 @@ export class CoreCourseHelperProvider { */ getPrefetchStatusIcon(status: string, trustDownload: boolean = false): string { if (status == CoreConstants.NOT_DOWNLOADED) { - return CoreConstants.NOT_DOWNLOADED_ICON; + return CoreConstants.ICON_NOT_DOWNLOADED; } if (status == CoreConstants.OUTDATED || (status == CoreConstants.DOWNLOADED && !trustDownload)) { - return CoreConstants.OUTDATED_ICON; + return CoreConstants.ICON_OUTDATED; } if (status == CoreConstants.DOWNLOADED && trustDownload) { - return CoreConstants.DOWNLOADED_ICON; + return CoreConstants.ICON_DOWNLOADED; } if (status == CoreConstants.DOWNLOADING) { - return CoreConstants.DOWNLOADING_ICON; + return CoreConstants.ICON_DOWNLOADING; } - return CoreConstants.DOWNLOADING_ICON; + return CoreConstants.ICON_DOWNLOADING; } /** @@ -1335,17 +1335,17 @@ export class CoreCourseHelperProvider { moduleInfo.status = results[1]; switch (results[1]) { case CoreConstants.NOT_DOWNLOADED: - moduleInfo.statusIcon = 'fas-cloud-download-alt'; + moduleInfo.statusIcon = CoreConstants.ICON_NOT_DOWNLOADED; break; case CoreConstants.DOWNLOADING: - moduleInfo.statusIcon = 'spinner'; + moduleInfo.statusIcon = CoreConstants.ICON_DOWNLOADING; break; case CoreConstants.OUTDATED: - moduleInfo.statusIcon = 'fas-redo'; + moduleInfo.statusIcon = CoreConstants.ICON_OUTDATED; break; case CoreConstants.DOWNLOADED: if (!CoreCourseModulePrefetchDelegate.instance.canCheckUpdates()) { - moduleInfo.statusIcon = 'fas-redo'; + moduleInfo.statusIcon = CoreConstants.ICON_OUTDATED; } break; default: diff --git a/src/core/features/courses/pages/my-courses/my-courses.ts b/src/core/features/courses/pages/my-courses/my-courses.ts index 0e95d0a58..3f3b95fbd 100644 --- a/src/core/features/courses/pages/my-courses/my-courses.ts +++ b/src/core/features/courses/pages/my-courses/my-courses.ts @@ -45,7 +45,7 @@ export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy { filter = ''; showFilter = false; coursesLoaded = false; - downloadAllCoursesIcon = CoreConstants.NOT_DOWNLOADED_ICON; + downloadAllCoursesIcon = CoreConstants.ICON_NOT_DOWNLOADED; downloadAllCoursesLoading = false; downloadAllCoursesBadge = ''; downloadAllCoursesEnabled = false; diff --git a/src/core/features/settings/constants.ts b/src/core/features/settings/constants.ts index ce3ba0763..67b3eda4e 100644 --- a/src/core/features/settings/constants.ts +++ b/src/core/features/settings/constants.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreConstants } from '@/core/constants'; + /** * Settings section. */ @@ -40,7 +42,7 @@ export class CoreSettingsConstants { { name: 'synchronization', path: 'sync', - icon: 'fas-sync-alt', + icon: CoreConstants.ICON_SYNC, }, // @TODO sharedfiles { From 11dafc92c38490777712357b7f3c4c891114018a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Feb 2021 16:12:46 +0100 Subject: [PATCH 4/5] MOBILE-3637 lesson: Change path to be coherent with book --- .../lesson/components/index/addon-mod-lesson-index.html | 4 +++- src/addons/mod/lesson/lesson-lazy.module.ts | 7 +------ src/addons/mod/lesson/pages/user-retake/user-retake.html | 4 +++- src/addons/mod/lesson/services/handlers/module.ts | 5 +++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html index e82d0d9c3..ab0d8373c 100644 --- a/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html +++ b/src/addons/mod/lesson/components/index/addon-mod-lesson-index.html @@ -161,7 +161,9 @@ - {{groupOpt.name}} + + {{groupOpt.name}} + diff --git a/src/addons/mod/lesson/lesson-lazy.module.ts b/src/addons/mod/lesson/lesson-lazy.module.ts index 4b9bb7f6f..41c85cdd7 100644 --- a/src/addons/mod/lesson/lesson-lazy.module.ts +++ b/src/addons/mod/lesson/lesson-lazy.module.ts @@ -17,12 +17,7 @@ import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { - path: '', - redirectTo: 'index', - pathMatch: 'full', - }, - { - path: 'index', + path: ':courseId/:cmdId', loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModLessonIndexPageModule), }, { diff --git a/src/addons/mod/lesson/pages/user-retake/user-retake.html b/src/addons/mod/lesson/pages/user-retake/user-retake.html index a6fa046c7..7554d3ae1 100644 --- a/src/addons/mod/lesson/pages/user-retake/user-retake.html +++ b/src/addons/mod/lesson/pages/user-retake/user-retake.html @@ -111,7 +111,9 @@ - {{ answer[0].buttonText }} + + {{ answer[0].buttonText }} +

diff --git a/src/addons/mod/lesson/services/handlers/module.ts b/src/addons/mod/lesson/services/handlers/module.ts index 008da58cf..e2413c3a8 100644 --- a/src/addons/mod/lesson/services/handlers/module.ts +++ b/src/addons/mod/lesson/services/handlers/module.ts @@ -79,9 +79,10 @@ export class AddonModLessonModuleHandlerService implements CoreCourseModuleHandl action: (event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions) => { options = options || {}; options.params = options.params || {}; - Object.assign(options.params, { module, courseId }); + Object.assign(options.params, { module }); + const routeParams = '/' + courseId + '/' + module.id; - CoreNavigator.instance.navigateToSitePath(AddonModLessonModuleHandlerService.PAGE_NAME, options); + CoreNavigator.instance.navigateToSitePath(AddonModLessonModuleHandlerService.PAGE_NAME + routeParams, options); }, }; } From b3d380c9c2aa4b636df865af632cbb6fc4c2bd10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 9 Feb 2021 11:35:36 +0100 Subject: [PATCH 5/5] MOBILE-3637 core: Fix some navigation to tabs problems --- src/addons/calendar/calendar.module.ts | 2 ++ src/addons/privatefiles/privatefiles.module.ts | 6 +++++- src/core/features/mainmenu/pages/more/more.ts | 16 ++++++++++++---- src/core/features/tag/components/list/list.ts | 2 +- src/core/features/tag/tag.module.ts | 2 ++ src/core/services/navigator.ts | 5 +++++ 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/addons/calendar/calendar.module.ts b/src/addons/calendar/calendar.module.ts index ab5cff1f0..1a2d8e16d 100644 --- a/src/addons/calendar/calendar.module.ts +++ b/src/addons/calendar/calendar.module.ts @@ -28,6 +28,7 @@ import { CALENDAR_SITE_SCHEMA } from './services/database/calendar'; import { CALENDAR_OFFLINE_SITE_SCHEMA } from './services/database/calendar-offline'; import { AddonCalendarComponentsModule } from './components/components.module'; import { AddonCalendar } from './services/calendar'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; const mainMenuChildrenRoutes: Routes = [ { @@ -38,6 +39,7 @@ const mainMenuChildrenRoutes: Routes = [ @NgModule({ imports: [ + CoreMainMenuTabRoutingModule.forChild(mainMenuChildrenRoutes), CoreMainMenuRoutingModule.forChild({ children: mainMenuChildrenRoutes }), AddonCalendarComponentsModule, ], diff --git a/src/addons/privatefiles/privatefiles.module.ts b/src/addons/privatefiles/privatefiles.module.ts index 26e7073ab..3ea3bf1a7 100644 --- a/src/addons/privatefiles/privatefiles.module.ts +++ b/src/addons/privatefiles/privatefiles.module.ts @@ -18,6 +18,7 @@ import { Routes } from '@angular/router'; import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate'; import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module'; import { AddonPrivateFilesMainMenuHandler, AddonPrivateFilesMainMenuHandlerService } from './services/handlers/mainmenu'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; const routes: Routes = [ { @@ -27,7 +28,10 @@ const routes: Routes = [ ]; @NgModule({ - imports: [CoreMainMenuRoutingModule.forChild({ children: routes })], + imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), + CoreMainMenuRoutingModule.forChild({ children: routes }), + ], exports: [CoreMainMenuRoutingModule], providers: [ { diff --git a/src/core/features/mainmenu/pages/more/more.ts b/src/core/features/mainmenu/pages/more/more.ts index 8db63b6ec..e96ec3ff9 100644 --- a/src/core/features/mainmenu/pages/more/more.ts +++ b/src/core/features/mainmenu/pages/more/more.ts @@ -22,6 +22,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../services/mainmenu-delegate'; import { CoreMainMenu, CoreMainMenuCustomItem } from '../../services/mainmenu'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreNavigator } from '@services/navigator'; /** * Page that displays the main menu of the app. @@ -131,10 +132,12 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { * Open a handler. * * @param handler Handler to open. + * @todo: use subPage? */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars openHandler(handler: CoreMainMenuHandlerData): void { - // @todo + const params = handler.pageParams; + + CoreNavigator.instance.navigateToSitePath(handler.page, { params }); } /** @@ -142,9 +145,11 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { * * @param item Item to open. */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars openItem(item: CoreMainMenuCustomItem): void { - // @todo + // @todo CoreNavigator.instance.navigateToSitePath('CoreViewerIframePage', {title: item.label, url: item.url}); + + // eslint-disable-next-line no-console + console.error('openItem not implemented', item); } /** @@ -153,6 +158,9 @@ export class CoreMainMenuMorePage implements OnInit, OnDestroy { async scanQR(): Promise { // Scan for a QR code. // @todo + // eslint-disable-next-line no-console + console.error('scanQR not implemented'); + } /** diff --git a/src/core/features/tag/components/list/list.ts b/src/core/features/tag/components/list/list.ts index 97d4ce83b..23dac9ccf 100644 --- a/src/core/features/tag/components/list/list.ts +++ b/src/core/features/tag/components/list/list.ts @@ -41,7 +41,7 @@ export class CoreTagListComponent { }; // @todo: Check split view to navigate on the outlet if any. - CoreNavigator.instance.navigate('/tag/index', { params }); + CoreNavigator.instance.navigateToSitePath('/tag/index', { params, preferCurrentTab: false }); } } diff --git a/src/core/features/tag/tag.module.ts b/src/core/features/tag/tag.module.ts index 706f0a26c..fc46121d3 100644 --- a/src/core/features/tag/tag.module.ts +++ b/src/core/features/tag/tag.module.ts @@ -21,6 +21,7 @@ import { CoreTagMainMenuHandler, CoreTagMainMenuHandlerService } from './service import { CoreTagIndexLinkHandler } from './services/handlers/index-link'; import { CoreTagSearchLinkHandler } from './services/handlers/search-link'; import { CoreTagComponentsModule } from './components/components.module'; +import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module'; const routes: Routes = [ { @@ -31,6 +32,7 @@ const routes: Routes = [ @NgModule({ imports: [ + CoreMainMenuTabRoutingModule.forChild(routes), CoreMainMenuRoutingModule.forChild({ children: routes }), CoreTagComponentsModule, ], diff --git a/src/core/services/navigator.ts b/src/core/services/navigator.ts index 473807261..2e795d519 100644 --- a/src/core/services/navigator.ts +++ b/src/core/services/navigator.ts @@ -48,6 +48,7 @@ export type CoreNavigationOptions = { animated?: boolean; params?: Params; reset?: boolean; + preferCurrentTab?: boolean; // Default true. }; /** @@ -363,6 +364,10 @@ export class CoreNavigatorService { false, ); + if (options.preferCurrentTab === false && isMainMenuTab) { + return this.navigate(`/main/${path}`, options); + } + // Open the path within the current main tab. if (currentMainMenuTab && (!isMainMenuTab || pathRoot !== currentMainMenuTab)) { return this.navigate(`/main/${currentMainMenuTab}/${path}`, options);