Merge pull request #2976 from crazyserver/MOBILE-3686

Mobile 3686
main
Dani Palou 2021-10-21 12:57:01 +02:00 committed by GitHub
commit 1ffe48c915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 942 additions and 826 deletions

View File

@ -1551,6 +1551,7 @@
"core.courses.selfenrolment": "local_moodlemobileapp",
"core.courses.sendpaymentbutton": "enrol_paypal",
"core.courses.show": "block_myoverview",
"core.courses.showonlyenrolled": "local_moodlemobileapp",
"core.courses.therearecourses": "moodle",
"core.courses.totalcoursesearchresults": "local_moodlemobileapp",
"core.currentdevice": "local_moodlemobileapp",

View File

@ -170,7 +170,7 @@ export class AddonBlogEntriesPage implements OnInit {
entry.contextInstanceId = entry.userid;
}
entry.summary = CoreTextUtils.instance.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
entry.summary = CoreTextUtils.replacePluginfileUrls(entry.summary, entry.summaryfiles || []);
return CoreUser.getProfile(entry.userid, entry.courseid, true).then((user) => {
entry.user = user;

View File

@ -44,8 +44,8 @@
'addon.messages.muteconversation') | translate" (action)="changeMute($event)" [closeOnClick]="false"
[iconAction]="muteIcon"></core-context-menu-item>
<core-context-menu-item [hidden]="!canDelete || !messages || !messages.length" [priority]="400"
[content]="'addon.messages.showdeletemessages' | translate" (action)="toggleDelete()"
[iconAction]="(showDelete ? 'far-check-square' : 'far-square')"></core-context-menu-item>
[content]="'addon.messages.showdeletemessages' | translate"
iconAction="toggle" [(toggle)]="showDelete"></core-context-menu-item>
<core-context-menu-item [hidden]="!groupMessagingEnabled || !conversationId || isGroup || !messages || !messages.length"
[priority]="200" [content]="'addon.messages.deleteconversation' | translate" (action)="deleteConversation($event)"
[closeOnClick]="false" [iconAction]="deleteIcon"></core-context-menu-item>

View File

@ -1263,13 +1263,6 @@ export class AddonMessagesDiscussionPage implements OnInit, OnDestroy, AfterView
return !nextMessage || nextMessage.useridfrom != message.useridfrom || !!nextMessage.showDate;
}
/**
* Toggles delete state.
*/
toggleDelete(): void {
this.showDelete = !this.showDelete;
}
/**
* View info. If it's an individual conversation, go to the user profile.
* If it's a group conversation, view info about the group.

View File

@ -212,12 +212,12 @@
</ion-item>
<!-- Button to start/continue. -->
<ion-button *ngIf="buttonText && !showStatusSpinner" expand="block" (click)="attemptQuiz()" class="ion-margin">
<ion-button *ngIf="buttonText && !showStatusSpinner" expand="block" (click)="attemptQuiz()" class="ion-margin ion-text-wrap">
{{ buttonText | translate }}
</ion-button>
<!-- Button to open in browser if it cannot be attempted in the app. -->
<ion-button class="ion-margin" *ngIf="!buttonText && ((!hasSupportedQuestions && unsupportedQuestions.length) ||
<ion-button class="ion-text-wrap ion-margin" *ngIf="!buttonText && ((!hasSupportedQuestions && unsupportedQuestions.length) ||
unsupportedRules.length || behaviourSupported === false)" expand="block" [href]="externalUrl" core-link
[showBrowserWarning]="false">
{{ 'core.openinbrowser' | translate }}

View File

@ -118,7 +118,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try {
await this.setStartTime(this.currentSco.id);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
}
}
@ -198,7 +198,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
try {
AddonModScormHelper.convertAttemptToOffline(this.scorm, this.attempt);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
}
this.refreshToc();
@ -303,7 +303,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
this.userData = data;
this.accessInfo = accessInfo;
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
}
}
@ -469,7 +469,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
await AddonModScorm.saveTracks(sco.id, this.attempt, tracks, this.scorm, true);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.error', true);
CoreDomUtils.showErrorModalDefault(error, 'core.error', true);
}
} finally {
// Refresh TOC, some prerequisites might have changed.
@ -510,7 +510,7 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
await this.fetchToc();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_scorm.errorgetscorm', true);
}
}

View File

@ -0,0 +1,52 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable } from '@angular/core';
import { makeSingleton } from '@singletons';
import { CoreSettingsHandler, CoreSettingsHandlerData } from '@features/settings/services/settings-delegate';
/**
* Mange storage settings handler.
*/
@Injectable({ providedIn: 'root' })
export class AddonStorageManagerSettingsHandlerService implements CoreSettingsHandler {
static readonly PAGE_NAME = 'storage';
name = 'AddonStorageManager';
priority = 400;
/**
* @inheritdoc
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* @inheritdoc
*/
getDisplayData(): CoreSettingsHandlerData {
return {
icon: 'fas-archive',
title: 'addon.storagemanager.managestorage',
page: AddonStorageManagerSettingsHandlerService.PAGE_NAME,
class: 'addon-storagemanager-settings-handler',
};
}
}
export const AddonStorageManagerSettingsHandler = makeSingleton(AddonStorageManagerSettingsHandlerService);

View File

@ -17,7 +17,10 @@ import { Routes } from '@angular/router';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreSitePreferencesRoutingModule } from '@features/settings/pages/site/site-routing';
import { CoreSettingsDelegate } from '@features/settings/services/settings-delegate';
import { AddonStorageManagerCourseMenuHandler } from './services/handlers/course-menu';
import { AddonStorageManagerSettingsHandler } from './services/handlers/settings';
const routes: Routes = [
{
@ -30,6 +33,7 @@ const routes: Routes = [
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
CoreMainMenuRoutingModule.forChild({ children: routes }),
CoreSitePreferencesRoutingModule.forChild(routes),
],
exports: [CoreMainMenuRoutingModule],
providers: [
@ -38,6 +42,7 @@ const routes: Routes = [
multi: true,
useValue: () => {
CoreCourseOptionsDelegate.registerHandler(AddonStorageManagerCourseMenuHandler.instance);
CoreSettingsDelegate.registerHandler(AddonStorageManagerSettingsHandler.instance);
},
},
],

View File

@ -119,7 +119,7 @@ export class AppComponent implements OnInit, AfterViewInit {
});
CoreUtils.closeInAppBrowser();
} else if (CoreApp.instance.isAndroid()) {
} else if (CoreApp.isAndroid()) {
// Check if the URL has a custom URL scheme. In Android they need to be opened manually.
const urlScheme = CoreUrlUtils.getUrlProtocol(url);
if (urlScheme && urlScheme !== 'file' && urlScheme !== 'cdvfile') {

View File

@ -38,6 +38,7 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange
@Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
@Input() iconAction?: string; // Name of the icon to show on the right side of the item. Represents the action to do on click.
// If is "spinner" an spinner will be shown.
// If is "toggle" a toggle switch will be shown.
// If no icon or spinner is selected, no action or link will work.
// If href but no iconAction is provided arrow-right will be used.
@Input() iconSlash?: boolean; // Display a red slash over the icon.
@ -52,8 +53,10 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange
@Input() badgeA11yText?: string; // Description for the badge, if needed.
@Input() hidden?: boolean; // Whether the item should be hidden.
@Input() showBrowserWarning = true; // Whether to show a warning before opening browser (for links). Defaults to true.
@Input() toggle = false; // Whether the toggle is on or off.
@Output() action?: EventEmitter<() => void>; // Will emit an event when the item clicked.
@Output() onClosed?: EventEmitter<() => void>; // Will emit an event when the popover is closed because the item was clicked.
@Output() toggleChange = new EventEmitter<boolean>();// Will emit an event when toggle changes to enable 2-way data binding.
protected hasAction = false;
protected destroyed = false;
@ -87,6 +90,21 @@ export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChange
}
}
/**
* Toggle changed.
*
* @param event Event.
*/
toggleChanged(event: Event): void {
if (this.toggle === undefined) {
return;
}
event.preventDefault();
event.stopPropagation();
this.toggleChange.emit(this.toggle);
}
/**
* Component destroyed.
*/

View File

@ -55,6 +55,12 @@ export class CoreContextMenuPopoverComponent {
* @return Return true if success, false if error.
*/
itemClicked(event: Event, item: CoreContextMenuItemComponent): boolean {
if (item.iconAction == 'toggle' && !event.defaultPrevented) {
event.preventDefault();
event.stopPropagation();
item.toggle = !item.toggle;
}
if (!!item.action && item.action.observers.length > 0) {
event.preventDefault();
event.stopPropagation();

View File

@ -11,11 +11,16 @@
<ion-label>
<p class="item-heading"><core-format-text [clean]="true" [text]="item.content" [filter]="false"></core-format-text></p>
</ion-label>
<ion-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction"
[class.icon-slash]="item.iconSlash" slot="end" aria-hidden="true">
</ion-icon>
<ion-spinner *ngIf="(item.href || item.action) && item.iconAction == 'spinner'" slot="end"
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ng-container *ngIf="(item.href || item.action) && item.iconAction">
<ion-icon *ngIf="item.iconAction != 'spinner' && item.iconAction != 'toggle'" [name]="item.iconAction"
[class.icon-slash]="item.iconSlash" slot="end" aria-hidden="true">
</ion-icon>
<ion-spinner *ngIf="item.iconAction == 'spinner'" slot="end"
[attr.aria-label]="'core.loading' | translate">
</ion-spinner>
<ion-toggle *ngIf="item.iconAction == 'toggle'" [(ngModel)]="item.toggle" (ionChange)="item.toggleChanged($event)" slot="end">
</ion-toggle>
</ng-container>
<ion-badge class="{{item.badgeClass}}" slot="end" *ngIf="item.badge">
<span [attr.ara-hidden]="!!item.badgeA11yText">{{item.badge}}</span>
<span class="sr-only" *ngIf="item.badgeA11yText">

View File

@ -1,6 +1,6 @@
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item [hidden]="!displayEnableDownload" [priority]="2000" [iconAction]="downloadEnabledIcon"
<core-context-menu-item [hidden]="!displayEnableDownload" [priority]="2000" iconAction="toggle" [(toggle)]="downloadEnabled"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()">
</core-context-menu-item>
<core-context-menu-item [hidden]="!downloadCourseEnabled" [priority]="1900"

View File

@ -18,7 +18,7 @@ import { IonContent, IonRefresher } 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 { CoreCourses, CoreCourseAnyCourseData, CoreCoursesProvider } from '@features/courses/services/courses';
import {
CoreCourse,
CoreCourseCompletionActivityStatus,
@ -64,7 +64,6 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
dataLoaded = false;
downloadEnabled = false;
downloadEnabledIcon = 'far-square'; // Disabled by default.
downloadCourseEnabled = false;
moduleId?: number;
displayEnableDownload = false;
@ -79,14 +78,34 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
protected formatOptions?: Record<string, unknown>;
protected completionObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected siteUpdatedObserver?: CoreEventObserver;
protected downloadEnabledObserver?: CoreEventObserver;
protected syncObserver?: CoreEventObserver;
protected isDestroyed = false;
protected modulesHaveCompletion = false;
protected isGuest?: boolean;
protected debouncedUpdateCachedCompletion?: () => void; // Update the cached completion after a certain time.
constructor() {
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.displayEnableDownload = !CoreSites.getRequiredCurrentSite().isOfflineDisabled() &&
CoreCourseFormatDelegate.displayEnableDownload(this.course);
this.downloadEnabled = this.displayEnableDownload && this.downloadEnabled;
this.initListeners();
}, CoreSites.getCurrentSiteId());
this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => {
this.downloadEnabled = this.displayEnableDownload && data.enabled;
});
}
/**
* Component being initialized.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
@ -104,10 +123,12 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
this.moduleId = CoreNavigator.getRouteNumberParam('moduleId');
this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest');
this.displayEnableDownload = !CoreSites.getCurrentSite()?.isOfflineDisabled() &&
this.displayEnableDownload = !CoreSites.getRequiredCurrentSite().isOfflineDisabled() &&
CoreCourseFormatDelegate.displayEnableDownload(this.course);
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = this.displayEnableDownload && CoreCourses.getCourseDownloadOptionsEnabled();
this.debouncedUpdateCachedCompletion = CoreUtils.debounce(() => {
if (this.modulesHaveCompletion) {
CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true));
@ -138,7 +159,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* @return Promise resolved when done.
*/
protected async initListeners(): Promise<void> {
if (this.downloadCourseEnabled) {
if (this.downloadCourseEnabled && !this.courseStatusObserver) {
// Listen for changes in course status.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
@ -153,26 +174,30 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
return;
}
this.completionObserver = CoreEvents.on(
CoreEvents.COMPLETION_MODULE_VIEWED,
(data) => {
if (data && data.courseId == this.course.id) {
this.refreshAfterCompletionChange(true);
if (!this.completionObserver) {
this.completionObserver = CoreEvents.on(
CoreEvents.COMPLETION_MODULE_VIEWED,
(data) => {
if (data && data.courseId == this.course.id) {
this.refreshAfterCompletionChange(true);
}
},
);
}
if (!this.syncObserver) {
this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
if (!data || data.courseId != this.course.id) {
return;
}
},
);
this.syncObserver = CoreEvents.on(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
if (!data || data.courseId != this.course.id) {
return;
}
this.refreshAfterCompletionChange(false);
this.refreshAfterCompletionChange(false);
if (data.warnings && data.warnings[0]) {
CoreDomUtils.showErrorModal(data.warnings[0]);
}
});
if (data.warnings && data.warnings[0]) {
CoreDomUtils.showErrorModal(data.warnings[0]);
}
});
}
}
/**
@ -471,8 +496,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
* Toggle download enabled.
*/
toggleDownload(): void {
this.downloadEnabled = !this.downloadEnabled;
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
CoreCourses.setCourseDownloadOptionsEnabled(this.downloadEnabled);
}
/**
@ -517,6 +541,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
this.completionObserver?.off();
this.courseStatusObserver?.off();
this.syncObserver?.off();
this.siteUpdatedObserver?.off();
this.downloadEnabledObserver?.off();
}
/**

View File

@ -6,6 +6,10 @@
<img [src]="course.courseImage" core-external-content alt=""/>
</ion-avatar>
<ion-label>
<h2>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
class="core-course-additional-info">
<span *ngIf="course.categoryname" class="core-course-category">
@ -19,10 +23,6 @@
</core-format-text>
</span>
</p>
<h2>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
<p *ngIf="isEnrolled && course.progress! >= 0 && course.completionusertracked !== false">
<core-progress-bar [progress]="course.progress" a11yText="core.courses.aria:courseprogress"></core-progress-bar>
</p>
@ -34,4 +34,14 @@
slot="end">
</ion-icon>
</ng-container>
<div class="core-button-spinner" *ngIf="isEnrolled && showDownload" slot="end">
<core-download-refresh
[status]="prefetchCourseData.status"
[statusTranslatable]="prefetchCourseData.statusTranslatable"
[enabled]="true"
canTrustDownload="false"
[loading]="prefetchCourseData.loading"
(action)="prefetchCourse()"></core-download-refresh>
</div>
</ion-item>

View File

@ -12,11 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, OnInit } from '@angular/core';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
import { CoreCourseProvider, CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper';
import { CoreNavigator } from '@services/navigator';
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses-helper';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseListItem, CoreCourses } from '../../services/courses';
import { CoreCoursesHelper } from '../../services/courses-helper';
/**
* This directive is meant to display an item for a list of courses.
@ -30,31 +34,49 @@ import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/c
templateUrl: 'core-courses-course-list-item.html',
styleUrls: ['course-list-item.scss'],
})
export class CoreCoursesCourseListItemComponent implements OnInit {
export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges {
@Input() course!: CoreCourseSearchedData & CoreCourseWithImageAndColor & {
completionusertracked?: boolean; // If the user is completion tracked.
progress?: number | null; // Progress percentage.
}; // The course to render.
@Input() course!: CoreCourseListItem; // The course to render.
@Input() showDownload = false; // If true, will show download button.
icons: CoreCoursesEnrolmentIcons[] = [];
isEnrolled = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: '',
statusTranslatable: 'core.loading',
status: '',
loading: true,
};
protected courseStatusObserver?: CoreEventObserver;
protected isDestroyed = false;
/**
* Component being initialized.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
CoreCoursesHelper.loadCourseColorAndImage(this.course);
// Check if the user is enrolled in the course.
try {
const course = await CoreCourses.getUserCourse(this.course.id);
this.course.progress = course.progress;
this.course.completionusertracked = course.completionusertracked;
this.isEnrolled = this.course.progress !== undefined;
this.isEnrolled = true;
} catch {
this.isEnrolled = false;
if (!this.isEnrolled) {
try {
const course = await CoreCourses.getUserCourse(this.course.id);
this.course.progress = course.progress;
this.course.completionusertracked = course.completionusertracked;
this.isEnrolled = true;
if (this.showDownload) {
this.initPrefetchCourse();
}
} catch {
this.isEnrolled = false;
}
}
if (!this.isEnrolled) {
this.icons = [];
this.course.enrollmentmethods.forEach((instance) => {
@ -85,6 +107,15 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
}
}
/**
* @inheritdoc
*/
ngOnChanges(): void {
if (this.showDownload && this.isEnrolled) {
this.initPrefetchCourse();
}
}
/**
* Open a course.
*
@ -101,6 +132,85 @@ export class CoreCoursesCourseListItemComponent implements OnInit {
}
}
/**
* Initialize prefetch course.
*/
async initPrefetchCourse(): Promise<void> {
if (this.courseStatusObserver !== undefined) {
// Already initialized.
return;
}
// Listen for status change in course.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => {
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status);
}
}, CoreSites.getCurrentSiteId());
// Determine course prefetch icon.
const status = await CoreCourse.getCourseStatus(this.course.id);
this.updateCourseStatus(status);
if (this.prefetchCourseData.loading) {
// Course is being downloaded. Get the download promise.
const promise = CoreCourseHelper.getCourseDownloadPromise(this.course.id);
if (promise) {
// There is a download promise. If it fails, show an error.
promise.catch((error) => {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
CoreCourse.setCoursePreviousStatus(this.course.id);
}
}
}
/**
* Update the course status icon and title.
*
* @param status Status to show.
*/
protected updateCourseStatus(status: string): void {
const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);
this.prefetchCourseData.status = statusData.status;
this.prefetchCourseData.icon = statusData.icon;
this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
this.prefetchCourseData.loading = statusData.loading;
}
/**
* Prefetch the course.
*
* @param e Click event.
*/
async prefetchCourse(e?: Event): Promise<void> {
e?.preventDefault();
e?.stopPropagation();
try {
await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.courseStatusObserver?.off();
}
}
/**

View File

@ -48,8 +48,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
@Input() showAll = false; // If true, will show all actions, options, star and progress.
@Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown.
courseStatus = CoreConstants.NOT_DOWNLOADED;
isDownloading = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: '',
statusTranslatable: 'core.loading',
@ -64,6 +62,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
progress = -1;
completionUserTracked: boolean | undefined = false;
protected courseStatus = CoreConstants.NOT_DOWNLOADED;
protected isDestroyed = false;
protected courseStatusObserver?: CoreEventObserver;
protected siteUpdatedObserver?: CoreEventObserver;
@ -109,7 +108,7 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On
* Initialize prefetch course.
*/
async initPrefetchCourse(): Promise<void> {
if (typeof this.courseStatusObserver != 'undefined') {
if (this.courseStatusObserver !== undefined) {
// Already initialized.
return;
}

View File

@ -18,7 +18,7 @@ import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'my',
redirectTo: 'list',
pathMatch: 'full',
},
{
@ -33,22 +33,10 @@ const routes: Routes = [
.then(m => m.CoreCoursesCategoriesPageModule),
},
{
path: 'all',
path: 'list',
loadChildren: () =>
import('./pages/available-courses/available-courses.module')
.then(m => m.CoreCoursesAvailableCoursesPageModule),
},
{
path: 'search',
loadChildren: () =>
import('./pages/search/search.module')
.then(m => m.CoreCoursesSearchPageModule),
},
{
path: 'my',
loadChildren: () =>
import('./pages/my-courses/my-courses.module')
.then(m => m.CoreCoursesMyCoursesPageModule),
import('./pages/list/list.module')
.then(m => m.CoreCoursesListPageModule),
},
];

View File

@ -15,9 +15,12 @@
import { APP_INITIALIZER, NgModule, Type } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreMainMenuRoutingModule } from '@features/mainmenu/mainmenu-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreMainMenuHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module';
import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate';
import { CoreMainMenuDelegate } from '@features/mainmenu/services/mainmenu-delegate';
import { CorePushNotificationsDelegate } from '@features/pushnotifications/services/push-delegate';
import { CoreCoursesProvider } from './services/courses';
import { CoreCoursesHelperProvider } from './services/courses-helper';
@ -28,7 +31,10 @@ import { CoreCoursesIndexLinkHandler } from './services/handlers/courses-index-l
import { CoreDashboardHomeHandler, CoreDashboardHomeHandlerService } from './services/handlers/dashboard-home';
import { CoreCoursesDashboardLinkHandler } from './services/handlers/dashboard-link';
import { CoreCoursesEnrolPushClickHandler } from './services/handlers/enrol-push-click';
import { CoreCoursesMyCoursesHomeHandler, CoreCoursesMyCoursesHomeHandlerService } from './services/handlers/my-courses-home';
import {
CoreCoursesMyCoursesHomeHandler,
CoreCoursesMyCoursesMainMenuHandlerService,
} from './services/handlers/my-courses-mainmenu';
import { CoreCoursesRequestPushClickHandler } from './services/handlers/request-push-click';
export const CORE_COURSES_SERVICES: Type<unknown>[] = [
@ -42,10 +48,6 @@ const mainMenuHomeChildrenRoutes: Routes = [
path: CoreDashboardHomeHandlerService.PAGE_NAME,
loadChildren: () => import('./pages/dashboard/dashboard.module').then(m => m.CoreCoursesDashboardPageModule),
},
{
path: CoreCoursesMyCoursesHomeHandlerService.PAGE_NAME,
loadChildren: () => import('./pages/my-courses/my-courses.module').then(m => m.CoreCoursesMyCoursesPageModule),
},
];
const mainMenuHomeSiblingRoutes: Routes = [
@ -55,20 +57,30 @@ const mainMenuHomeSiblingRoutes: Routes = [
},
];
const mainMenuTabRoutes: Routes = [
{
path: CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME,
loadChildren: () => import('./pages/list/list.module').then(m => m.CoreCoursesListPageModule),
},
];
@NgModule({
imports: [
CoreMainMenuHomeRoutingModule.forChild({
children: mainMenuHomeChildrenRoutes,
siblings: mainMenuHomeSiblingRoutes,
}),
CoreMainMenuRoutingModule.forChild({ children: mainMenuTabRoutes }),
CoreMainMenuTabRoutingModule.forChild(mainMenuTabRoutes),
],
exports: [CoreMainMenuRoutingModule],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
useValue: () => {
CoreMainMenuHomeDelegate.registerHandler(CoreDashboardHomeHandler.instance);
CoreMainMenuHomeDelegate.registerHandler(CoreCoursesMyCoursesHomeHandler.instance);
CoreMainMenuDelegate.registerHandler(CoreCoursesMyCoursesHomeHandler.instance);
CoreContentLinksDelegate.registerHandler(CoreCoursesCourseLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(CoreCoursesIndexLinkHandler.instance);
CoreContentLinksDelegate.registerHandler(CoreCoursesDashboardLinkHandler.instance);

View File

@ -40,6 +40,7 @@
"selfenrolment": "Self enrolment",
"sendpaymentbutton": "Send payment via PayPal",
"show": "Restore to view",
"showonlyenrolled": "Show only my courses",
"therearecourses": "There are {{$a}} courses",
"totalcoursesearchresults": "Total courses: {{$a}}"
}

View File

@ -1,19 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1>{{ 'core.courses.availablecourses' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ng-container *ngIf="courses.length > 0">
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
</ng-container>
<core-empty-box *ngIf="!courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

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

View File

@ -1,78 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
/**
* Page that displays available courses in current site.
*/
@Component({
selector: 'page-core-courses-available-courses',
templateUrl: 'available-courses.html',
})
export class CoreCoursesAvailableCoursesPage implements OnInit {
courses: CoreCourseSearchedData[] = [];
coursesLoaded = false;
/**
* View loaded.
*/
ngOnInit(): void {
this.loadCourses().finally(() => {
this.coursesLoaded = true;
});
}
/**
* Load the courses.
*
* @return Promise resolved when done.
*/
protected async loadCourses(): Promise<void> {
const frontpageCourseId = CoreSites.getCurrentSiteHomeId();
try {
const courses = await CoreCourses.getCoursesByField();
this.courses = courses.filter((course) => course.id != frontpageCourseId);
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: IonRefresher): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCoursesByField());
Promise.all(promises).finally(() => {
this.loadCourses().finally(() => {
refresher?.complete();
});
});
}
}

View File

@ -7,6 +7,16 @@
<core-format-text [text]="title" contextLevel="coursecat" [contextInstanceId]="currentCategory && currentCategory!.id">
</core-format-text>
</h1>
<ion-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="900"
[content]="'core.courses.showonlyenrolled' | translate" (action)="filterEnrolled()"
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
</core-context-menu>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
@ -17,22 +27,18 @@
<ion-item *ngIf="currentCategory" class="ion-text-wrap">
<ion-icon name="fas-folder" slot="start" [attr.aria-label]="'core.category' | translate"></ion-icon>
<ion-label>
<h2>
<core-format-text [text]="currentCategory!.name" contextLevel="coursecat"
[contextInstanceId]="currentCategory!.id"></core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="currentCategory && currentCategory!.description">
<ion-label>
<h2>
<core-format-text [text]="currentCategory!.description" maxHeight="60" contextLevel="coursecat"
[contextInstanceId]="currentCategory!.id"></core-format-text>
</h2>
<p class="item-heading">
<core-format-text [text]="currentCategory.name" contextLevel="coursecat"
[contextInstanceId]="currentCategory.id"></core-format-text>
</p>
<p *ngIf="currentCategory.description">
<core-format-text [text]="currentCategory.description" maxHeight="60" contextLevel="coursecat"
[contextInstanceId]="currentCategory.id"></core-format-text>
</p>
</ion-label>
</ion-item>
<div *ngIf="categories.length > 0">
<ng-container *ngIf="categories.length > 0">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.courses.categories' | translate }}</h2>
@ -48,22 +54,24 @@
</core-format-text>
</h2>
</ion-label>
<ion-badge slot="end" *ngIf="category.coursecount > 0" color="light">
<ion-badge slot="end" *ngIf="!showOnlyEnrolled && category.coursecount > 0" color="light">
<span aria-hidden="true">{{ category.coursecount }}</span>
<span class="sr-only">{{ 'core.courses.therearecourses' | translate:{ $a: category.coursecount } }}</span>
</ion-badge>
</ion-item>
</section>
</div>
</ng-container>
<div *ngIf="courses.length > 0">
<ng-container *ngIf="courses.length > 0">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.courses.courses' | translate }}</h2>
<h2 *ngIf="!showOnlyEnrolled">{{ 'core.courses.courses' | translate }}</h2>
<h2 *ngIf="showOnlyEnrolled">{{ 'core.courses.mycourses' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
</div>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course" [showDownload]="downloadEnabled">
</core-courses-course-list-item>
</ng-container>
<core-empty-box *ngIf="!categories.length && !courses.length" icon="fas-graduation-cap"
[message]="'core.courses.nocoursesyet' | translate">
</core-empty-box>

View File

@ -12,14 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreCategoryData, CoreCourses, CoreCourseSearchedData } from '../../services/courses';
import { CoreCategoryData, CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses';
import { Translate } from '@singletons';
import { CoreNavigator } from '@services/navigator';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Page that displays a list of categories and the courses in the current category if any.
@ -28,25 +29,69 @@ import { CoreNavigator } from '@services/navigator';
selector: 'page-core-courses-categories',
templateUrl: 'categories.html',
})
export class CoreCoursesCategoriesPage implements OnInit {
export class CoreCoursesCategoriesPage implements OnInit, OnDestroy {
title: string;
currentCategory?: CoreCategoryData;
categories: CoreCategoryData[] = [];
courses: CoreCourseSearchedData[] = [];
courses: CoreCourseListItem[] = [];
categoriesLoaded = false;
showOnlyEnrolled = false;
downloadEnabled = false;
downloadCourseEnabled = false;
downloadCoursesEnabled = false;
protected categoryCourses: CoreCourseListItem[] = [];
protected currentSiteId: string;
protected categoryId = 0;
protected myCoursesObserver: CoreEventObserver;
protected siteUpdatedObserver: CoreEventObserver;
protected downloadEnabledObserver: CoreEventObserver;
protected isDestroyed = false;
constructor() {
this.title = Translate.instant('core.courses.categories');
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
// Update list if user enrols in a course.
this.myCoursesObserver = CoreEvents.on(
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
(data) => {
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
this.fetchCategories();
}
},
this.currentSiteId,
);
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && this.downloadEnabled;
}, this.currentSiteId);
this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => {
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled;
});
}
/**
* View loaded.
* @inheritdoc
*/
ngOnInit(): void {
this.categoryId = CoreNavigator.getRouteNumberParam('id') || 0;
this.showOnlyEnrolled = CoreNavigator.getRouteBooleanParam('enrolled') || this.showOnlyEnrolled;
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.downloadEnabled =
(this.downloadCourseEnabled || this.downloadCoursesEnabled) && CoreCourses.getCourseDownloadOptionsEnabled();
this.fetchCategories().finally(() => {
this.categoriesLoaded = true;
@ -87,13 +132,14 @@ export class CoreCoursesCategoriesPage implements OnInit {
this.title = this.currentCategory.name;
try {
this.courses = await CoreCourses.getCoursesByField('category', this.categoryId);
this.categoryCourses = await CoreCourses.getCoursesByField('category', this.categoryId);
await this.filterEnrolled();
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcategories', true);
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcategories', true);
}
}
@ -108,7 +154,7 @@ export class CoreCoursesCategoriesPage implements OnInit {
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourses.invalidateCategories(this.categoryId, true));
promises.push(CoreCourses.invalidateCoursesByField('category', this.categoryId));
promises.push(CoreSites.getCurrentSite()!.invalidateConfig());
promises.push(CoreSites.getRequiredCurrentSite().invalidateConfig());
Promise.all(promises).finally(() => {
this.fetchCategories().finally(() => {
@ -123,7 +169,53 @@ export class CoreCoursesCategoriesPage implements OnInit {
* @param categoryId Category Id.
*/
openCategory(categoryId: number): void {
CoreNavigator.navigateToSitePath('courses/categories/' + categoryId);
CoreNavigator.navigateToSitePath(
'courses/categories/' + categoryId,
{ params: {
enrolled: this.showOnlyEnrolled,
} },
);
}
/**
* Filter my courses or not.
*/
async filterEnrolled(): Promise<void> {
if (!this.showOnlyEnrolled) {
this.courses = this.categoryCourses;
} else {
await Promise.all(this.categoryCourses.map(async (course) => {
const isEnrolled = course.progress !== undefined;
if (!isEnrolled) {
try {
const userCourse = await CoreCourses.getUserCourse(course.id);
course.progress = userCourse.progress;
course.completionusertracked = userCourse.completionusertracked;
} catch {
// Ignore errors.
}
}
}));
this.courses = this.categoryCourses.filter((course) => 'progress' in course);
}
}
/**
* Toggle download enabled.
*/
toggleDownload(): void {
CoreCourses.setCourseDownloadOptionsEnabled(this.downloadEnabled);
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.myCoursesObserver.off();
this.siteUpdatedObserver.off();
this.downloadEnabledObserver.off();
this.isDestroyed = true;
}
}

View File

@ -4,11 +4,11 @@
</ion-button>
<core-context-menu>
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
[iconAction]="downloadEnabledIcon"></core-context-menu-item>
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
[content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>

View File

@ -40,30 +40,39 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
downloadEnabled = false;
downloadCourseEnabled = false;
downloadCoursesEnabled = false;
downloadEnabledIcon = 'far-square';
userId?: number;
blocks: Partial<CoreCourseBlock>[] = [];
loaded = false;
protected updateSiteObserver?: CoreEventObserver;
/**
* Initialize the component.
*/
ngOnInit(): void {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
protected updateSiteObserver: CoreEventObserver;
protected downloadEnabledObserver: CoreEventObserver;
constructor() {
// Refresh the enabled flags if site is updated.
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled);
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && this.downloadEnabled;
}, CoreSites.getCurrentSiteId());
this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => {
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled;
});
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.downloadEnabled =
(this.downloadCourseEnabled || this.downloadCoursesEnabled) && CoreCourses.getCourseDownloadOptionsEnabled();
this.loadContent();
}
@ -139,21 +148,10 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
}
/**
* Toggle download enabled.
* Switch download enabled.
*/
toggleDownload(): void {
this.switchDownload(!this.downloadEnabled);
}
/**
* Convenience function to switch download enabled.
*
* @param enable If enable or disable.
*/
protected switchDownload(enable: boolean): void {
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && enable;
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
CoreEvents.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, { enabled: this.downloadEnabled });
switchDownload(): void {
CoreCourses.setCourseDownloadOptionsEnabled(this.downloadEnabled);
}
/**
@ -167,14 +165,15 @@ export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
* Go to search courses.
*/
async openSearch(): Promise<void> {
CoreNavigator.navigateToSitePath('/courses/search');
CoreNavigator.navigateToSitePath('/courses/list', { params : { mode: 'search' } });
}
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver?.off();
this.updateSiteObserver.off();
this.downloadEnabledObserver.off();
}
}

View File

@ -0,0 +1,51 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1 *ngIf="!showOnlyEnrolled">{{ 'core.courses.availablecourses' | translate }}</h1>
<h1 *ngIf="showOnlyEnrolled">{{ 'core.courses.mycourses' | translate }}</h1>
<ion-buttons slot="end"></ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="downloadCourseEnabled || downloadCoursesEnabled" [priority]="1000"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="900"
[content]="'core.courses.showonlyenrolled' | translate" (action)="toggleEnrolled()"
iconAction="toggle" [(toggle)]="showOnlyEnrolled"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-search-box *ngIf="searchEnabled" (onSubmit)="search($event)" (onClear)="clearSearch()"
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" [autoFocus]="searchMode"
searchArea="CoreCoursesSearch"></core-search-box>
<core-loading [hideUntil]="loaded">
<ng-container *ngIf="searchMode && searchTotal > 0">
<ion-item-divider>
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: searchTotal} }}</h2></ion-label>
</ion-item-divider>
</ng-container>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course" [showDownload]="downloadEnabled">
</core-courses-course-list-item>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreCourses($event)" [error]="loadMoreError">
</core-infinite-loading>
<core-empty-box *ngIf="searchMode && !courses.length" icon="fas-search" [message]="'core.courses.nosearchresults' | translate">
</core-empty-box>
<core-empty-box *ngIf="!searchMode && !courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -19,12 +19,12 @@ import { CoreSharedModule } from '@/core/shared.module';
import { CoreCoursesComponentsModule } from '../../components/components.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreCoursesSearchPage } from './search';
import { CoreCoursesListPage } from './list';
const routes: Routes = [
{
path: '',
component: CoreCoursesSearchPage,
component: CoreCoursesListPage,
},
];
@ -36,8 +36,8 @@ const routes: Routes = [
CoreSearchComponentsModule,
],
declarations: [
CoreCoursesSearchPage,
CoreCoursesListPage,
],
exports: [RouterModule],
})
export class CoreCoursesSearchPageModule { }
export class CoreCoursesListPageModule { }

View File

@ -0,0 +1,313 @@
// (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, OnDestroy, OnInit } from '@angular/core';
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfo } from '@features/courses/services/courses-helper';
import { IonRefresher } from '@ionic/angular';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreCourseBasicSearchedData, CoreCourses, CoreCoursesProvider } from '../../services/courses';
type CoreCoursesListMode = 'search' | 'all' | 'my';
/**
* Page that shows a list of courses.
*/
@Component({
selector: 'page-core-courses-list',
templateUrl: 'list.html',
})
export class CoreCoursesListPage implements OnInit, OnDestroy {
downloadAllCoursesEnabled = false;
searchEnabled = false;
searchMode = false;
searchTotal = 0;
downloadEnabled = false;
downloadCourseEnabled = false;
downloadCoursesEnabled = false;
courses: (CoreCourseBasicSearchedData|CoreEnrolledCourseDataWithExtraInfo)[] = [];
loaded = false;
coursesLoaded = 0;
canLoadMore = false;
loadMoreError = false;
showOnlyEnrolled = false;
protected loadedCourses: (CoreCourseBasicSearchedData|CoreEnrolledCourseDataWithExtraInfo)[] = [];
protected loadCoursesPerPage = 20;
protected currentSiteId: string;
protected frontpageCourseId: number;
protected searchPage = 0;
protected searchText = '';
protected myCoursesObserver: CoreEventObserver;
protected siteUpdatedObserver: CoreEventObserver;
protected downloadEnabledObserver: CoreEventObserver;
protected courseIds = '';
protected isDestroyed = false;
constructor() {
this.currentSiteId = CoreSites.getRequiredCurrentSite().getId();
this.frontpageCourseId = CoreSites.getRequiredCurrentSite().getSiteHomeId();
// Update list if user enrols in a course.
this.myCoursesObserver = CoreEvents.on(
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
(data) => {
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
this.fetchCourses();
}
},
this.currentSiteId,
);
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && this.downloadEnabled;
if (!this.searchEnabled && this.searchMode) {
this.searchMode = false;
this.fetchCourses();
}
}, this.currentSiteId);
this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => {
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && data.enabled;
});
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.downloadEnabled =
(this.downloadCourseEnabled || this.downloadCoursesEnabled) && CoreCourses.getCourseDownloadOptionsEnabled();
const mode = CoreNavigator.getRouteParam<CoreCoursesListMode>('mode') || 'my';
if (mode == 'search') {
this.searchMode = true;
}
if (mode == 'my') {
this.showOnlyEnrolled = true;
}
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
if (!this.searchEnabled) {
this.searchMode = false;
}
this.fetchCourses();
}
/**
* Load the course list.
*
* @return Promise resolved when done.
*/
protected async fetchCourses(): Promise<void> {
try {
if (this.searchMode) {
if (this.searchText) {
await this.search(this.searchText);
}
} else {
await this.loadCourses(true);
}
} finally {
this.loaded = true;
}
}
/**
* Fetch the courses.
*
* @param clearTheList If list needs to be reloaded.
* @return Promise resolved when done.
*/
protected async loadCourses(clearTheList = false): Promise<void> {
this.loadMoreError = false;
try {
if (clearTheList) {
if (this.showOnlyEnrolled) {
this.loadedCourses = await CoreCourses.getUserCourses();
} else {
const courses = await CoreCourses.getCoursesByField();
this.loadedCourses = courses.filter((course) => course.id != this.frontpageCourseId);
}
this.coursesLoaded = 0;
this.courses = [];
}
const addCourses = this.loadedCourses.slice(this.coursesLoaded, this.coursesLoaded + this.loadCoursesPerPage);
await CoreCoursesHelper.loadCoursesExtraInfo(addCourses, true);
this.courses = this.courses.concat(addCourses);
this.courseIds = this.courses.map((course) => course.id).join(',');
this.coursesLoaded = this.courses.length;
this.canLoadMore = this.loadedCourses.length > this.courses.length;
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: IonRefresher): void {
const promises: Promise<void>[] = [];
if (!this.searchMode) {
if (this.showOnlyEnrolled) {
promises.push(CoreCourses.invalidateUserCourses());
} else {
promises.push(CoreCourses.invalidateCoursesByField());
}
if (this.courseIds) {
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds));
}
}
Promise.all(promises).finally(() => {
this.fetchCourses().finally(() => {
refresher?.complete();
});
});
}
/**
* Search a new text.
*
* @param text The text to search.
*/
async search(text: string): Promise<void> {
this.searchMode = true;
this.searchText = text;
this.courses = [];
this.searchPage = 0;
this.searchTotal = 0;
const modal = await CoreDomUtils.showModalLoading('core.searching', true);
await this.searchCourses().finally(() => {
modal.dismiss();
});
}
/**
* Clear search box.
*/
clearSearch(): void {
this.searchText = '';
this.courses = [];
this.searchPage = 0;
this.searchTotal = 0;
this.searchMode = false;
this.loaded = false;
this.fetchCourses();
}
/**
* Load more courses.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
*/
async loadMoreCourses(infiniteComplete?: () => void ): Promise<void> {
try {
if (this.searchMode) {
await this.searchCourses();
} else {
await this.loadCourses();
}
} finally {
infiniteComplete && infiniteComplete();
}
}
/**
* Search courses or load the next page of current search.
*
* @return Promise resolved when done.
*/
protected async searchCourses(): Promise<void> {
this.loadMoreError = false;
try {
const response = await CoreCourses.search(this.searchText, this.searchPage, undefined, this.showOnlyEnrolled);
if (this.searchPage === 0) {
this.courses = response.courses;
} else {
this.courses = this.courses.concat(response.courses);
}
this.searchTotal = response.total;
this.searchPage++;
this.canLoadMore = this.courses.length < this.searchTotal;
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
!this.isDestroyed && CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
}
}
/**
* Toggle show only my courses.
*/
toggleEnrolled(): void {
this.loaded = false;
this.fetchCourses();
}
/**
* Toggle download enabled.
*/
toggleDownload(): void {
CoreCourses.setCourseDownloadOptionsEnabled(this.downloadEnabled);
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.myCoursesObserver.off();
this.siteUpdatedObserver.off();
this.downloadEnabledObserver.off();
this.isDestroyed = true;
}
}

View File

@ -1,54 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1>{{ 'core.courses.mycourses' | translate }}</h1>
<ion-buttons slot="end">
<core-navbar-buttons>
<ion-button *ngIf="searchEnabled" (click)="openSearch()"
[attr.aria-label]="'core.courses.searchcourses' | translate">
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-button [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || downloadAllCoursesLoading"
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
<ion-icon [name]="downloadAllCoursesIcon" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<ion-spinner [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 ||
downloadAllCoursesBadge != '' || !downloadAllCoursesLoading"
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
<ion-badge [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || !downloadAllCoursesLoading ||
downloadAllCoursesBadge == '' || !downloadAllCoursesLoading"
role="progressbar" [attr.aria-valuemax]="downloadAllCoursesTotal"
[attr.aria-valuenow]="downloadAllCoursesCount" [attr.aria-valuetext]="downloadAllCoursesBadgeA11yText">
{{downloadAllCoursesBadge}}
</ion-badge>
</core-navbar-buttons>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event.target)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ion-searchbar #searchbar *ngIf="courses && courses.length > 5" [(ngModel)]="filter" (ionInput)="filterChanged($event)"
(ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
</ion-searchbar>
<ion-grid class="ion-no-padding safe-area-padding">
<ion-row class="ion-no-padding">
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding"
size="12" size-sm="6" size-md="6" size-lg="4" size-xl="4">
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true">
</core-courses-course-progress>
</ion-col>
</ion-row>
</ion-grid>
<core-empty-box *ngIf="!courses || !courses.length" icon="fas-graduation-cap"
[message]="'core.courses.nocourses' | translate">
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
</core-empty-box>
</core-loading>
</ion-content>

View File

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

View File

@ -1,219 +0,0 @@
// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { IonSearchbar, IonRefresher } from '@ionic/angular';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import {
CoreCoursesProvider,
CoreCourses,
} from '../../services/courses';
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper';
import { CoreCourseHelper } from '@features/course/services/course-helper';
import { CoreConstants } from '@/core/constants';
import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate';
import { CoreNavigator } from '@services/navigator';
import { Translate } from '@singletons';
/**
* Page that displays the list of courses the user is enrolled in.
*/
@Component({
selector: 'page-core-courses-my-courses',
templateUrl: 'my-courses.html',
})
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
@ViewChild(IonSearchbar) searchbar!: IonSearchbar;
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
searchEnabled = false;
filter = '';
showFilter = false;
coursesLoaded = false;
downloadAllCoursesIcon = CoreConstants.ICON_NOT_DOWNLOADED;
downloadAllCoursesLoading = false;
downloadAllCoursesBadge = '';
downloadAllCoursesEnabled = false;
downloadAllCoursesCount?: number;
downloadAllCoursesTotal?: number;
downloadAllCoursesBadgeA11yText = '';
protected myCoursesObserver: CoreEventObserver;
protected siteUpdatedObserver: CoreEventObserver;
protected isDestroyed = false;
protected courseIds = '';
constructor() {
// Update list if user enrols in a course.
this.myCoursesObserver = CoreEvents.on(
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
(data) => {
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
this.fetchCourses();
}
},
CoreSites.getCurrentSiteId(),
);
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadAllCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
}, CoreSites.getCurrentSiteId());
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadAllCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.fetchCourses().finally(() => {
this.coursesLoaded = true;
});
}
/**
* Fetch the user courses.
*
* @return Promise resolved when done.
*/
protected async fetchCourses(): Promise<void> {
try {
const courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = await CoreCourses.getUserCourses();
const courseIds = courses.map((course) => course.id);
this.courseIds = courseIds.join(',');
await CoreCoursesHelper.loadCoursesExtraInfo(courses);
const options = await CoreCourses.getCoursesAdminAndNavOptions(courseIds);
courses.forEach((course) => {
course.navOptions = options.navOptions[course.id];
course.admOptions = options.admOptions[course.id];
});
this.courses = courses;
this.filteredCourses = this.courses;
this.filter = '';
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: IonRefresher): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.invalidateUserCourses());
promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions());
if (this.courseIds) {
promises.push(CoreCourses.invalidateCoursesByField('ids', this.courseIds));
}
Promise.all(promises).finally(() => {
this.fetchCourses().finally(() => {
refresher?.complete();
});
});
}
/**
* Show or hide the filter.
*/
switchFilter(): void {
this.filter = '';
this.showFilter = !this.showFilter;
this.filteredCourses = this.courses;
if (this.showFilter) {
setTimeout(() => {
this.searchbar.setFocus();
}, 500);
}
}
/**
* The filter has changed.
*
* @param Received Event.
*/
filterChanged(event?: Event): void {
const target = <HTMLInputElement>event?.target || null;
const newValue = target ? String(target.value).trim().toLowerCase() : null;
if (!newValue || !this.courses) {
this.filteredCourses = this.courses;
} else {
// Use displayname if available, or fullname if not.
if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') {
this.filteredCourses = this.courses.filter((course) => course.displayname!.toLowerCase().indexOf(newValue) > -1);
} else {
this.filteredCourses = this.courses.filter((course) => course.fullname.toLowerCase().indexOf(newValue) > -1);
}
}
}
/**
* Prefetch all the courses.
*
* @return Promise resolved when done.
*/
async prefetchCourses(): Promise<void> {
this.downloadAllCoursesLoading = true;
try {
await CoreCourseHelper.confirmAndPrefetchCourses(this.courses, { onProgress: (progress) => {
this.downloadAllCoursesBadge = progress.count + ' / ' + progress.total;
this.downloadAllCoursesBadgeA11yText =
Translate.instant('core.course.downloadcoursesprogressdescription', progress);
this.downloadAllCoursesCount = progress.count;
this.downloadAllCoursesTotal = progress.total;
} });
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
}
this.downloadAllCoursesBadge = '';
this.downloadAllCoursesLoading = false;
}
/**
* Go to search courses.
*/
openSearch(): void {
CoreNavigator.navigateToSitePath('courses/search');
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
this.myCoursesObserver?.off();
this.siteUpdatedObserver?.off();
}
}

View File

@ -1,23 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<h1>{{ 'core.courses.searchcourses' | translate }}</h1>
</ion-toolbar>
</ion-header>
<ion-content>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch()"
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
searchArea="CoreCoursesSearch"></core-search-box>
<ng-container *ngIf="total > 0">
<ion-item-divider>
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}</h2></ion-label>
</ion-item-divider>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreResults($event)" [error]="loadMoreError">
</core-infinite-loading>
</ng-container>
<core-empty-box *ngIf="total == 0" icon="search" [message]="'core.courses.nosearchresults' | translate"></core-empty-box>
</ion-content>

View File

@ -1,100 +0,0 @@
// (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 } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
/**
* Page that allows searching for courses.
*/
@Component({
selector: 'page-core-courses-search',
templateUrl: 'search.html',
})
export class CoreCoursesSearchPage {
total = 0;
courses: CoreCourseBasicSearchedData[] = [];
canLoadMore = false;
loadMoreError = false;
protected page = 0;
protected currentSearch = '';
/**
* Search a new text.
*
* @param text The text to search.
*/
async search(text: string): Promise<void> {
this.currentSearch = text;
this.courses = [];
this.page = 0;
this.total = 0;
const modal = await CoreDomUtils.showModalLoading('core.searching', true);
this.searchCourses().finally(() => {
modal.dismiss();
});
}
/**
* Clear search box.
*/
clearSearch(): void {
this.currentSearch = '';
this.courses = [];
this.page = 0;
this.total = 0;
}
/**
* Load more results.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
*/
loadMoreResults(infiniteComplete?: () => void ): void {
this.searchCourses().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Search courses or load the next page of current search.
*
* @return Promise resolved when done.
*/
protected async searchCourses(): Promise<void> {
this.loadMoreError = false;
try {
const response = await CoreCourses.search(this.currentSearch, this.page);
if (this.page === 0) {
this.courses = response.courses;
} else {
this.courses = this.courses.concat(response.courses);
}
this.total = response.total;
this.page++;
this.canLoadMore = this.courses.length < this.total;
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorsearching', true);
}
}
}

View File

@ -13,13 +13,13 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreLogger } from '@singletons/logger';
import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { makeSingleton } from '@singletons';
import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
import { CoreEvents } from '@singletons/events';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreCourseWithImageAndColor } from './courses-helper';
const ROOT_CACHE_KEY = 'mmCourses:';
@ -62,12 +62,9 @@ export class CoreCoursesProvider {
static readonly STATE_HIDDEN = 'hidden';
static readonly STATE_FAVOURITE = 'favourite';
protected logger: CoreLogger;
protected userCoursesIds: { [id: number]: boolean } = {}; // Use an object to make it faster to search.
constructor() {
this.logger = CoreLogger.getInstance('CoreCoursesProvider');
}
protected downloadOptionsEnabled = false;
/**
* Whether current site supports getting course options.
@ -1121,6 +1118,7 @@ export class CoreCoursesProvider {
* @param text Text to search.
* @param page Page to get.
* @param perPage Number of courses per page. Defaults to CoreCoursesProvider.SEARCH_PER_PAGE.
* @param limitToEnrolled Limit to enrolled courses.
* @param siteId Site ID. If not defined, use current site.
* @return Promise resolved with the courses and the total of matches.
*/
@ -1128,6 +1126,7 @@ export class CoreCoursesProvider {
text: string,
page: number = 0,
perPage: number = CoreCoursesProvider.SEARCH_PER_PAGE,
limitToEnrolled: boolean = false,
siteId?: string,
): Promise<{ total: number; courses: CoreCourseBasicSearchedData[] }> {
const site = await CoreSites.getSite(siteId);
@ -1136,6 +1135,7 @@ export class CoreCoursesProvider {
criteriavalue: text,
page: page,
perpage: perPage,
limittoenrolled: limitToEnrolled,
};
const preSets: CoreSiteWSPreSets = {
getFromCache: false,
@ -1216,6 +1216,29 @@ export class CoreCoursesProvider {
return site.write('core_course_set_favourite_courses', params);
}
/**
* Get download options enabled option.
*
* @return True if enabled, false otherwise.
*/
getCourseDownloadOptionsEnabled(): boolean {
return this.downloadOptionsEnabled;
}
/**
* Set trigger and save the download option.
*
* @param enable True to enable, false to disable.
*/
setCourseDownloadOptionsEnabled(enable: boolean): void {
if (this.downloadOptionsEnabled == enable) {
return;
}
this.downloadOptionsEnabled = enable;
CoreEvents.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, { enabled: enable });
}
}
export const CoreCourses = makeSingleton(CoreCoursesProvider);
@ -1358,6 +1381,14 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & {
courseformatoptions?: CoreCourseFormatOption[]; // Additional options for particular course format.
};
/**
* Course to render as list item.
*/
export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & {
completionusertracked?: boolean; // If the user is completion tracked.
progress?: number | null; // Progress percentage.
};
export type CoreCourseGetCoursesData = CoreEnrolledCourseBasicData & {
categoryid: number; // Category id.
categorysortorder?: number; // Sort order into the category.

View File

@ -107,7 +107,7 @@ export class CoreCoursesDashboardProvider {
}
/**
* Check if Site Home is disabled in a certain site.
* Check if Dashboard is disabled in a certain site.
*
* @param site Site. If not defined, use current site.
* @return Whether it's disabled.

View File

@ -18,7 +18,7 @@ import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { CoreCoursesMyCoursesHomeHandlerService } from './my-courses-home';
import { CoreCoursesMyCoursesMainMenuHandlerService } from './my-courses-mainmenu';
/**
* Handler to treat links to course index (list of courses).
@ -31,25 +31,22 @@ export class CoreCoursesIndexLinkHandlerService extends CoreContentLinksHandlerB
pattern = /\/course\/?(index\.php.*)?$/;
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @return List of (or promise resolved with list of) actions.
* @inheritdoc
*/
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] {
return [{
action: (siteId): void => {
let pageName = CoreCoursesMyCoursesHomeHandlerService.PAGE_NAME;
let pageName = CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME;
const pageParams: Params = {};
if (params.categoryid) {
pageName += '/categories/' + params.categoryid;
} else {
pageName += '/all';
pageName += '/list';
pageParams.mode = 'all';
}
CoreNavigator.navigateToSitePath(pageName, { siteId });
CoreNavigator.navigateToSitePath(pageName, { params: pageParams, siteId });
},
}];
}

View File

@ -13,39 +13,29 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { CoreMainMenuHomeHandler, CoreMainMenuHomeHandlerToDisplay } from '@features/mainmenu/services/home-delegate';
import { CoreMainMenuHandler, CoreMainMenuHandlerData } from '@features/mainmenu/services/mainmenu-delegate';
import { CoreSiteHomeHomeHandler } from '@features/sitehome/services/handlers/sitehome-home';
import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons';
import { CoreCourses } from '../courses';
import { CoreDashboardHomeHandler } from './dashboard-home';
/**
* Handler to add my courses into home page.
* Handler to add my courses into main menu.
*/
@Injectable({ providedIn: 'root' })
export class CoreCoursesMyCoursesHomeHandlerService implements CoreMainMenuHomeHandler {
export class CoreCoursesMyCoursesMainMenuHandlerService implements CoreMainMenuHandler {
static readonly PAGE_NAME = 'courses';
name = 'CoreCoursesMyCourses';
priority = 900;
priority = 850;
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
* @inheritdoc
*/
isEnabled(): Promise<boolean> {
return this.isEnabledForSite();
}
/**
* Check if the handler is enabled on a certain site.
*
* @param siteId Site ID. If not defined, current site.
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabledForSite(siteId?: string): Promise<boolean> {
async isEnabled(): Promise<boolean> {
const siteId = CoreSites.getCurrentSiteId();
const disabled = await CoreCourses.isMyCoursesDisabled(siteId);
if (disabled) {
@ -59,20 +49,17 @@ export class CoreCoursesMyCoursesHomeHandlerService implements CoreMainMenuHomeH
}
/**
* Returns the data needed to render the handler.
*
* @return Data needed to render the handler.
* @inheritdoc
*/
getDisplayData(): CoreMainMenuHomeHandlerToDisplay {
getDisplayData(): CoreMainMenuHandlerData {
return {
title: 'core.courses.mycourses',
page: CoreCoursesMyCoursesHomeHandlerService.PAGE_NAME,
page: CoreCoursesMyCoursesMainMenuHandlerService.PAGE_NAME,
class: 'core-courses-my-courses-handler',
icon: 'fas-graduation-cap',
selectPriority: 900,
};
}
}
export const CoreCoursesMyCoursesHomeHandler = makeSingleton(CoreCoursesMyCoursesHomeHandlerService);
export const CoreCoursesMyCoursesHomeHandler = makeSingleton(CoreCoursesMyCoursesMainMenuHandlerService);

View File

@ -3,12 +3,12 @@
<ion-icon name="fas-search" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
<core-context-menu>
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000"
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
[iconAction]="downloadEnabledIcon"></core-context-menu-item>
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
<core-context-menu-item [priority]="1000" *ngIf="displayEnableDownload"
[content]="'core.settings.showdownloadoptions' | translate" (action)="switchDownload()"
iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="500"
[content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>

View File

@ -50,31 +50,32 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
siteHomeId = 1;
currentSite!: CoreSite;
searchEnabled = false;
displayEnableDownload = false;
downloadEnabled = false;
downloadCourseEnabled = false;
downloadCoursesEnabled = false;
downloadEnabledIcon = 'far-square';
newsForumModule?: NewsForum;
protected updateSiteObserver?: CoreEventObserver;
/**
* Page being initialized.
*/
ngOnInit(): void {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
protected updateSiteObserver: CoreEventObserver;
protected downloadEnabledObserver: CoreEventObserver;
constructor() {
// Refresh the enabled flags if site is updated.
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !CoreCourses.isDownloadCoursesDisabledInSite();
this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled);
this.displayEnableDownload = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
}, CoreSites.getCurrentSiteId());
this.downloadEnabledObserver = CoreEvents.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, (data) => {
this.downloadEnabled = data.enabled;
});
}
/**
* @inheritdoc
*/
ngOnInit(): void {
this.searchEnabled = !CoreCourses.isSearchCoursesDisabledInSite();
this.currentSite = CoreSites.getRequiredCurrentSite();
this.siteHomeId = CoreSites.getCurrentSiteHomeId();
@ -84,6 +85,9 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
CoreCourseHelper.openModule(module, this.siteHomeId, undefined, modParams);
}
this.displayEnableDownload = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
this.downloadEnabled = CoreCourses.getCourseDownloadOptionsEnabled();
this.loadContent().finally(() => {
this.dataLoaded = true;
});
@ -190,21 +194,10 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
}
/**
* Toggle download enabled.
* Switch download enabled.
*/
toggleDownload(): void {
this.switchDownload(!this.downloadEnabled);
}
/**
* Convenience function to switch download enabled.
*
* @param enable If enable or disable.
*/
protected switchDownload(enable: boolean): void {
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && enable;
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
CoreEvents.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, { enabled: this.downloadEnabled });
switchDownload(): void {
CoreCourses.setCourseDownloadOptionsEnabled(this.downloadEnabled);
}
/**
@ -218,21 +211,21 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
* Go to search courses.
*/
openSearch(): void {
CoreNavigator.navigateToSitePath('courses/search');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'search' } });
}
/**
* Go to available courses.
*/
openAvailableCourses(): void {
CoreNavigator.navigateToSitePath('courses/all');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'all' } });
}
/**
* Go to my courses.
*/
openMyCourses(): void {
CoreNavigator.navigateToSitePath('courses/my');
CoreNavigator.navigateToSitePath('courses/list', { params : { mode: 'my' } });
}
/**
@ -246,7 +239,8 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
* Component being destroyed.
*/
ngOnDestroy(): void {
this.updateSiteObserver?.off();
this.updateSiteObserver.off();
this.downloadEnabledObserver.off();
}
}

View File

@ -169,20 +169,12 @@ export class CoreSiteHomeProvider {
// Get number of news items to show.
add = !!CoreSites.getCurrentSite()?.getStoredConfig('newsitems');
break;
case FrontPageItemNames['LIST_OF_CATEGORIES']:
case FrontPageItemNames['COMBO_LIST']:
itemNumber = FrontPageItemNames['LIST_OF_CATEGORIES']; // Do not break here.
case FrontPageItemNames['LIST_OF_CATEGORIES']:
case FrontPageItemNames['LIST_OF_COURSE']:
add = true;
if (itemNumber == FrontPageItemNames['COMBO_LIST']) {
itemNumber = FrontPageItemNames['LIST_OF_CATEGORIES'];
}
break;
case FrontPageItemNames['ENROLLED_COURSES']:
if (!CoreCourses.isMyCoursesDisabledInSite()) {
const courses = await CoreCourses.getUserCourses();
add = courses.length > 0;
}
add = true;
break;
case FrontPageItemNames['COURSE_SEARCH_BOX']:
add = !CoreCourses.isSearchCoursesDisabledInSite();

View File

@ -124,7 +124,7 @@ export class CoreSitePluginsCallWSBaseDirective implements OnInit, OnDestroy {
invalidate(): Promise<void> {
const params = this.getParamsForWS();
return CoreSitePlugins.instance.invalidateCallWS(this.name, params, this.preSets);
return CoreSitePlugins.invalidateCallWS(this.name, params, this.preSets);
}
/**