Merge pull request #2674 from crazyserver/MOBILE-3637

Mobile 3637
main
Dani Palou 2021-02-09 15:31:31 +01:00 committed by GitHub
commit 2d19033bb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1796 additions and 90 deletions

View File

@ -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,
],

View File

@ -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<void> {
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<void> {
this.syncIcon = 'spinner';
this.syncIcon = CoreConstants.ICON_LOADING;
const promises: Promise<void>[] = [];

View File

@ -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<void> {
this.syncIcon = 'spinner';
this.syncIcon = CoreConstants.ICON_LOADING;
const promises: Promise<void>[] = [];

View File

@ -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<void> {
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<void> {
this.syncIcon = 'spinner';
this.syncIcon = CoreConstants.ICON_LOADING;
const promises: Promise<void>[] = [];

View File

@ -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<void> {
this.syncIcon = 'spinner';
this.syncIcon = CoreConstants.ICON_LOADING;
const promises: Promise<void>[] = [];

View File

@ -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;

View File

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

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
import { AddonModBookComponentsModule } from './components/components.module';
import { AddonModBookModuleHandler, AddonModBookModuleHandlerService } from './services/handlers/module';
import { AddonModBookIndexLinkHandler } from './services/handlers/index-link';
import { AddonModBookListLinkHandler } from './services/handlers/list-link';
import { AddonModBookPrefetchHandler } from './services/handlers/prefetch';
import { AddonModBookTagAreaHandler } from './services/handlers/tag-area';
const routes: Routes = [
{
path: AddonModBookModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./book-lazy.module').then(m => m.AddonModBookLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModBookComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.instance.registerHandler(AddonModBookModuleHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModBookIndexLinkHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModBookListLinkHandler.instance);
CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModBookPrefetchHandler.instance);
CoreTagAreaDelegate.instance.registerHandler(AddonModBookTagAreaHandler.instance);
},
},
],
})
export class AddonModBookModule {}

View File

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

View File

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

View File

@ -0,0 +1,251 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Optional, Input, OnInit } from '@angular/core';
import { IonContent } from '@ionic/angular';
import {
CoreCourseModuleMainResourceComponent, CoreCourseResourceDownloadResult,
} from '@features/course/classes/main-resource-component';
import {
AddonModBookProvider,
AddonModBookContentsMap,
AddonModBookTocChapter,
AddonModBookNavStyle,
AddonModBook,
AddonModBookBookWSData,
} from '../../services/book';
import { CoreTag, CoreTagItem } from '@features/tag/services/tag';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { ModalController, Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourse } from '@features/course/services/course';
import { AddonModBookTocComponent } from '../toc/toc';
import { CoreConstants } from '@/core/constants';
/**
* Component that displays a book.
*/
@Component({
selector: 'addon-mod-book-index',
templateUrl: 'addon-mod-book-index.html',
})
export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
@Input() initialChapterId?: number; // The initial chapter ID to load.
component = AddonModBookProvider.COMPONENT;
chapterContent?: string;
previousChapter?: AddonModBookTocChapter;
nextChapter?: AddonModBookTocChapter;
tagsEnabled = false;
displayNavBar = true;
previousNavBarTitle?: string;
nextNavBarTitle?: string;
warning = '';
tags?: CoreTagItem[];
protected chapters: AddonModBookTocChapter[] = [];
protected currentChapter?: number;
protected book?: AddonModBookBookWSData;
protected displayTitlesInNavBar = false;
protected contentsMap: AddonModBookContentsMap = {};
constructor(
protected content?: IonContent,
@Optional() courseContentsPage?: CoreCourseContentsPage,
) {
super('AddonModBookIndexComponent', courseContentsPage);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.tagsEnabled = CoreTag.instance.areTagsAvailableInSite();
this.loadContent();
}
/**
* Show the TOC.
*/
async showToc(): Promise<void> {
// Create the toc modal.
const modal = await ModalController.instance.create({
component: AddonModBookTocComponent,
componentProps: {
moduleId: this.module!.id,
chapters: this.chapters,
selected: this.currentChapter,
courseId: this.courseId,
book: this.book,
},
cssClass: 'core-modal-lateral',
showBackdrop: true,
backdropDismiss: true,
// @todo enterAnimation: 'core-modal-lateral-transition',
// @todo leaveAnimation: 'core-modal-lateral-transition',
});
await modal.present();
const result = await modal.onDidDismiss();
if (result.data) {
this.changeChapter(result.data);
}
}
/**
* Change the current chapter.
*
* @param chapterId Chapter to load.
* @return Promise resolved when done.
*/
changeChapter(chapterId: number): void {
if (chapterId && chapterId != this.currentChapter) {
this.loaded = false;
this.refreshIcon = CoreConstants.ICON_LOADING;
this.loadChapter(chapterId, true);
}
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected invalidateContent(): Promise<void> {
return AddonModBook.instance.invalidateContent(this.module!.id, this.courseId!);
}
/**
* Download book contents and load the current chapter.
*
* @param refresh Whether we're refreshing data.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh = false): Promise<void> {
const promises: Promise<void>[] = [];
let downloadResult: CoreCourseResourceDownloadResult | undefined;
// Try to get the book data. Ignore errors since this WS isn't available in some Moodle versions.
promises.push(CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(this.courseId!, this.module!.id))
.then((book) => {
if (!book) {
return;
}
this.book = book;
this.dataRetrieved.emit(book);
this.description = book.intro;
this.displayNavBar = book.navstyle != AddonModBookNavStyle.TOC_ONLY;
this.displayTitlesInNavBar = book.navstyle == AddonModBookNavStyle.TEXT;
return;
}));
// Get module status to determine if it needs to be downloaded.
promises.push(this.downloadResourceIfNeeded(refresh).then((result) => {
downloadResult = result;
return;
}));
try {
await Promise.all(promises);
this.contentsMap = AddonModBook.instance.getContentsMap(this.module!.contents);
this.chapters = AddonModBook.instance.getTocList(this.module!.contents);
if (typeof this.currentChapter == 'undefined' && typeof this.initialChapterId != 'undefined' && this.chapters) {
// Initial chapter set. Validate that the chapter exists.
const chapter = this.chapters.find((chapter) => chapter.id == this.initialChapterId);
if (chapter) {
this.currentChapter = this.initialChapterId;
}
}
if (typeof this.currentChapter == 'undefined') {
// Load the first chapter.
this.currentChapter = AddonModBook.instance.getFirstChapter(this.chapters);
}
// Show chapter.
try {
await this.loadChapter(this.currentChapter!, refresh);
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
} catch {
// Ignore errors, they're handled inside the loadChapter function.
}
} finally {
this.fillContextMenu(refresh);
}
}
/**
* Load a book chapter.
*
* @param chapterId Chapter to load.
* @param logChapterId Whether chapter ID should be passed to the log view function.
* @return Promise resolved when done.
*/
protected async loadChapter(chapterId: number, logChapterId: boolean): Promise<void> {
this.currentChapter = chapterId;
this.content?.scrollToTop();
try {
const content = await AddonModBook.instance.getChapterContent(this.contentsMap, chapterId, this.module!.id);
this.tags = this.tagsEnabled ? this.contentsMap[this.currentChapter].tags : [];
this.chapterContent = content;
this.previousChapter = AddonModBook.instance.getPreviousChapter(this.chapters, chapterId);
this.nextChapter = AddonModBook.instance.getNextChapter(this.chapters, chapterId);
this.previousNavBarTitle = this.previousChapter && this.displayTitlesInNavBar
? Translate.instance.instant('addon.mod_book.navprevtitle', { $a: this.previousChapter.title })
: '';
this.nextNavBarTitle = this.nextChapter && this.displayTitlesInNavBar
? Translate.instance.instant('addon.mod_book.navnexttitle', { $a: this.nextChapter.title })
: '';
// Chapter loaded, log view. We don't return the promise because we don't want to block the user for this.
await CoreUtils.instance.ignoreErrors(AddonModBook.instance.logView(
this.module!.instance!,
logChapterId ? chapterId : undefined,
this.module!.name,
));
// Module is completed when last chapter is viewed, so we only check completion if the last is reached.
if (!this.nextChapter) {
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_book.errorchapter', true);
throw error;
} finally {
this.loaded = true;
this.refreshIcon = CoreConstants.ICON_REFRESH;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,66 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { ModalController } from '@singletons';
import { AddonModBookTocChapter, AddonModBookBookWSData, AddonModBookNumbering } from '../../services/book';
/**
* Modal to display the TOC of a book.
*/
@Component({
selector: 'addon-mod-book-toc',
templateUrl: 'toc.html',
styleUrls: ['toc.scss'],
})
export class AddonModBookTocComponent implements OnInit {
@Input() moduleId?: number;
@Input() chapters: AddonModBookTocChapter[] = [];
@Input() selected?: number;
@Input() courseId?: number;
showNumbers = true;
addPadding = true;
showBullets = false;
@Input() protected book?: AddonModBookBookWSData;
/**
* Component loaded.
*/
ngOnInit(): void {
if (this.book) {
this.showNumbers = this.book.numbering == AddonModBookNumbering.NUMBERS;
this.showBullets = this.book.numbering == AddonModBookNumbering.BULLETS;
this.addPadding = this.book.numbering != AddonModBookNumbering.NONE;
}
}
/**
* Function called when a course is clicked.
*
* @param id ID of the clicked chapter.
*/
loadChapter(id: number): void {
ModalController.instance.dismiss(id);
}
/**
* Close modal.
*/
closeModal(): void {
ModalController.instance.dismiss();
}
}

View File

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

View File

@ -0,0 +1,23 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!bookComponent?.loaded" (ionRefresh)="bookComponent?.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-book-index [module]="module" [courseId]="courseId" [initialChapterId]="chapterId"
(dataRetrieved)="updateData($event)">
</addon-mod-book-index>
</ion-content>

View File

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

View File

@ -0,0 +1,57 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, ViewChild } from '@angular/core';
import { CoreCourseWSModule } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { AddonModBookIndexComponent } from '../../components/index/index';
import { AddonModBookBookWSData } from '../../services/book';
/**
* Page that displays a book.
*/
@Component({
selector: 'page-addon-mod-book-index',
templateUrl: 'index.html',
})
export class AddonModBookIndexPage implements OnInit {
@ViewChild(AddonModBookIndexComponent) bookComponent?: AddonModBookIndexComponent;
title?: string;
module?: CoreCourseWSModule;
courseId?: number;
chapterId?: number;
/**
* Component being initialized.
*/
ngOnInit(): void {
this.module = CoreNavigator.instance.getRouteParam('module');
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
this.chapterId = CoreNavigator.instance.getRouteNumberParam('chapterId');
this.title = this.module?.name;
}
/**
* Update some data based on the book instance.
*
* @param book Book instance.
*/
updateData(book: AddonModBookBookWSData): void {
this.title = book.name || this.title;
}
}

View File

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

View File

@ -0,0 +1,55 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to treat links to book.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModBookLinkHandler';
constructor() {
super('AddonModBook', 'book', 'b');
}
/**
* Get the mod params necessary to open an activity.
*
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @return List of params to pass to navigateToModule / navigateToModuleByInstance.
*/
getPageParams(url: string, params: Record<string, string>): Params {
return params.chapterid ? { chapterId: parseInt(params.chapterid, 10) } : {};
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
*
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled(siteId);
}
}
export class AddonModBookIndexLinkHandler extends makeSingleton(AddonModBookIndexLinkHandlerService) {}

View File

@ -0,0 +1,44 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to treat links to book list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModBookListLinkHandler';
constructor() {
super('AddonModBook', 'book');
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
}
export class AddonModBookListLinkHandler extends makeSingleton(AddonModBookListLinkHandlerService) {}

View File

@ -0,0 +1,94 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { AddonModBookIndexComponent } from '../../components/index';
import { AddonModBook } from '../book';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreConstants } from '@/core/constants';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { makeSingleton } from '@singletons';
/**
* Handler to support book modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_book';
name = 'AddonModBook';
modName = 'book';
supportedFeatures = {
[CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
};
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
/**
* Get the data required to display the module in the course contents view.
*
* @param module The module object.
* @param courseId The course ID.
* @param sectionId The section ID.
* @return Data to render the module.
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_book-handler',
showDownloadButton: true,
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.instance.navigateToSitePath(AddonModBookModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
* It's recommended to return the class of the component, but you can also return an instance of the component.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
async getMainComponent(): Promise<Type<unknown> | undefined> {
return AddonModBookIndexComponent;
}
}
export class AddonModBookModuleHandler extends makeSingleton(AddonModBookModuleHandlerService) {}

View File

@ -0,0 +1,86 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
import { CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { AddonModBook, AddonModBookProvider } from '../book';
/**
* Handler to prefetch books.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookPrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
name = 'AddonModBook';
modName = 'book';
component = AddonModBookProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$|^entries$/;
/**
* Download or prefetch the content.
*
* @param module The module object returned by WS.
* @param courseId Course ID.
* @param prefetch True to prefetch, false to download right away.
* @return Promise resolved when all content is downloaded. Data returned is not reliable.
*/
async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise<void> {
const promises: Promise<unknown>[] = [];
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
// Ignore errors since this WS isn't available in some Moodle versions.
promises.push(CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(courseId, module.id)));
await Promise.all(promises);
}
/**
* Returns module intro files.
*
* @param module The module object returned by WS.
* @param courseId Course ID.
* @return Promise resolved with list of intro files.
*/
async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
const book = await CoreUtils.instance.ignoreErrors(AddonModBook.instance.getBook(courseId, module.id));
return this.getIntroFilesFromInstance(module, book);
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when the data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModBook.instance.invalidateContent(moduleId, courseId);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
}
export class AddonModBookPrefetchHandler extends makeSingleton(AddonModBookPrefetchHandlerService) {}

View File

@ -0,0 +1,79 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Type } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreTagFeedComponent } from '@features/tag/components/feed/feed';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreTagFeedElement, CoreTagHelper } from '@features/tag/services/tag-helper';
import { CoreUrlUtils } from '@services/utils/url';
import { makeSingleton } from '@singletons';
import { AddonModBook } from '../book';
/**
* Handler to support tags.
*/
@Injectable({ providedIn: 'root' })
export class AddonModBookTagAreaHandlerService implements CoreTagAreaHandler {
name = 'AddonModBookTagAreaHandler';
type = 'mod_book/book_chapters';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return AddonModBook.instance.isPluginEnabled();
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param content Rendered content.
* @return Area items (or promise resolved with the items).
*/
async parseContent(content: string): Promise<CoreTagFeedElement[]> {
const items = CoreTagHelper.instance.parseFeedContent(content);
// Find module ids of the returned books, they are needed by the link delegate.
await Promise.all(items.map((item) => {
const params = item.url ? CoreUrlUtils.instance.extractUrlParams(item.url) : {};
if (params.b && !params.id) {
const bookId = parseInt(params.b, 10);
return CoreCourse.instance.getModuleBasicInfoByInstance(bookId, 'book').then((module) => {
item.url += '&id=' + module.id;
return;
});
}
}));
return items;
}
/**
* Get the component to use to display items.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return CoreTagFeedComponent;
}
}
export class AddonModBookTagAreaHandler extends makeSingleton(AddonModBookTagAreaHandlerService) {}

View File

@ -20,7 +20,7 @@
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="400" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-cube" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
@ -161,7 +161,9 @@
</ion-label>
<ion-select [(ngModel)]="group" (ionChange)="setGroup(group)" aria-labelledby="addon-mod_lesson-groupslabel"
interface="action-sheet">
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">{{groupOpt.name}}</ion-select-option>
<ion-select-option *ngFor="let groupOpt of groupInfo.groups" [value]="groupOpt.id">
{{groupOpt.name}}
</ion-select-option>
</ion-select>
</ion-item>

View File

@ -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(<string> 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);
}

View File

@ -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),
},
{

View File

@ -111,7 +111,9 @@
<!-- Content page, display a button and the content. -->
<ion-row>
<ion-col>
<ion-button expand="block" class="ion-text-wrap" color="light" [disabled]="true">{{ answer[0].buttonText }}</ion-button>
<ion-button expand="block" class="ion-text-wrap" color="light" [disabled]="true">
{{ answer[0].buttonText }}
</ion-button>
</ion-col>
<ion-col>
<p [innerHTML]="answer[0].content"></p>

View File

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

View File

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

View File

@ -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: [
{

View File

@ -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 {}

View File

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

View File

@ -0,0 +1,25 @@
<ion-grid class="ion-no-padding ion-padding-bottom" *ngIf="previous || info || next">
<ion-row>
<ion-col class="ion-text-start" size="4">
<ion-button *ngIf="previous" class="core-navigation-bar-arrow" color="light"
[title]="previousTitle || ('core.previous' | translate)" (click)="action?.emit(previous)">
<ion-icon name="fas-arrow-left" [slot]="previousTitle ? 'start' : 'icon-only'"></ion-icon>
<core-format-text *ngIf="previousTitle" [text]="previousTitle" [component]="component" [componentId]="componentId"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text>
</ion-button>
</ion-col>
<ion-col class="ion-text-center ion-padding-horizontal" size="4">
<ion-button fill="clear" *ngIf="info" (click)="showInfo()" [title]="title">
<ion-icon slot="icon-only" name="fas-info-circle"></ion-icon>
</ion-button>
</ion-col>
<ion-col class="ion-text-end" size="4">
<ion-button *ngIf="next" class="core-navigation-bar-arrow" [title]="nextTitle || ('core.next' | translate)"
(click)="action?.emit(next)">
<core-format-text *ngIf="nextTitle" [text]="nextTitle" [component]="component" [componentId]="componentId"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [courseId]="courseId"></core-format-text>
<ion-icon name="fas-arrow-right" [slot]="nextTitle ? 'end' : 'icon-only'"></ion-icon>
</ion-button>
</ion-col>
</ion-row>
</ion-grid>

View File

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

View File

@ -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:
* <core-navigation-bar [previous]="prevItem" [next]="nextItem" (action)="goTo($event)"></core-navigation-bar>
*/
@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<unknown> =
new EventEmitter<unknown>(); // 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,
});
}
}

View File

@ -77,11 +77,17 @@ export class CoreConstants {
static readonly OUTDATED = 'outdated';
static readonly NOT_DOWNLOADABLE = 'notdownloadable';
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';
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.

View File

@ -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<void> {
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<void> {
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;
}
}

View File

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

View File

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

View File

@ -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<void> {
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<void> {
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:

View File

@ -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;

View File

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

View File

@ -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<void> {
// Scan for a QR code.
// @todo
// eslint-disable-next-line no-console
console.error('scanQR not implemented');
}
/**

View File

@ -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
{

View File

@ -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 {}

View File

@ -0,0 +1,3 @@
<ng-container *ngFor="let tag of tags">
<ion-badge (click)="openTag(tag)" class="core-tag-list-tag">{{ tag.rawname }}</ion-badge>
</ng-container>

View File

@ -0,0 +1,7 @@
:host {
line-height: 1.6;
ion-badge {
cursor: pointer;
}
}

View File

@ -0,0 +1,47 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { 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.navigateToSitePath('/tag/index', { params, preferCurrentTab: false });
}
}

View File

@ -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,
],

View File

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

View File

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