MOBILE-3659 course: Implement index and contents pages
parent
a91c042cc2
commit
183a033dc8
|
@ -106,7 +106,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
protected unregisterBackButtonAction: any;
|
||||
protected languageChangedSubscription: Subscription;
|
||||
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 stackEventsSubscription?: Subscription;
|
||||
|
||||
|
@ -338,7 +338,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
return;
|
||||
}
|
||||
|
||||
this.firstSelectedTab = selectedTab.id;
|
||||
this.firstSelectedTab = selectedTab.id!;
|
||||
this.selectTab(this.firstSelectedTab);
|
||||
|
||||
// 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.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
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 (this.selected) {
|
||||
// Invalid index do not change tab.
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -568,12 +581,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
index = 0;
|
||||
}
|
||||
|
||||
const selectedTab = this.tabs[index];
|
||||
if (tabId == this.selected || !selectedTab || !selectedTab.enabled) {
|
||||
const tabToSelect = this.tabs[index];
|
||||
if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
|
||||
// Already selected or not enabled.
|
||||
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -583,17 +595,17 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
}
|
||||
|
||||
const pageParams: NavigationOptions = {};
|
||||
if (selectedTab.pageParams) {
|
||||
pageParams.queryParams = selectedTab.pageParams;
|
||||
if (tabToSelect.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) {
|
||||
this.selectHistory.push(tabId);
|
||||
this.selected = tabId;
|
||||
this.selectHistory.push(tabToSelect.id!);
|
||||
this.selected = tabToSelect.id;
|
||||
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.
|
||||
*/
|
||||
class CoreTab {
|
||||
|
||||
id = ''; // Unique tab id.
|
||||
class = ''; // Class, if needed.
|
||||
title = ''; // The translatable tab title.
|
||||
export type CoreTab = {
|
||||
page: string; // Page to navigate to.
|
||||
title: string; // The translatable tab title.
|
||||
id?: string; // Unique tab id.
|
||||
class?: string; // Class, if needed.
|
||||
icon?: string; // The tab icon.
|
||||
badge?: string; // A badge to add in the tab.
|
||||
badgeStyle?: string; // The badge color.
|
||||
enabled = true; // Whether the tab is enabled.
|
||||
page = ''; // Page to navigate to.
|
||||
enabled?: boolean; // Whether the tab is enabled.
|
||||
pageParams?: Params; // Page params.
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="ion-padding">
|
||||
<core-course-module-description [description]="module && module.description" contextLevel="module"
|
||||
[contextInstanceId]="module.id" [courseId]="courseId">
|
||||
<core-course-module-description [description]="module?.description" contextLevel="module"
|
||||
[contextInstanceId]="module?.id" [courseId]="courseId">
|
||||
</core-course-module-description>
|
||||
|
||||
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
||||
|
|
|
@ -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 {}
|
|
@ -13,18 +13,38 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreCourseComponentsModule } from './components/components.module';
|
||||
import { CoreCourseFormatModule } from './format/formats.module';
|
||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
|
||||
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({
|
||||
imports: [
|
||||
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
CoreCourseFormatModule,
|
||||
CoreCourseComponentsModule,
|
||||
],
|
||||
exports: [CoreCourseIndexRoutingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
|
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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;
|
||||
};
|
|
@ -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 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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 {}
|
|
@ -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;
|
||||
};
|
|
@ -122,7 +122,6 @@ export class CoreCourseHelperProvider {
|
|||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
|
||||
this.logger = CoreLogger.getInstance('CoreCourseHelperProvider');
|
||||
}
|
||||
|
||||
|
@ -138,17 +137,24 @@ export class CoreCourseHelperProvider {
|
|||
* @return Whether the sections have content.
|
||||
*/
|
||||
addHandlerDataForModules(
|
||||
sections: CoreCourseSectionFormatted[],
|
||||
sections: CoreCourseSection[],
|
||||
courseId: number,
|
||||
completionStatus?: Record<string, CoreCourseCompletionActivityStatus>,
|
||||
courseName?: string,
|
||||
forCoursePage = false,
|
||||
): boolean {
|
||||
): { hasContent: boolean; sections: CoreCourseSectionFormatted[] } {
|
||||
|
||||
const formattedSections: CoreCourseSectionFormatted[] = sections;
|
||||
let hasContent = false;
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (!section || !this.sectionHasContent(section) || !section.modules) {
|
||||
formattedSections.forEach((section) => {
|
||||
if (!section || !section.modules) {
|
||||
return;
|
||||
}
|
||||
|
||||
section.hasContent = this.sectionHasContent(section);
|
||||
|
||||
if (!section.hasContent) {
|
||||
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 True if module can be opened, false otherwise.
|
||||
*/
|
||||
openModule(): void {
|
||||
// @todo params and logic
|
||||
openModule(module: CoreCourseModuleDataFormatted, courseId: number, sectionId?: number, modParams?: Params): boolean {
|
||||
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.
|
||||
*/
|
||||
export type CoreCourseSectionFormatted = Omit<CoreCourseSection, 'modules'> & {
|
||||
hasContent?: boolean;
|
||||
modules: CoreCourseModuleDataFormatted[];
|
||||
};
|
||||
|
||||
|
|
|
@ -13,12 +13,18 @@
|
|||
// limitations under the License.
|
||||
// @todo test delegate
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
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 { Params } from '@angular/router';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
@ -61,7 +67,7 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
|||
* @return Data or promise resolved with the data.
|
||||
*/
|
||||
getDisplayData?(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseDataWithOptions,
|
||||
): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>;
|
||||
|
||||
/**
|
||||
|
@ -98,7 +104,7 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler {
|
|||
* @return Data or promise resolved with data.
|
||||
*/
|
||||
getMenuDisplayData(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseDataWithOptions,
|
||||
): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>;
|
||||
}
|
||||
|
||||
|
@ -117,15 +123,14 @@ export interface CoreCourseOptionsHandlerData {
|
|||
class?: string;
|
||||
|
||||
/**
|
||||
* The component to render the handler. It must be the component class, not the name or an instance.
|
||||
* When the component is created, it will receive the courseId as input.
|
||||
* Path of the page to load for the handler.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Name of the page to load for the handler.
|
||||
* Path of the page to load for the handler.
|
||||
*/
|
||||
page: string;
|
||||
|
||||
|
@ -161,22 +166,12 @@ export interface CoreCourseOptionsMenuHandlerData {
|
|||
/**
|
||||
* Data returned by the delegate for each handler.
|
||||
*/
|
||||
export interface CoreCourseOptionsHandlerToDisplay {
|
||||
export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay {
|
||||
/**
|
||||
* Data to display.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -368,7 +363,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @return Promise resolved with array of handlers.
|
||||
*/
|
||||
getHandlersToDisplay(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
|
@ -390,7 +385,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @return Promise resolved with array of handlers.
|
||||
*/
|
||||
getMenuHandlersToDisplay(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
|
@ -414,28 +409,31 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
*/
|
||||
protected async getHandlersToDisplayInternal(
|
||||
menu: boolean,
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> {
|
||||
|
||||
const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course;
|
||||
const accessData = {
|
||||
type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT,
|
||||
};
|
||||
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
||||
|
||||
if (navOptions) {
|
||||
course.navOptions = navOptions;
|
||||
courseWithOptions.navOptions = navOptions;
|
||||
}
|
||||
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.
|
||||
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>[] = [];
|
||||
|
||||
let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[];
|
||||
|
@ -450,7 +448,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
|
||||
: (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({
|
||||
data: data,
|
||||
priority: handler.priority,
|
||||
|
@ -587,7 +585,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @param refresh True if it should refresh the list.
|
||||
* @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() &&
|
||||
(typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) {
|
||||
|
||||
|
|
|
@ -981,7 +981,7 @@ export class CoreCourseProvider {
|
|||
* @return Promise resolved when done.
|
||||
*/
|
||||
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.
|
||||
// @todo await this.sitePluginsProvider.waitFetchPlugins();
|
||||
|
@ -992,14 +992,13 @@ export class CoreCourseProvider {
|
|||
course = result.course;
|
||||
}
|
||||
|
||||
/* @todo
|
||||
if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
||||
if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
||||
// 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();
|
||||
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
// This course uses a custom format plugin, wait for the format plugin to finish loading.
|
||||
try {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { Params } from '@angular/router';
|
||||
|
||||
import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreNavHelper } from '@services/nav-helper';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCourseSection } from '../course';
|
||||
import { CoreCourseFormatHandler } from '../format-delegate';
|
||||
|
@ -175,7 +176,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
|
|||
Object.assign(params, { course: course });
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -49,8 +49,8 @@ export class CoreCourseModuleDefaultHandler implements CoreCourseModuleHandler {
|
|||
getData(
|
||||
module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
|
||||
courseId: 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
|
||||
sectionId?: number, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
forCoursePage?: boolean, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
): CoreCourseModuleHandlerData {
|
||||
// Return the default data.
|
||||
const defaultData: CoreCourseModuleHandlerData = {
|
||||
|
|
|
@ -54,8 +54,8 @@ export interface CoreCourseModuleHandler extends CoreDelegateHandler {
|
|||
getData(
|
||||
module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
|
||||
courseId: number,
|
||||
sectionId: number,
|
||||
forCoursePage: boolean,
|
||||
sectionId?: number,
|
||||
forCoursePage?: boolean,
|
||||
): CoreCourseModuleHandlerData;
|
||||
|
||||
/**
|
||||
|
@ -270,7 +270,7 @@ export class CoreCourseModuleDelegateService extends CoreDelegate<CoreCourseModu
|
|||
modname: string,
|
||||
module: CoreCourseModuleData | CoreCourseModuleBasicInfo,
|
||||
courseId: number,
|
||||
sectionId: number,
|
||||
sectionId?: number,
|
||||
forCoursePage?: boolean,
|
||||
): CoreCourseModuleHandlerData | undefined {
|
||||
return this.executeFunctionOnEnabled<CoreCourseModuleHandlerData>(
|
||||
|
|
|
@ -1589,4 +1589,15 @@ type CoreCourseSetFavouriteCoursesWSParams = {
|
|||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Any of the possible course data.
|
||||
*/
|
||||
export type CoreCourseAnyCourseData = CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData;
|
||||
|
||||
/**
|
||||
* Course data with admin and navigation option availability.
|
||||
*/
|
||||
export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & {
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Subscription } from 'rxjs';
|
|||
|
||||
import { CoreSites } from '@services/sites';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
@ -33,7 +33,7 @@ export class CoreMainMenuHomePage implements OnInit {
|
|||
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
|
||||
|
||||
siteName!: string;
|
||||
tabs: CoreMainMenuHomeHandlerToDisplay[] = [];
|
||||
tabs: CoreTab[] = [];
|
||||
loaded = false;
|
||||
selectedTab?: number;
|
||||
|
||||
|
@ -68,9 +68,10 @@ export class CoreMainMenuHomePage implements OnInit {
|
|||
const tab = this.tabs.find((tab) => tab.title == handler.title);
|
||||
|
||||
return tab || handler;
|
||||
})
|
||||
});
|
||||
|
||||
// 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) {
|
||||
let maxPriority = 0;
|
||||
|
|
|
@ -131,15 +131,14 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
|||
// Check "Include a topic section" setting from numsections.
|
||||
this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
|
||||
if (this.section) {
|
||||
this.section.hasContent = false;
|
||||
this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section);
|
||||
this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules(
|
||||
const result = CoreCourseHelper.instance.addHandlerDataForModules(
|
||||
[this.section],
|
||||
this.siteHomeId,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
) || this.hasContent;
|
||||
);
|
||||
this.hasContent = result.hasContent || this.hasContent;
|
||||
}
|
||||
|
||||
// Add log in Moodle.
|
||||
|
|
|
@ -264,10 +264,25 @@ export type CoreEventFormActionData = CoreEventSiteData & {
|
|||
online?: boolean; // Whether the data was sent to server or not. Only when submitting.
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Data passed to NOTIFICATION_SOUND_CHANGED event.
|
||||
*/
|
||||
export type CoreEventNotificationSoundChangedData = CoreEventSiteData & {
|
||||
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;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue