MOBILE-3659 course: Implement index and contents pages

main
Dani Palou 2021-01-15 10:32:21 +01:00
parent a91c042cc2
commit 183a033dc8
21 changed files with 1099 additions and 88 deletions

View File

@ -106,7 +106,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
protected unregisterBackButtonAction: any; protected unregisterBackButtonAction: any;
protected languageChangedSubscription: Subscription; protected languageChangedSubscription: Subscription;
protected isInTransition = false; // Weather Slides is in transition. protected isInTransition = false; // Weather Slides is in transition.
protected slidesSwiper: any; protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
protected slidesSwiperLoaded = false; protected slidesSwiperLoaded = false;
protected stackEventsSubscription?: Subscription; protected stackEventsSubscription?: Subscription;
@ -338,7 +338,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
return; return;
} }
this.firstSelectedTab = selectedTab.id; this.firstSelectedTab = selectedTab.id!;
this.selectTab(this.firstSelectedTab); this.selectTab(this.firstSelectedTab);
// Setup tab scrolling. // Setup tab scrolling.
@ -548,18 +548,31 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
} }
/** /**
* Tab selected. * Select a tab by ID.
* *
* @param tabId Selected tab index. * @param tabId Tab ID.
* @param e Event. * @param e Event.
* @return Promise resolved when done.
*/ */
async selectTab(tabId: string, e?: Event): Promise<void> { async selectTab(tabId: string, e?: Event): Promise<void> {
let index = this.tabs.findIndex((tab) => tabId == tab.id); const index = this.tabs.findIndex((tab) => tabId == tab.id);
return this.selectByIndex(index, e);
}
/**
* Select a tab by index.
*
* @param index Index to select.
* @param e Event.
* @return Promise resolved when done.
*/
async selectByIndex(index: number, e?: Event): Promise<void> {
if (index < 0 || index >= this.tabs.length) { if (index < 0 || index >= this.tabs.length) {
if (this.selected) { if (this.selected) {
// Invalid index do not change tab. // Invalid index do not change tab.
e && e.preventDefault(); e?.preventDefault();
e && e.stopPropagation(); e?.stopPropagation();
return; return;
} }
@ -568,12 +581,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
index = 0; index = 0;
} }
const selectedTab = this.tabs[index]; const tabToSelect = this.tabs[index];
if (tabId == this.selected || !selectedTab || !selectedTab.enabled) { if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
// Already selected or not enabled. // Already selected or not enabled.
e?.preventDefault();
e && e.preventDefault(); e?.stopPropagation();
e && e.stopPropagation();
return; return;
} }
@ -583,17 +595,17 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
} }
const pageParams: NavigationOptions = {}; const pageParams: NavigationOptions = {};
if (selectedTab.pageParams) { if (tabToSelect.pageParams) {
pageParams.queryParams = selectedTab.pageParams; pageParams.queryParams = tabToSelect.pageParams;
} }
const ok = await this.navCtrl.navigateForward(selectedTab.page, pageParams); const ok = await this.navCtrl.navigateForward(tabToSelect.page, pageParams);
if (ok !== false) { if (ok !== false) {
this.selectHistory.push(tabId); this.selectHistory.push(tabToSelect.id!);
this.selected = tabId; this.selected = tabToSelect.id;
this.selectedIndex = index; this.selectedIndex = index;
this.ionChange.emit(selectedTab); this.ionChange.emit(tabToSelect);
} }
} }
@ -644,16 +656,14 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
/** /**
* Core Tab class. * Core Tab class.
*/ */
class CoreTab { export type CoreTab = {
page: string; // Page to navigate to.
id = ''; // Unique tab id. title: string; // The translatable tab title.
class = ''; // Class, if needed. id?: string; // Unique tab id.
title = ''; // The translatable tab title. class?: string; // Class, if needed.
icon?: string; // The tab icon. icon?: string; // The tab icon.
badge?: string; // A badge to add in the tab. badge?: string; // A badge to add in the tab.
badgeStyle?: string; // The badge color. badgeStyle?: string; // The badge color.
enabled = true; // Whether the tab is enabled. enabled?: boolean; // Whether the tab is enabled.
page = ''; // Page to navigate to.
pageParams?: Params; // Page params. pageParams?: Params; // Page params.
};
}

View File

@ -1,6 +1,6 @@
<div class="ion-padding"> <div class="ion-padding">
<core-course-module-description [description]="module && module.description" contextLevel="module" <core-course-module-description [description]="module?.description" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId"> [contextInstanceId]="module?.id" [courseId]="courseId">
</core-course-module-description> </core-course-module-description>
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2> <h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>

View File

@ -0,0 +1,33 @@
// (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: '',
redirectTo: 'index',
pathMatch: 'full',
},
{
path: 'index',
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class CoreCourseLazyModule {}

View File

@ -13,18 +13,38 @@
// limitations under the License. // limitations under the License.
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CORE_SITE_SCHEMAS } from '@services/sites'; import { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreCourseComponentsModule } from './components/components.module'; import { CoreCourseComponentsModule } from './components/components.module';
import { CoreCourseFormatModule } from './format/formats.module'; import { CoreCourseFormatModule } from './format/formats.module';
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course'; import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log'; import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module';
const routes: Routes = [
{
path: 'course',
loadChildren: () => import('@features/course/course-lazy.module').then(m => m.CoreCourseLazyModule),
},
];
const courseIndexRoutes: Routes = [
{
path: 'contents',
loadChildren: () => import('./pages/contents/contents.module').then(m => m.CoreCourseContentsPageModule),
},
];
@NgModule({ @NgModule({
imports: [ imports: [
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
CoreMainMenuTabRoutingModule.forChild(routes),
CoreCourseFormatModule, CoreCourseFormatModule,
CoreCourseComponentsModule, CoreCourseComponentsModule,
], ],
exports: [CoreCourseIndexRoutingModule],
providers: [ providers: [
{ {
provide: CORE_SITE_SCHEMAS, provide: CORE_SITE_SCHEMAS,

View File

@ -0,0 +1,29 @@
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item [hidden]="!displayEnableDownload" [priority]="2000" [iconAction]="downloadEnabledIcon"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()">
</core-context-menu-item>
<core-context-menu-item [hidden]="!downloadCourseEnabled" [priority]="1900"
[content]="prefetchCourseData.statusTranslatable | translate" (action)="prefetchCourse()"
[iconAction]="prefetchCourseData.icon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item [priority]="1800" [content]="'core.course.coursesummary' | translate" (action)="openCourseSummary()"
iconAction="fa-graduation-cap">
</core-context-menu-item>
<core-context-menu-item *ngFor="let item of courseMenuHandlers" [priority]="item.priority" (action)="openMenuItem(item)"
[content]="item.data.title | translate" [iconAction]="item.data.icon" [class]="item.data.class">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="dataLoaded">
<!-- <core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId"
[initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" [moduleId]="moduleId"
(completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}">
</core-course-format> -->
</core-loading>
</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 { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseContentsPage } from './contents';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
const routes: Routes = [
{
path: '',
component: CoreCourseContentsPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
CoreCourseComponentsModule,
],
declarations: [
CoreCourseContentsPage,
],
exports: [RouterModule],
})
export class CoreCourseContentsPageModule {}

View File

@ -0,0 +1,533 @@
// (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, ViewChild, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IonContent, IonRefresher, NavController } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourses, CoreCourseAnyCourseData } from '@features/courses/services/courses';
import {
CoreCourse,
CoreCourseCompletionActivityStatus,
CoreCourseModuleCompletionData,
CoreCourseProvider,
} from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseSectionFormatted, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
// import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import {
CoreCourseOptionsDelegate,
CoreCourseOptionsMenuHandlerToDisplay,
} from '@features/course/services/course-options-delegate';
// import { CoreCourseSyncProvider } from '../../providers/sync';
// import { CoreCourseFormatComponent } from '../../components/format/format';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import {
CoreEvents,
CoreEventObserver,
CoreEventCourseStatusChanged,
CoreEventCompletionModuleViewedData,
} from '@singletons/events';
import { Translate } from '@singletons';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that displays the contents of a course.
*/
@Component({
selector: 'page-core-course-contents',
templateUrl: 'contents.html',
})
export class CoreCourseContentsPage implements OnInit, OnDestroy {
@ViewChild(IonContent) content?: IonContent;
// @ViewChild(CoreCourseFormatComponent) formatComponent: CoreCourseFormatComponent;
course!: CoreCourseAnyCourseData;
sections?: Section[];
sectionId?: number;
sectionNumber?: number;
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
dataLoaded = false;
downloadEnabled = false;
downloadEnabledIcon = 'far-square'; // Disabled by default.
downloadCourseEnabled = false;
moduleId?: number;
displayEnableDownload = false;
displayRefresher = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: 'spinner',
statusTranslatable: 'core.course.downloadcourse',
status: '',
loading: true,
};
protected formatOptions?: Record<string, unknown>;
protected completionObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected syncObserver?: CoreEventObserver;
protected isDestroyed = false;
constructor(
protected route: ActivatedRoute,
protected navCtrl: NavController,
) { }
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
// Get params.
this.course = this.route.snapshot.queryParams['course'];
this.sectionId = this.route.snapshot.queryParams['sectionId'];
this.sectionNumber = this.route.snapshot.queryParams['sectionNumber'];
this.moduleId = this.route.snapshot.queryParams['moduleId'];
if (!this.course) {
CoreDomUtils.instance.showErrorModal('Missing required course parameter.');
this.navCtrl.pop();
return;
}
this.displayEnableDownload = !CoreSites.instance.getCurrentSite()?.isOfflineDisabled() &&
CoreCourseFormatDelegate.instance.displayEnableDownload(this.course);
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
this.initListeners();
await this.loadData(false, true);
this.dataLoaded = true;
this.initPrefetch();
}
/**
* Init listeners.
*
* @return Promise resolved when done.
*/
protected async initListeners(): Promise<void> {
if (this.downloadCourseEnabled) {
// Listen for changes in course status.
this.courseStatusObserver = CoreEvents.on<CoreEventCourseStatusChanged>(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status);
}
}, CoreSites.instance.getCurrentSiteId());
}
// Check if the course format requires the view to be refreshed when completion changes.
const shouldRefresh = await CoreCourseFormatDelegate.instance.shouldRefreshWhenCompletionChanges(this.course);
if (!shouldRefresh) {
return;
}
this.completionObserver = CoreEvents.on<CoreEventCompletionModuleViewedData>(
CoreEvents.COMPLETION_MODULE_VIEWED,
(data) => {
if (data && data.courseId == this.course.id) {
this.refreshAfterCompletionChange(true);
}
},
);
// @todo this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
// if (data && data.courseId == this.course.id) {
// this.refreshAfterCompletionChange(false);
// if (data.warnings && data.warnings[0]) {
// CoreDomUtils.instance.showErrorModal(data.warnings[0]);
// }
// }
// });
}
/**
* Init prefetch data if needed.
*
* @return Promise resolved when done.
*/
protected async initPrefetch(): Promise<void> {
if (!this.downloadCourseEnabled) {
// Cannot download the whole course, stop.
return;
}
// Determine the course prefetch status.
await this.determineCoursePrefetchIcon();
if (this.prefetchCourseData.icon != 'spinner') {
return;
}
// Course is being downloaded. Get the download promise.
const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id);
if (promise) {
// There is a download promise. Show an error if it fails.
promise.catch((error) => {
if (!this.isDestroyed) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
const status = await CoreCourse.instance.setCoursePreviousStatus(this.course.id);
this.updateCourseStatus(status);
}
}
/**
* Fetch and load all the data required for the view.
*
* @param refresh If it's refreshing content.
* @param sync If it should try to sync.
* @return Promise resolved when done.
*/
protected async loadData(refresh?: boolean, sync?: boolean): Promise<void> {
// First of all, get the course because the data might have changed.
const result = await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.getCourse(this.course.id));
if (result) {
if (this.course.id === result.course.id && 'displayname' in this.course && !('displayname' in result.course)) {
result.course.displayname = this.course.displayname;
}
this.course = result.course;
}
// @todo: Get the overview files. Maybe move it to format component?
// if ('overviewfiles' in this.course && this.course.overviewfiles) {
// this.course.imageThumb = this.course.overviewfiles[0] && this.course.overviewfiles[0].fileurl;
// }
if (sync) {
// Try to synchronize the course data.
// @todo return this.syncProvider.syncCourse(this.course.id).then((result) => {
// if (result.warnings && result.warnings.length) {
// CoreDomUtils.instance.showErrorModal(result.warnings[0]);
// }
// }).catch(() => {
// // For now we don't allow manual syncing, so ignore errors.
// });
}
try {
await Promise.all([
this.loadSections(refresh),
this.loadMenuHandlers(refresh),
this.loadCourseFormatOptions(),
]);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
}
}
/**
* Load course sections.
*
* @param refresh If it's refreshing content.
* @return Promise resolved when done.
*/
protected async loadSections(refresh?: boolean): Promise<void> {
// Get all the sections.
const sections = await CoreCourse.instance.getSections(this.course.id, false, true);
if (refresh) {
// Invalidate the recently downloaded module list. To ensure info can be prefetched.
// const modules = CoreCourse.instance.getSectionsModules(sections);
// @todo await this.prefetchDelegate.invalidateModules(modules, this.course.id);
}
let completionStatus: Record<string, CoreCourseCompletionActivityStatus> = {};
// Get the completion status.
if (this.course.enablecompletion !== false) {
const sectionWithModules = sections.find((section) => section.modules.length > 0);
if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') {
// The module already has completion (3.6 onwards). Load the offline completion.
await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.loadOfflineCompletion(this.course.id, sections));
} else {
const fetchedData = await CoreUtils.instance.ignoreErrors(
CoreCourse.instance.getActivitiesCompletionStatus(this.course.id),
);
completionStatus = fetchedData || completionStatus;
}
}
// Add handlers
const result = CoreCourseHelper.instance.addHandlerDataForModules(
sections,
this.course.id,
completionStatus,
this.course.fullname,
true,
);
// Format the name of each section.
result.sections.forEach(async (section: Section) => {
const result = await CoreFilterHelper.instance.getFiltersAndFormatText(
section.name.trim(),
'course',
this.course.id,
{ clean: true, singleLine: true },
);
section.formattedName = result.text;
});
this.sections = result.sections;
if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) {
// Add a fake first section (all sections).
this.sections.unshift({
id: CoreCourseProvider.ALL_SECTIONS_ID,
name: Translate.instance.instant('core.course.allsections'),
hasContent: true,
summary: '',
summaryformat: 1,
modules: [],
});
}
// Get whether to show the refresher now that we have sections.
this.displayRefresher = CoreCourseFormatDelegate.instance.displayRefresher(this.course, this.sections);
}
/**
* Load the course menu handlers.
*
* @param refresh If it's refreshing content.
* @return Promise resolved when done.
*/
protected async loadMenuHandlers(refresh?: boolean): Promise<void> {
this.courseMenuHandlers = await CoreCourseOptionsDelegate.instance.getMenuHandlersToDisplay(this.course, refresh);
}
/**
* Load course format options if needed.
*
* @return Promise resolved when done.
*/
protected async loadCourseFormatOptions(): Promise<void> {
// Load the course format options when course completion is enabled to show completion progress on sections.
if (!this.course.enablecompletion || !CoreCourses.instance.isGetCoursesByFieldAvailable()) {
return;
}
if ('courseformatoptions' in this.course && this.course.courseformatoptions) {
// Already loaded.
this.formatOptions = CoreUtils.instance.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value');
return;
}
const course = await CoreUtils.instance.ignoreErrors(CoreCourses.instance.getCourseByField('id', this.course.id));
course && Object.assign(this.course, course);
if (course?.courseformatoptions) {
this.formatOptions = CoreUtils.instance.objectToKeyValueMap(course.courseformatoptions, 'name', 'value');
}
}
/**
* Refresh the data.
*
* @param refresher Refresher.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: CustomEvent<IonRefresher>): Promise<void> {
await CoreUtils.instance.ignoreErrors(this.invalidateData());
try {
await this.loadData(true, true);
} finally {
// Do not call doRefresh on the format component if the refresher is defined in the format component
// to prevent an inifinite loop.
if (this.displayRefresher) {
// @todo await CoreUtils.instance.ignoreErrors(this.formatComponent.doRefresh(refresher));
}
refresher?.detail.complete();
}
}
/**
* The completion of any of the modules has changed.
*
* @param completionData Completion data.
* @return Promise resolved when done.
*/
async onCompletionChange(completionData: CoreCourseModuleCompletionData): Promise<void> {
const shouldReload = typeof completionData.valueused == 'undefined' || completionData.valueused;
if (!shouldReload) {
return;
}
await CoreUtils.instance.ignoreErrors(this.invalidateData());
await this.refreshAfterCompletionChange(true);
}
/**
* Invalidate the data.
*
* @return Promise resolved when done.
*/
protected async invalidateData(): Promise<void> {
const promises: Promise<void>[] = [];
promises.push(CoreCourse.instance.invalidateSections(this.course.id));
promises.push(CoreCourses.instance.invalidateUserCourses());
promises.push(CoreCourseFormatDelegate.instance.invalidateData(this.course, this.sections || []));
if (this.sections) {
// @todo promises.push(this.prefetchDelegate.invalidateCourseUpdates(this.course.id));
}
await Promise.all(promises);
}
/**
* Refresh list after a completion change since there could be new activities.
*
* @param sync If it should try to sync.
* @return Promise resolved when done.
*/
protected async refreshAfterCompletionChange(sync?: boolean): Promise<void> {
// Save scroll position to restore it once done.
const scrollElement = await this.content?.getScrollElement();
const scrollTop = scrollElement?.scrollTop || 0;
const scrollLeft = scrollElement?.scrollLeft || 0;
this.dataLoaded = false;
this.content?.scrollToTop(0); // Scroll top so the spinner is seen.
try {
await this.loadData(true, sync);
// @todo await this.formatComponent.doRefresh(undefined, undefined, true);
} finally {
this.dataLoaded = true;
// Wait for new content height to be calculated and scroll without animation.
setTimeout(() => {
this.content?.scrollToPoint(scrollLeft, scrollTop, 0);
});
}
}
/**
* Determines the prefetch icon of the course.
*
* @return Promise resolved when done.
*/
protected async determineCoursePrefetchIcon(): Promise<void> {
this.prefetchCourseData = await CoreCourseHelper.instance.getCourseStatusIconAndTitle(this.course.id);
}
/**
* Prefetch the whole course.
*/
prefetchCourse(): void {
try {
// @todo await CoreCourseHelper.instance.confirmAndPrefetchCourse(
// this.prefetchCourseData,
// this.course,
// this.sections,
// this.courseHandlers,
// this.courseMenuHandlers,
// );
} catch (error) {
if (this.isDestroyed) {
return;
}
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
}
/**
* Toggle download enabled.
*/
toggleDownload(): void {
this.downloadEnabled = !this.downloadEnabled;
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
}
/**
* Update the course status icon and title.
*
* @param status Status to show.
*/
protected updateCourseStatus(status: string): void {
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
}
/**
* Open the course summary
*/
openCourseSummary(): void {
CoreNavHelper.instance.goInCurrentMainMenuTab('/courses/preview', { course: this.course, avoidOpenCourse: true });
}
/**
* Opens a menu item registered to the delegate.
*
* @param item Item to open
*/
openMenuItem(item: CoreCourseOptionsMenuHandlerToDisplay): void {
const params = Object.assign({ course: this.course }, item.data.pageParams);
CoreNavHelper.instance.goInCurrentMainMenuTab(item.data.page, params);
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.completionObserver?.off();
this.courseStatusObserver?.off();
this.syncObserver?.off();
}
/**
* User entered the page.
*/
ionViewDidEnter(): void {
// @todo this.formatComponent?.ionViewDidEnter();
}
/**
* User left the page.
*/
ionViewDidLeave(): void {
// @todo this.formatComponent?.ionViewDidLeave();
}
}
type Section = CoreCourseSectionFormatted & {
formattedName?: string;
};

View File

@ -0,0 +1,33 @@
// (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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
import { ModuleRoutesConfig } from '@/app/app-routing.module';
export const COURSE_INDEX_ROUTES = new InjectionToken('COURSE_INDEX_ROUTES');
@NgModule()
export class CoreCourseIndexRoutingModule {
static forChild(routes: ModuleRoutesConfig): ModuleWithProviders<CoreCourseIndexRoutingModule> {
return {
ngModule: CoreCourseIndexRoutingModule,
providers: [
{ provide: COURSE_INDEX_ROUTES, multi: true, useValue: routes },
],
};
}
}

View File

@ -0,0 +1,15 @@
<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="course" [contextInstanceId]="course?.id"></core-format-text>
</ion-title>
<ion-buttons slot="end"></ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-tabs [tabs]="tabs" [hideUntil]="loaded"></core-tabs>
</ion-content>

View File

@ -0,0 +1,54 @@
// (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 { Injector, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, ROUTES, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { resolveModuleRoutes } from '@/app/app-routing.module';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseIndexPage } from './index';
import { COURSE_INDEX_ROUTES } from './index-routing.module';
function buildRoutes(injector: Injector): Routes {
const routes = resolveModuleRoutes(injector, COURSE_INDEX_ROUTES);
return [
{
path: '',
component: CoreCourseIndexPage,
children: routes.children,
},
...routes.siblings,
];
}
@NgModule({
providers: [
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
],
declarations: [
CoreCourseIndexPage,
],
exports: [RouterModule],
})
export class CoreCourseIndexPageModule {}

View File

@ -0,0 +1,191 @@
// (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, ViewChild, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { CoreTab, CoreTabsComponent } from '@components/tabs/tabs';
import { CoreCourseFormatDelegate } from '../../services/format-delegate';
import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
import { CoreEventObserver, CoreEvents, CoreEventSelectCourseTabData } from '@singletons/events';
import { CoreCourse, CoreCourseModuleData } from '@features/course/services/course';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreUtils } from '@services/utils/utils';
import { CoreTextUtils } from '@services/utils/text';
import { CoreNavHelper } from '@services/nav-helper';
import { CoreObject } from '@singletons/object';
/**
* Page that displays the list of courses the user is enrolled in.
*/
@Component({
selector: 'page-core-course-index',
templateUrl: 'index.html',
})
export class CoreCourseIndexPage implements OnInit, OnDestroy {
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
title?: string;
course?: CoreCourseAnyCourseData;
tabs: CourseTab[] = [];
loaded = false;
protected currentPagePath = '';
protected selectTabObserver: CoreEventObserver;
protected firstTabName?: string;
protected contentsTab: CoreTab = {
page: 'contents',
title: 'core.course.contents',
pageParams: {},
};
constructor(
protected route: ActivatedRoute,
) {
this.selectTabObserver = CoreEvents.on<CoreEventSelectCourseTabData>(CoreEvents.SELECT_COURSE_TAB, (data) => {
if (!data.name) {
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
if (data.sectionId) {
this.contentsTab.pageParams!.sectionId = data.sectionId;
}
if (data.sectionNumber) {
this.contentsTab.pageParams!.sectionNumber = data.sectionNumber;
}
// Select course contents.
this.tabsComponent?.selectByIndex(0);
} else if (this.tabs) {
const index = this.tabs.findIndex((tab) => tab.name == data.name);
if (index >= 0) {
this.tabsComponent?.selectByIndex(index + 1);
}
}
});
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
// Get params.
this.course = this.route.snapshot.queryParams['course'];
this.firstTabName = this.route.snapshot.queryParams['selectedTab'];
const module: CoreCourseModuleData | undefined = this.route.snapshot.queryParams['module'];
const modParams: Params | undefined = this.route.snapshot.queryParams['modParams'];
this.currentPagePath = CoreNavHelper.instance.getCurrentPage();
this.contentsTab.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, this.contentsTab.page);
this.contentsTab.pageParams = CoreObject.removeUndefined({
course: this.course,
sectionId: this.route.snapshot.queryParams['sectionId'],
sectionNumber: this.route.snapshot.queryParams['sectionNumber'],
});
if (module) {
this.contentsTab.pageParams!.moduleId = module.id;
CoreCourseHelper.instance.openModule(module, this.course!.id, this.contentsTab.pageParams!.sectionId, modParams);
}
this.tabs.push(this.contentsTab);
this.loaded = true;
await Promise.all([
this.loadCourseHandlers(),
this.loadTitle(),
]);
}
/**
* Load course option handlers.
*
* @return Promise resolved when done.
*/
protected async loadCourseHandlers(): Promise<void> {
// Load the course handlers.
const handlers = await CoreCourseOptionsDelegate.instance.getHandlersToDisplay(this.course!, false, false);
this.tabs.concat(handlers.map(handler => handler.data));
let tabToLoad: number | undefined;
// Add the courseId to the handler component data.
handlers.forEach((handler, index) => {
handler.data.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, handler.data.page);
handler.data.pageParams = handler.data.pageParams || {};
handler.data.pageParams.courseId = this.course!.id;
// Check if this handler should be the first selected tab.
if (this.firstTabName && handler.name == this.firstTabName) {
tabToLoad = index + 1;
}
});
// Select the tab if needed.
this.firstTabName = undefined;
if (tabToLoad) {
setTimeout(() => {
this.tabsComponent?.selectByIndex(tabToLoad!);
});
}
}
/**
* Load title for the page.
*
* @return Promise resolved when done.
*/
protected async loadTitle(): Promise<void> {
// Get the title to display initially.
this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!);
// Load sections.
const sections = await CoreUtils.instance.ignoreErrors(CoreCourse.instance.getSections(this.course!.id, false, true));
if (!sections) {
return;
}
// Get the title again now that we have sections.
this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!, sections);
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.selectTabObserver?.off();
}
/**
* User entered the page.
*/
ionViewDidEnter(): void {
this.tabsComponent?.ionViewDidEnter();
}
/**
* User left the page.
*/
ionViewDidLeave(): void {
this.tabsComponent?.ionViewDidLeave();
}
}
type CourseTab = CoreTab & {
name?: string;
};

View File

@ -122,7 +122,6 @@ export class CoreCourseHelperProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreCourseHelperProvider'); this.logger = CoreLogger.getInstance('CoreCourseHelperProvider');
} }
@ -138,17 +137,24 @@ export class CoreCourseHelperProvider {
* @return Whether the sections have content. * @return Whether the sections have content.
*/ */
addHandlerDataForModules( addHandlerDataForModules(
sections: CoreCourseSectionFormatted[], sections: CoreCourseSection[],
courseId: number, courseId: number,
completionStatus?: Record<string, CoreCourseCompletionActivityStatus>, completionStatus?: Record<string, CoreCourseCompletionActivityStatus>,
courseName?: string, courseName?: string,
forCoursePage = false, forCoursePage = false,
): boolean { ): { hasContent: boolean; sections: CoreCourseSectionFormatted[] } {
const formattedSections: CoreCourseSectionFormatted[] = sections;
let hasContent = false; let hasContent = false;
sections.forEach((section) => { formattedSections.forEach((section) => {
if (!section || !this.sectionHasContent(section) || !section.modules) { if (!section || !section.modules) {
return;
}
section.hasContent = this.sectionHasContent(section);
if (!section.hasContent) {
return; return;
} }
@ -189,7 +195,7 @@ export class CoreCourseHelperProvider {
}); });
}); });
return hasContent; return { hasContent, sections: formattedSections };
} }
/** /**
@ -821,8 +827,24 @@ export class CoreCourseHelperProvider {
* @param modParams Params to pass to the module * @param modParams Params to pass to the module
* @param True if module can be opened, false otherwise. * @param True if module can be opened, false otherwise.
*/ */
openModule(): void { openModule(module: CoreCourseModuleDataFormatted, courseId: number, sectionId?: number, modParams?: Params): boolean {
// @todo params and logic if (!module.handlerData) {
module.handlerData = CoreCourseModuleDelegate.instance.getModuleDataFor(
module.modname,
module,
courseId,
sectionId,
false,
);
}
if (module.handlerData?.action) {
module.handlerData.action(new Event('click'), module, courseId, { animated: false }, modParams);
return true;
}
return false;
} }
/** /**
@ -1054,6 +1076,7 @@ export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {}
* Section with calculated data. * Section with calculated data.
*/ */
export type CoreCourseSectionFormatted = Omit<CoreCourseSection, 'modules'> & { export type CoreCourseSectionFormatted = Omit<CoreCourseSection, 'modules'> & {
hasContent?: boolean;
modules: CoreCourseModuleDataFormatted[]; modules: CoreCourseModuleDataFormatted[];
}; };

View File

@ -13,12 +13,18 @@
// limitations under the License. // limitations under the License.
// @todo test delegate // @todo test delegate
import { Injectable, Type } from '@angular/core'; import { Injectable } from '@angular/core';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate';
import { CoreEvents } from '@singletons/events'; import { CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { CoreCourses, CoreCoursesProvider, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses'; import {
CoreCourseAnyCourseData,
CoreCourseAnyCourseDataWithOptions,
CoreCourses,
CoreCoursesProvider,
CoreCourseUserAdminOrNavOptionIndexed,
} from '@features/courses/services/courses';
import { CoreCourseProvider } from './course'; import { CoreCourseProvider } from './course';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { makeSingleton } from '@singletons'; import { makeSingleton } from '@singletons';
@ -61,7 +67,7 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
* @return Data or promise resolved with the data. * @return Data or promise resolved with the data.
*/ */
getDisplayData?( getDisplayData?(
course: CoreEnrolledCourseDataWithExtraInfoAndOptions, course: CoreCourseAnyCourseDataWithOptions,
): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>; ): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>;
/** /**
@ -98,7 +104,7 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler {
* @return Data or promise resolved with data. * @return Data or promise resolved with data.
*/ */
getMenuDisplayData( getMenuDisplayData(
course: CoreEnrolledCourseDataWithExtraInfoAndOptions, course: CoreCourseAnyCourseDataWithOptions,
): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>; ): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>;
} }
@ -117,15 +123,14 @@ export interface CoreCourseOptionsHandlerData {
class?: string; class?: string;
/** /**
* The component to render the handler. It must be the component class, not the name or an instance. * Path of the page to load for the handler.
* When the component is created, it will receive the courseId as input.
*/ */
component: Type<unknown>; page: string;
/** /**
* Data to pass to the component. All the properties in this object will be passed to the component as inputs. * Params to pass to the page (other than 'courseId' which is always sent).
*/ */
componentData?: Record<string | number, unknown>; pageParams?: Params;
} }
/** /**
@ -143,7 +148,7 @@ export interface CoreCourseOptionsMenuHandlerData {
class?: string; class?: string;
/** /**
* Name of the page to load for the handler. * Path of the page to load for the handler.
*/ */
page: string; page: string;
@ -161,22 +166,12 @@ export interface CoreCourseOptionsMenuHandlerData {
/** /**
* Data returned by the delegate for each handler. * Data returned by the delegate for each handler.
*/ */
export interface CoreCourseOptionsHandlerToDisplay { export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay {
/** /**
* Data to display. * Data to display.
*/ */
data: CoreCourseOptionsHandlerData; data: CoreCourseOptionsHandlerData;
/**
* Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...).
*/
name: string;
/**
* The highest priority is displayed first.
*/
priority?: number;
/** /**
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline. * Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
* *
@ -368,7 +363,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
* @return Promise resolved with array of handlers. * @return Promise resolved with array of handlers.
*/ */
getHandlersToDisplay( getHandlersToDisplay(
course: CoreEnrolledCourseDataWithExtraInfoAndOptions, course: CoreCourseAnyCourseData,
refresh = false, refresh = false,
isGuest = false, isGuest = false,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
@ -390,7 +385,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
* @return Promise resolved with array of handlers. * @return Promise resolved with array of handlers.
*/ */
getMenuHandlersToDisplay( getMenuHandlersToDisplay(
course: CoreEnrolledCourseDataWithExtraInfoAndOptions, course: CoreCourseAnyCourseData,
refresh = false, refresh = false,
isGuest = false, isGuest = false,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
@ -414,28 +409,31 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
*/ */
protected async getHandlersToDisplayInternal( protected async getHandlersToDisplayInternal(
menu: boolean, menu: boolean,
course: CoreEnrolledCourseDataWithExtraInfoAndOptions, course: CoreCourseAnyCourseData,
refresh = false, refresh = false,
isGuest = false, isGuest = false,
navOptions?: CoreCourseUserAdminOrNavOptionIndexed, navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
admOptions?: CoreCourseUserAdminOrNavOptionIndexed, admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> { ): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> {
const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course;
const accessData = { const accessData = {
type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT, type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT,
}; };
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = []; const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = [];
if (navOptions) { if (navOptions) {
course.navOptions = navOptions; courseWithOptions.navOptions = navOptions;
} }
if (admOptions) { if (admOptions) {
course.admOptions = admOptions; courseWithOptions.admOptions = admOptions;
} }
await this.loadCourseOptions(course, refresh); await this.loadCourseOptions(courseWithOptions, refresh);
// Call getHandlersForAccess to make sure the handlers have been loaded. // Call getHandlersForAccess to make sure the handlers have been loaded.
await this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions); await this.getHandlersForAccess(course.id, refresh, accessData, courseWithOptions.navOptions, courseWithOptions.admOptions);
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[]; let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[];
@ -450,7 +448,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData ? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
: (handler as CoreCourseOptionsHandler).getDisplayData; : (handler as CoreCourseOptionsHandler).getDisplayData;
promises.push(Promise.resolve(getFunction!.call(handler, course)).then((data) => { promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
handlersToDisplay.push({ handlersToDisplay.push({
data: data, data: data,
priority: handler.priority, priority: handler.priority,
@ -587,7 +585,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
* @param refresh True if it should refresh the list. * @param refresh True if it should refresh the list.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async loadCourseOptions(course: CoreEnrolledCourseDataWithExtraInfoAndOptions, refresh = false): Promise<void> { protected async loadCourseOptions(course: CoreCourseAnyCourseDataWithOptions, refresh = false): Promise<void> {
if (CoreCourses.instance.canGetAdminAndNavOptions() && if (CoreCourses.instance.canGetAdminAndNavOptions() &&
(typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) { (typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) {

View File

@ -981,7 +981,7 @@ export class CoreCourseProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> { async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> {
// @todo const loading = await CoreDomUtils.instance.showModalLoading(); const loading = await CoreDomUtils.instance.showModalLoading();
// Wait for site plugins to be fetched. // Wait for site plugins to be fetched.
// @todo await this.sitePluginsProvider.waitFetchPlugins(); // @todo await this.sitePluginsProvider.waitFetchPlugins();
@ -992,14 +992,13 @@ export class CoreCourseProvider {
course = result.course; course = result.course;
} }
/* @todo if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
// No custom format plugin. We don't need to wait for anything. // No custom format plugin. We don't need to wait for anything.
await CoreCourseFormatDelegate.instance.openCourse(course, params); await CoreCourseFormatDelegate.instance.openCourse(<CoreCourseAnyCourseData> course, params);
loading.dismiss(); loading.dismiss();
return; return;
} */ }
// This course uses a custom format plugin, wait for the format plugin to finish loading. // This course uses a custom format plugin, wait for the format plugin to finish loading.
try { try {

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
import { CoreNavHelper } from '@services/nav-helper';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreCourseSection } from '../course'; import { CoreCourseSection } from '../course';
import { CoreCourseFormatHandler } from '../format-delegate'; import { CoreCourseFormatHandler } from '../format-delegate';
@ -175,7 +176,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
Object.assign(params, { course: course }); Object.assign(params, { course: course });
// Don't return the .push promise, we don't want to display a loading modal during the page transition. // Don't return the .push promise, we don't want to display a loading modal during the page transition.
// @todo navCtrl.push('CoreCourseSectionPage', params); CoreNavHelper.instance.goInCurrentMainMenuTab('course', params);
} }
/** /**

View File

@ -49,8 +49,8 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
getData( getData(
module: CoreCourseModuleData | CoreCourseModuleBasicInfo, module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
courseId: number, // eslint-disable-line @typescript-eslint/no-unused-vars courseId: number, // eslint-disable-line @typescript-eslint/no-unused-vars
sectionId: number, // eslint-disable-line @typescript-eslint/no-unused-vars sectionId?: number, // eslint-disable-line @typescript-eslint/no-unused-vars
forCoursePage: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars forCoursePage?: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
): CoreCourseModuleHandlerData { ): CoreCourseModuleHandlerData {
// Return the default data. // Return the default data.
const defaultData: CoreCourseModuleHandlerData = { const defaultData: CoreCourseModuleHandlerData = {

View File

@ -54,8 +54,8 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
getData( getData(
module: CoreCourseModuleData | CoreCourseModuleBasicInfo, module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
courseId: number, courseId: number,
sectionId: number, sectionId?: number,
forCoursePage: boolean, forCoursePage?: boolean,
): CoreCourseModuleHandlerData; ): CoreCourseModuleHandlerData;
/** /**
@ -270,7 +270,7 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
modname: string, modname: string,
module: CoreCourseModuleData | CoreCourseModuleBasicInfo, module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
courseId: number, courseId: number,
sectionId: number, sectionId?: number,
forCoursePage?: boolean, forCoursePage?: boolean,
): CoreCourseModuleHandlerData | undefined { ): CoreCourseModuleHandlerData | undefined {
return this.executeFunctionOnEnabled<CoreCourseModuleHandlerData>( return this.executeFunctionOnEnabled<CoreCourseModuleHandlerData>(

View File

@ -1589,4 +1589,15 @@ type CoreCourseSetFavouriteCoursesWSParams = {
}[]; }[];
}; };
/**
* Any of the possible course data.
*/
export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData; export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData;
/**
* Course data with admin and navigation option availability.
*/
export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & {
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
};

View File

@ -17,7 +17,7 @@ import { Subscription } from 'rxjs';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreTabsComponent } from '@components/tabs/tabs'; import { CoreTab, CoreTabsComponent } from '@components/tabs/tabs';
import { CoreMainMenuHomeDelegate, CoreMainMenuHomeHandlerToDisplay } from '../../services/home-delegate'; import { CoreMainMenuHomeDelegate, CoreMainMenuHomeHandlerToDisplay } from '../../services/home-delegate';
/** /**
@ -33,7 +33,7 @@ export class CoreMainMenuHomePage implements OnInit {
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent; @ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
siteName!: string; siteName!: string;
tabs: CoreMainMenuHomeHandlerToDisplay[] = []; tabs: CoreTab[] = [];
loaded = false; loaded = false;
selectedTab?: number; selectedTab?: number;
@ -68,9 +68,10 @@ export class CoreMainMenuHomePage implements OnInit {
const tab = this.tabs.find((tab) => tab.title == handler.title); const tab = this.tabs.find((tab) => tab.title == handler.title);
return tab || handler; return tab || handler;
}) });
// Sort them by priority so new handlers are in the right position. // Sort them by priority so new handlers are in the right position.
.sort((a, b) => (b.priority || 0) - (a.priority || 0)); newTabs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
if (typeof this.selectedTab == 'undefined' && newTabs.length > 0) { if (typeof this.selectedTab == 'undefined' && newTabs.length > 0) {
let maxPriority = 0; let maxPriority = 0;

View File

@ -131,15 +131,14 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
// Check "Include a topic section" setting from numsections. // Check "Include a topic section" setting from numsections.
this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined; this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
if (this.section) { if (this.section) {
this.section.hasContent = false; const result = CoreCourseHelper.instance.addHandlerDataForModules(
this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section);
this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules(
[this.section], [this.section],
this.siteHomeId, this.siteHomeId,
undefined, undefined,
undefined, undefined,
true, true,
) || this.hasContent; );
this.hasContent = result.hasContent || this.hasContent;
} }
// Add log in Moodle. // Add log in Moodle.

View File

@ -264,10 +264,25 @@ export type CoreEventFormActionData = CoreEventSiteData & {
online?: boolean; // Whether the data was sent to server or not. Only when submitting. online?: boolean; // Whether the data was sent to server or not. Only when submitting.
}; };
/** /**
* Data passed to NOTIFICATION_SOUND_CHANGED event. * Data passed to NOTIFICATION_SOUND_CHANGED event.
*/ */
export type CoreEventNotificationSoundChangedData = CoreEventSiteData & { export type CoreEventNotificationSoundChangedData = CoreEventSiteData & {
enabled: boolean; enabled: boolean;
}; };
/**
* Data passed to SELECT_COURSE_TAB event.
*/
export type CoreEventSelectCourseTabData = CoreEventSiteData & {
name?: string; // Name of the tab's handler. If not set, load course contents.
sectionId?: number;
sectionNumber?: number;
};
/**
* Data passed to COMPLETION_MODULE_VIEWED event.
*/
export type CoreEventCompletionModuleViewedData = CoreEventSiteData & {
courseId?: number;
};