diff --git a/src/core/features/block/components/side-blocks/side-blocks.html b/src/core/features/block/components/side-blocks/side-blocks.html
index 198cb21d2..91fee4e1e 100644
--- a/src/core/features/block/components/side-blocks/side-blocks.html
+++ b/src/core/features/block/components/side-blocks/side-blocks.html
@@ -1,8 +1,5 @@
-
+
-
- {{ 'core.block.blocks' | translate }}
-
@@ -10,7 +7,7 @@
-
+
diff --git a/src/core/features/course/classes/main-activity-component.ts b/src/core/features/course/classes/main-activity-component.ts
index 1c2621036..d0214e996 100644
--- a/src/core/features/course/classes/main-activity-component.ts
+++ b/src/core/features/course/classes/main-activity-component.ts
@@ -17,15 +17,11 @@ import { IonContent } from '@ionic/angular';
import { CoreCourseModuleMainResourceComponent } from './main-resource-component';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
-import { Network, NgZone } from '@singletons';
-import { Subscription } from 'rxjs';
-import { CoreApp } from '@services/app';
import { CoreCourse } from '../services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseContentsPage } from '../pages/contents/contents';
-import { CoreConstants } from '@/core/constants';
import { CoreSites } from '@services/sites';
/**
@@ -40,13 +36,7 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
moduleName?: string; // Raw module name to be translated. It will be translated on init.
- // Data for context menu.
- syncIcon?: string; // Sync icon.
- hasOffline?: boolean; // If it has offline data to be synced.
- isOnline?: boolean; // If the app is online or not.
-
protected syncObserver?: CoreEventObserver; // It will observe the sync auto event.
- protected onlineSubscription: Subscription; // It will observe the status of the network connection.
protected syncEventName?: string; // Auto sync event name.
constructor(
@@ -55,14 +45,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
courseContentsPage?: CoreCourseContentsPage,
) {
super(loggerName, courseContentsPage);
-
- // Refresh online status when changes.
- this.onlineSubscription = Network.onChange().subscribe(() => {
- // Execute the callback in the Angular zone, so change detection doesn't stop working.
- NgZone.run(() => {
- this.isOnline = CoreApp.isOnline();
- });
- });
}
/**
@@ -72,7 +54,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
await super.ngOnInit();
this.hasOffline = false;
- this.syncIcon = CoreConstants.ICON_LOADING;
this.moduleName = CoreCourse.translateModuleName(this.moduleName || '');
if (this.syncEventName) {
@@ -119,20 +100,12 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
return;
}
- this.refreshIcon = CoreConstants.ICON_LOADING;
- this.syncIcon = CoreConstants.ICON_LOADING;
+ await CoreUtils.ignoreErrors(Promise.all([
+ this.invalidateContent(),
+ this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
+ ]));
- try {
- await CoreUtils.ignoreErrors(Promise.all([
- this.invalidateContent(),
- this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
- ]));
-
- await this.loadContent(true, sync, showErrors);
- } finally {
- this.refreshIcon = CoreConstants.ICON_REFRESH;
- this.syncIcon = CoreConstants.ICON_SYNC;
- }
+ await this.loadContent(true, sync, showErrors);
}
/**
@@ -143,17 +116,10 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
* @return Resolved when done.
*/
protected async showLoadingAndFetch(sync: boolean = false, showErrors: boolean = false): Promise {
- this.refreshIcon = CoreConstants.ICON_LOADING;
- this.syncIcon = CoreConstants.ICON_LOADING;
this.loaded = false;
this.content?.scrollToTop();
- try {
- await this.loadContent(false, sync, showErrors);
- } finally {
- this.refreshIcon = CoreConstants.ICON_REFRESH;
- this.syncIcon = CoreConstants.ICON_REFRESH;
- }
+ await this.loadContent(false, sync, showErrors);
}
/**
@@ -164,8 +130,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
* @return Resolved when done.
*/
protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise {
- this.refreshIcon = CoreConstants.ICON_LOADING;
- this.syncIcon = CoreConstants.ICON_LOADING;
this.loaded = false;
this.content?.scrollToTop();
@@ -194,8 +158,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
* @return Promise resolved when done.
*/
protected async loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise {
- this.isOnline = CoreApp.isOnline();
-
if (!this.module) {
// This can happen if course format changes from single activity to weekly/topics.
return;
@@ -216,8 +178,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
} finally {
this.loaded = true;
- this.refreshIcon = CoreConstants.ICON_REFRESH;
- this.syncIcon = CoreConstants.ICON_REFRESH;
}
}
@@ -270,8 +230,6 @@ export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainR
*/
ngOnDestroy(): void {
super.ngOnDestroy();
-
- this.onlineSubscription?.unsubscribe();
this.syncObserver?.off();
}
diff --git a/src/core/features/course/classes/main-resource-component.ts b/src/core/features/course/classes/main-resource-component.ts
index 1cf1b4a0b..6536145e0 100644
--- a/src/core/features/course/classes/main-resource-component.ts
+++ b/src/core/features/course/classes/main-resource-component.ts
@@ -13,14 +13,10 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
-import { AddonBlog } from '@addons/blog/services/blog';
-import { AddonBlogMainMenuHandlerService } from '@addons/blog/services/handlers/mainmenu';
import { OnInit, OnDestroy, Input, Output, EventEmitter, Component, Optional, Inject } from '@angular/core';
-import { Params } from '@angular/router';
import { CoreAnyError } from '@classes/errors/error';
import { IonRefresher } from '@ionic/angular';
import { CoreApp } from '@services/app';
-import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
@@ -29,6 +25,7 @@ import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreLogger } from '@singletons/logger';
+import { CoreCourseModuleSummaryComponent, CoreCourseModuleSummaryResult } from '../components/module-summary/module-summary';
import { CoreCourseContentsPage } from '../pages/contents/contents';
import { CoreCourse } from '../services/course';
import { CoreCourseHelper, CoreCourseModuleData } from '../services/course-helper';
@@ -58,30 +55,23 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
loaded = false; // If the component has been loaded.
component?: string; // Component name.
componentId?: number; // Component ID.
- blog?: boolean; // If blog is available.
+ hasOffline = false; // Resources don't have any data to sync.
- // Data for context menu.
- externalUrl?: string; // External URL to open in browser.
description?: string; // Module description.
- refreshIcon = CoreConstants.ICON_LOADING; // Refresh icon, normally spinner or refresh.
- prefetchStatusIcon?: string; // Used when calling fillContextMenu.
- prefetchStatus?: string; // Used when calling fillContextMenu.
- prefetchText?: string; // Used when calling fillContextMenu.
- size?: string; // Used when calling fillContextMenu.
- downloadTimeReadable?: string; // Last download time in a readable format. Used when calling fillContextMenu.
- isDestroyed = false; // Whether the component is destroyed, used when calling fillContextMenu.
- contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
- contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
protected isCurrentView = false; // Whether the component is in the current view.
protected siteId?: string; // Current Site ID.
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
- protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
+ currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
+ downloadTimeReadable?: string; // Last download time in a readable format. Only if setStatusListener is called.
+
protected completionObserver?: CoreEventObserver;
protected logger: CoreLogger;
protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
protected showCompletion = false; // Whether to show completion inside the activity.
+ protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary.
+ protected isDestroyed = false; // Whether the component is destroyed.
constructor(
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
@@ -91,13 +81,12 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}
/**
- * Component being initialized.
+ * @inheritdoc
*/
async ngOnInit(): Promise {
this.siteId = CoreSites.getCurrentSiteId();
this.description = this.module.description;
this.componentId = this.module.id;
- this.externalUrl = this.module.url;
this.courseId = this.courseId || this.module.course;
this.showCompletion = !!CoreSites.getRequiredCurrentSite().isVersionGreaterEqualThan('3.11');
@@ -116,20 +105,17 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
this.fetchModule();
}, 10000);
}
-
- this.blog = await AddonBlog.isPluginEnabled();
}
/**
* Refresh the data.
*
* @param refresher Refresher.
- * @param done Function to call when done.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
- async doRefresh(refresher?: IonRefresher | null, done?: () => void, showErrors: boolean = false): Promise {
- if (!this.loaded || !this.module) {
+ async doRefresh(refresher?: IonRefresher | null, showErrors = false): Promise {
+ if (!this.module) {
// Module can be undefined if course format changes from single activity to weekly/topics.
return;
}
@@ -143,7 +129,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
await CoreUtils.ignoreErrors(this.refreshContent(true, showErrors));
refresher?.complete();
- done && done();
}
/**
@@ -160,22 +145,16 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
return;
}
- this.refreshIcon = CoreConstants.ICON_LOADING;
+ await CoreUtils.ignoreErrors(Promise.all([
+ this.invalidateContent(),
+ this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
+ ]));
- try {
- await CoreUtils.ignoreErrors(Promise.all([
- this.invalidateContent(),
- this.showCompletion ? CoreCourse.invalidateModule(this.module.id) : undefined,
- ]));
-
- if (this.showCompletion) {
- this.fetchModule();
- }
-
- await this.loadContent(true);
- } finally {
- this.refreshIcon = CoreConstants.ICON_REFRESH;
+ if (this.showCompletion) {
+ this.fetchModule();
}
+
+ await this.loadContent(true);
}
/**
@@ -221,7 +200,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
CoreDomUtils.showErrorModalDefault(error, this.fetchContentDefaultError, true);
} finally {
this.loaded = true;
- this.refreshIcon = CoreConstants.ICON_REFRESH;
}
}
@@ -236,66 +214,25 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
}
/**
- * Fill the context menu options
+ * Updage package last downloaded.
*/
- protected fillContextMenu(refresh: boolean = false): void {
- // All data obtained, now fill the context menu.
- CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component);
- }
-
- /**
- * Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu.
- * This means that you need to call fillContextMenu to make this work.
- */
- protected isPrefetched(): boolean {
- return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED;
- }
-
- /**
- * Expand the description.
- */
- expandDescription(): void {
- CoreTextUtils.viewText(Translate.instant('core.description'), this.description!, {
- component: this.component,
- componentId: this.module.id,
- filter: true,
- contextLevel: 'module',
- instanceId: this.module.id,
- courseId: this.courseId,
- });
- }
-
- /**
- * Go to blog posts.
- */
- async gotoBlog(): Promise {
- const params: Params = { cmId: this.module.id };
-
- await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
- }
-
- /**
- * Prefetch the module.
- *
- * @param done Function to call when done.
- */
- prefetch(done?: () => void): void {
- CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId, done);
- }
-
- /**
- * Confirm and remove downloaded files.
- *
- * @param done Function to call when done.
- */
- removeFiles(done?: () => void): void {
- if (this.prefetchStatus == CoreConstants.DOWNLOADING) {
- CoreDomUtils.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading');
-
+ protected async getPackageLastDownloaded(): Promise {
+ if (!this.module) {
return;
}
- CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId, done);
+ const lastDownloaded =
+ await CoreCourseHelper.getModulePackageLastDownloaded(this.module, this.component);
+
+ this.downloadTimeReadable = CoreTextUtils.ucFirst(lastDownloaded.downloadTimeReadable);
+ }
+
+ /**
+ * Check if the module is prefetched or being prefetched.
+ * To make it faster, just use the data calculated by setStatusListener.
+ */
+ protected isPrefetched(): boolean {
+ return this.currentStatus != CoreConstants.NOT_DOWNLOADABLE && this.currentStatus != CoreConstants.NOT_DOWNLOADED;
}
/**
@@ -355,6 +292,8 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
const previousStatus = this.currentStatus;
this.currentStatus = data.status;
+ this.getPackageLastDownloaded();
+
this.showStatus(this.currentStatus, previousStatus);
}, this.siteId);
} else if (!refresh) {
@@ -369,6 +308,9 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(this.module, this.courseId, undefined, refresh);
this.currentStatus = status;
+
+ this.getPackageLastDownloaded();
+
this.showStatus(status);
}
@@ -449,13 +391,47 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
this.module = module;
}
+ /**
+ * Opens a module summary page.
+ */
+ async openModuleSummary(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ const data = await CoreDomUtils.openSideModal({
+ component: CoreCourseModuleSummaryComponent,
+ componentProps: {
+ moduleId: this.module.id,
+ module: this.module,
+ description: this.description,
+ component: this.component,
+ courseId: this.courseId,
+ hasOffline: this.hasOffline,
+ displayOptions: {
+ // Show description on summary if not shown on the page.
+ displayDescription: !this.displayDescription,
+ },
+ },
+ });
+
+ if (data) {
+ if (this.loaded && (data.action == 'refresh' || data.action == 'sync')) {
+ this.loaded = false;
+ try {
+ await this.doRefresh(undefined, data.action == 'sync');
+ } finally {
+ this.loaded = true;
+ }
+ }
+ }
+ }
+
/**
* Component being destroyed.
*/
ngOnDestroy(): void {
this.isDestroyed = true;
- this.contextMenuStatusObserver?.off();
- this.contextFileStatusObserver?.off();
this.statusObserver?.off();
this.completionObserver?.off();
}
diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts
index 739dc216e..5eee268bf 100644
--- a/src/core/features/course/components/components.module.ts
+++ b/src/core/features/course/components/components.module.ts
@@ -16,7 +16,7 @@ import { NgModule } from '@angular/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
-import { CoreCourseFormatComponent } from './format/format';
+import { CoreCourseFormatComponent } from './course-format/course-format';
import { CoreCourseModuleComponent } from './module/module';
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
@@ -27,6 +27,7 @@ import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-l
import { CoreCourseModuleInfoComponent } from './module-info/module-info';
import { CoreCourseModuleManualCompletionComponent } from './module-manual-completion/module-manual-completion';
import { CoreCourseModuleNavigationComponent } from './module-navigation/module-navigation';
+import { CoreCourseModuleSummaryComponent } from './module-summary/module-summary';
@NgModule({
declarations: [
@@ -41,6 +42,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
+ CoreCourseModuleSummaryComponent,
],
imports: [
CoreBlockComponentsModule,
@@ -58,6 +60,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module-
CoreCourseTagAreaComponent,
CoreCourseUnsupportedModuleComponent,
CoreCourseModuleNavigationComponent,
+ CoreCourseModuleSummaryComponent,
],
})
export class CoreCourseComponentsModule {}
diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/course-format/course-format.html
similarity index 100%
rename from src/core/features/course/components/format/core-course-format.html
rename to src/core/features/course/components/course-format/course-format.html
diff --git a/src/core/features/course/components/format/format.scss b/src/core/features/course/components/course-format/course-format.scss
similarity index 100%
rename from src/core/features/course/components/format/format.scss
rename to src/core/features/course/components/course-format/course-format.scss
diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/course-format/course-format.ts
similarity index 99%
rename from src/core/features/course/components/format/format.ts
rename to src/core/features/course/components/course-format/course-format.ts
index 2d5067cdc..5b6797f98 100644
--- a/src/core/features/course/components/format/format.ts
+++ b/src/core/features/course/components/course-format/course-format.ts
@@ -55,8 +55,8 @@ import { CoreCourseModuleDelegate } from '@features/course/services/module-deleg
*/
@Component({
selector: 'core-course-format',
- templateUrl: 'core-course-format.html',
- styleUrls: ['format.scss'],
+ templateUrl: 'course-format.html',
+ styleUrls: ['course-format.scss'],
})
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
diff --git a/src/core/features/course/components/module-navigation/module-navigation.ts b/src/core/features/course/components/module-navigation/module-navigation.ts
index 2b3573738..3a566c701 100644
--- a/src/core/features/course/components/module-navigation/module-navigation.ts
+++ b/src/core/features/course/components/module-navigation/module-navigation.ts
@@ -110,7 +110,7 @@ export class CoreCourseModuleNavigationComponent implements OnInit, OnDestroy {
return;
}
// Set a minimum height value.
- this.initialHeight = this.initialHeight || 56;
+ this.initialHeight = this.initialHeight || 48;
this.previousHeight = this.initialHeight;
this.content = this.element.closest('ion-content');
diff --git a/src/core/features/course/components/module-summary/module-summary.html b/src/core/features/course/components/module-summary/module-summary.html
new file mode 100644
index 000000000..820e944ef
--- /dev/null
+++ b/src/core/features/course/components/module-summary/module-summary.html
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{moduleNameTranslated}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.course' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.description' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'addon.storagemanager.downloads' | translate }}
+
+
+
+
+
+ {{ 'addon.storagemanager.totalspaceusage' | translate }}
+ {{ sizeReadable | coreBytesToSize }}
+
+
+
+
+
+
+
+
+ {{ 'core.lastdownloaded' | translate }} {{ downloadTimeReadable }}
+
+
+
+
+
+
+ {{ 'core.download' | translate }}
+
+
+
+
+ 0">
+
+
+
+
+ {{ 'core.grades.gradebook' | translate
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.grades.grade' | translate}}
+
+
+ Not graded
+
+
+
+
+
+
+
+
+
0 && grade.weight != '-'">
+
+ {{ 'core.grades.weight' | translate}}
+
+
+
+
0 && grade.range != '-'">
+
+ {{ 'core.grades.range' | translate}}
+
+
+
+
+
0 && grade.percentage != '-'">
+
+ {{ 'core.grades.percentage' | translate}}
+
+
+
+
+
0 && grade.lettergrade != '-'">
+
+ {{ 'core.grades.lettergrade' | translate}}
+
+
+
+
+
0 && grade.rank != '-'">
+
+ {{ 'core.grades.rank' | translate}}
+
+
+
+
+
0 && grade.average != '-'">
+
+ {{ 'core.grades.average' | translate}}
+
+
+
+
+
0 && grade.feedback != '-'">
+
+ {{ 'core.grades.feedback' | translate}}
+
+
+
+
+
+
+
+
0 && grade.contributiontocoursetotal != '-'">
+
+ {{ 'core.grades.contributiontocoursetotal' | translate}}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'addon.blog.blog' | translate }}
+
+
+
+
+
+
+
+
+ {{ 'core.refresh' | translate }}
+
+
+
+
+
+
+ {{ 'core.settings.synchronizenow' | translate }}
+
+
+
diff --git a/src/core/features/course/components/module-summary/module-summary.scss b/src/core/features/course/components/module-summary/module-summary.scss
new file mode 100644
index 000000000..59ce443d5
--- /dev/null
+++ b/src/core/features/course/components/module-summary/module-summary.scss
@@ -0,0 +1,24 @@
+@import "~theme/globals";
+
+:host ::ng-deep .collapsible-title ion-label {
+ margin-top: 12px;
+}
+
+h1 {
+ font-size: 20px;
+}
+
+.core-modulename {
+ text-transform: uppercase;
+ core-mod-icon {
+ padding: 3px;
+ --size: 10px;
+ margin: 0;
+ }
+}
+
+
+ion-item ion-label ion-icon {
+ @include margin-horizontal(0, 4px);
+ vertical-align: text-top;
+}
diff --git a/src/core/features/course/components/module-summary/module-summary.ts b/src/core/features/course/components/module-summary/module-summary.ts
new file mode 100644
index 000000000..a82c4801c
--- /dev/null
+++ b/src/core/features/course/components/module-summary/module-summary.ts
@@ -0,0 +1,385 @@
+// (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 { CoreConstants } from '@/core/constants';
+import { AddonBlog } from '@addons/blog/services/blog';
+import { AddonBlogMainMenuHandlerService } from '@addons/blog/services/handlers/mainmenu';
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { Params } from '@angular/router';
+import { CoreCourse } from '@features/course/services/course';
+import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
+import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
+import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
+import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
+import { CoreGradesFormattedRow, CoreGradesFormattedTableRow, CoreGradesHelper } from '@features/grades/services/grades-helper';
+import { CoreApp } from '@services/app';
+import { CoreFilepool } from '@services/filepool';
+import { CoreNavigator } from '@services/navigator';
+import { CoreSites } from '@services/sites';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreTextUtils } from '@services/utils/text';
+import { CoreUtils } from '@services/utils/utils';
+import { ModalController, Network, NgZone } from '@singletons';
+import { CoreEventObserver, CoreEvents } from '@singletons/events';
+import { Subscription } from 'rxjs';
+
+/**
+ * Component to display a module summary modal.
+ */
+@Component({
+ selector: 'core-course-module-summary',
+ templateUrl: 'module-summary.html',
+ styleUrls: ['module-summary.scss'],
+})
+export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
+
+ @Input() module?: CoreCourseModuleData; // The module of the component.
+ @Input() courseId = 0; // Course ID the component belongs to.
+ @Input() moduleId = 0; // Module ID the component belongs to.
+ @Input() component = ''; // Component name.
+ @Input() description = ''; // Module description.
+ @Input() hasOffline = false; // If it has offline data to be synced.
+ @Input() displayOptions: CoreCourseModuleSummaryDisplayOptions = {};
+
+ loaded = false; // If the component has been loaded.
+ componentId?: number; // Component ID.
+
+ // Data for context menu.
+ externalUrl?: string; // External URL to open in browser.
+
+ removeFilesLoading = false;
+ prefetchLoading = false;
+ canPrefetch = false;;
+ prefetchDisabled = false;
+ sizeReadable = '';
+ downloadTimeReadable = ''; // Last download time in a readable format.
+ grades?: CoreGradesFormattedRow[];
+ blog = false; // If blog is available.
+ isOnline = false; // If the app is online or not.
+ course?: CoreEnrolledCourseData;
+ modicon = '';
+ moduleNameTranslated = '';
+
+ protected onlineSubscription: Subscription; // It will observe the status of the network connection.
+ protected packageStatusObserver?: CoreEventObserver; // Observer of package status.
+ protected fileStatusObserver?: CoreEventObserver; // Observer of file status.
+ protected siteId: string;
+ protected isDestroyed = false;
+
+ constructor() {
+ this.siteId = CoreSites.getCurrentSiteId();
+ this.isOnline = CoreApp.isOnline();
+
+ // Refresh online status when changes.
+ this.onlineSubscription = Network.onChange().subscribe(() => {
+ // Execute the callback in the Angular zone, so change detection doesn't stop working.
+ NgZone.run(() => {
+ this.isOnline = CoreApp.isOnline();
+ });
+ });
+ }
+
+ /**
+ * @inheritdoc
+ */
+ async ngOnInit(): Promise {
+ if (!this.module) {
+ this.closeModal();
+
+ return;
+ }
+
+ this.displayOptions = Object.assign({
+ displayOpenInBrowser: true,
+ displayDescription: true,
+ displayRefresh: true,
+ displayPrefetch: true,
+ displaySize: true,
+ displayBlog: true,
+ displayGrades: true,
+ }, this.displayOptions);
+
+ this.displayOptions.displayGrades = this.displayOptions.displayGrades &&
+ CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_GRADE_HAS_GRADE, true);
+
+ this.displayOptions.displayDescription = this.displayOptions.displayDescription &&
+ CoreCourseModuleDelegate.supportsFeature(this.module.modname, CoreConstants.FEATURE_SHOW_DESCRIPTION, true);
+
+ this.fetchContent();
+
+ if (this.component) {
+ this.packageStatusObserver = CoreEvents.on(
+ CoreEvents.PACKAGE_STATUS_CHANGED,
+ (data) => {
+ if (data.componentId == module.id && data.component == this.component) {
+ this.getPackageStatus();
+ }
+ },
+ this.siteId,
+ );
+
+ // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity.
+ const debouncedUpdateSize = CoreUtils.debounce(async () => {
+ if (!this.module) {
+ return;
+ }
+
+ const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(this.module, this.courseId);
+
+ this.sizeReadable = moduleSize > 0 ? CoreTextUtils.bytesToSize(moduleSize, 2) : '';
+ }, 1000);
+
+ this.fileStatusObserver = CoreEvents.on(
+ CoreEvents.COMPONENT_FILE_ACTION,
+ (data) => {
+ if (data.component != this.component || data.componentId != module.id) {
+ // The event doesn't belong to this component, ignore.
+ return;
+ }
+
+ if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) {
+ return;
+ }
+
+ // Update the module size.
+ debouncedUpdateSize();
+ },
+ this.siteId,
+ );
+ }
+
+ }
+
+ /**
+ * Fetch content to populate the page.
+ */
+ protected async fetchContent(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ this.componentId = this.module.id;
+ this.externalUrl = this.module.url;
+ this.courseId = this.courseId || this.module.course;
+
+ this.modicon = await CoreCourseModuleDelegate.getModuleIconSrc(this.module.modname, this.module.modicon, this.module);
+ this.moduleNameTranslated = CoreCourse.translateModuleName(this.module.modname || '');
+
+ this.blog = await AddonBlog.isPluginEnabled();
+
+ await Promise.all([
+ this.getPackageStatus(),
+ this.fetchGrades(),
+ this.fetchCourse(),
+ ]);
+
+ this.loaded = true;
+ }
+
+ /**
+ * Updage package status.
+ *
+ * @param refresh If prefetch info has to be refreshed.
+ */
+ protected async getPackageStatus(refresh = false): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ const moduleInfo =
+ await CoreCourseHelper.getModulePrefetchInfo(this.module, this.courseId, refresh, this.component);
+
+ this.canPrefetch = moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE;
+ this.downloadTimeReadable = '';
+
+ if (this.canPrefetch) {
+ if (moduleInfo.downloadTime && moduleInfo.downloadTime > 0) {
+ this.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable);
+ }
+ this.prefetchLoading = moduleInfo.status == CoreConstants.DOWNLOADING;
+ this.prefetchDisabled = moduleInfo.status == CoreConstants.DOWNLOADED;
+ }
+
+ this.sizeReadable = moduleInfo.size && moduleInfo.size > 0
+ ? moduleInfo.sizeReadable
+ : '';
+ }
+
+ /**
+ * Go to blog posts.
+ */
+ async gotoBlog(): Promise {
+ const params: Params = { cmId: this.moduleId };
+
+ await CoreNavigator.navigateToSitePath(AddonBlogMainMenuHandlerService.PAGE_NAME, { params });
+ }
+
+ /**
+ * Fetch grade module info.
+ */
+ protected async fetchGrades(): Promise {
+ if (!this.displayOptions.displayGrades) {
+ return;
+ }
+
+ this.grades = await CoreGradesHelper.getModuleGrades(this.courseId, this.moduleId);
+ }
+
+ /**
+ * Toggle grades expand.
+ *
+ * @param grade Row to expand.
+ */
+ toggleGrade(grade: CoreGradesFormattedTableRow): void {
+ grade.expanded = !grade.expanded;
+ }
+
+ /**
+ * Fetch course.
+ */
+ protected async fetchCourse(): Promise {
+ this.course = await CoreCourses.getUserCourse(this.courseId, true);
+ }
+
+ /**
+ * Open course.
+ */
+ openCourse(): void {
+ if (!this.course) {
+ return;
+ }
+
+ CoreCourse.openCourse(
+ this.course,
+ {
+ replace: true,
+ animationDirection: 'back',
+ },
+ );
+ }
+
+ /**
+ * Prefetch the module.
+ */
+ async prefetch(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ this.prefetchLoading = true; // Show spinner since this operation might take a while.
+
+ try {
+ // We need to call getDownloadSize, the package might have been updated.
+ const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(this.module, this.courseId, true);
+
+ await CoreDomUtils.confirmDownloadSize(size);
+
+ await CoreCourseModulePrefetchDelegate.prefetchModule(this.module, this.courseId, true);
+
+ await this.getPackageStatus(true);
+ } catch (error) {
+ this.prefetchLoading = false;
+
+ if (!this.isDestroyed) {
+ CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ }
+ }
+ }
+
+ /**
+ * Confirm and remove downloaded files.
+ */
+ async removeFiles(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ if (this.prefetchLoading) {
+ CoreDomUtils.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading');
+
+ return;
+ }
+
+ try {
+ await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: this.module.name });
+
+ this.removeFilesLoading = true;
+
+ await CoreCourseHelper.removeModuleStoredData(this.module, this.courseId);
+
+ } catch (error) {
+ if (!this.isDestroyed &&error) {
+ CoreDomUtils.showErrorModal(error);
+ }
+ } finally {
+ this.removeFilesLoading = false;
+ }
+
+ await this.getPackageStatus();
+ }
+
+ /**
+ * Refresh the data.
+ */
+ async refresh(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ ModalController.dismiss({ action: 'refresh' });
+ }
+
+ /**
+ * Sync the data.
+ */
+ async sync(): Promise {
+ if (!this.module) {
+ return;
+ }
+
+ ModalController.dismiss({ action: 'sync' });
+ }
+
+ /**
+ * Close the modal.
+ */
+ closeModal(): void {
+ ModalController.dismiss();
+ }
+
+ /**
+ * @inheritdoc
+ */
+ ngOnDestroy(): void {
+ this.isDestroyed = true;
+ this.packageStatusObserver?.off();
+ this.fileStatusObserver?.off();
+ this.onlineSubscription.unsubscribe();
+ }
+
+}
+
+export type CoreCourseModuleSummaryResult = {
+ action: 'sync'|'refresh';
+};
+
+export type CoreCourseModuleSummaryDisplayOptions = {
+ displayOpenInBrowser?: boolean;
+ displayDescription?: boolean;
+ displayRefresh?: boolean;
+ displayPrefetch?: boolean;
+ displaySize?: boolean;
+ displayBlog?: boolean;
+ displayGrades?: boolean;
+};
diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts
index 77d752743..85d7bfce1 100644
--- a/src/core/features/course/pages/contents/contents.ts
+++ b/src/core/features/course/pages/contents/contents.ts
@@ -31,7 +31,7 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreCourseOptionsMenuHandlerToDisplay } from '@features/course/services/course-options-delegate';
import { CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync';
-import { CoreCourseFormatComponent } from '../../components/format/format';
+import { CoreCourseFormatComponent } from '../../components/course-format/course-format';
import {
CoreEvents,
CoreEventObserver,
diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html
index 3fceb9d64..eb42c4b54 100644
--- a/src/core/features/course/pages/module-preview/module-preview.html
+++ b/src/core/features/course/pages/module-preview/module-preview.html
@@ -11,11 +11,10 @@
-
-
-
-
+
+
+
diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts
index 93c8b0ca6..e14cae7f0 100644
--- a/src/core/features/course/services/course-helper.ts
+++ b/src/core/features/course/services/course-helper.ts
@@ -64,7 +64,6 @@ import { CoreFile } from '@services/file';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
-import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
@@ -75,36 +74,19 @@ import { CoreStatusWithWarningsWSResponse } from '@services/ws';
/**
* Prefetch info of a module.
*/
-export type CoreCourseModulePrefetchInfo = {
- /**
- * Downloaded size.
- */
- size: number;
+export type CoreCourseModulePrefetchInfo = CoreCourseModulePackageLastDownloaded & {
+ size: number; // Downloaded size.
+ sizeReadable: string; // Downloadable size in a readable format.
+ status: string; // Module status.
+ statusIcon?: string; // Icon's name of the module status.
+};
- /**
- * Downloadable size in a readable format.
- */
- sizeReadable: string;
-
- /**
- * Module status.
- */
- status: string;
-
- /**
- * Icon's name of the module status.
- */
- statusIcon?: string;
-
- /**
- * Time when the module was last downloaded.
- */
- downloadTime: number;
-
- /**
- * Download time in a readable format.
- */
- downloadTimeReadable: string;
+/**
+ * Prefetch info of a module.
+ */
+export type CoreCourseModulePackageLastDownloaded = {
+ downloadTime: number; // Time when the module was last downloaded.
+ downloadTimeReadable: string; // Download time in a readable format.
};
/**
@@ -491,22 +473,18 @@ export class CoreCourseHelperProvider {
*
* @param module Module to remove the files.
* @param courseId Course ID the module belongs to.
- * @param done Function to call when done. It will close the context menu.
* @return Promise resolved when done.
+ * @deprecated since 4.0
*/
- async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number, done?: () => void): Promise {
+ async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number): Promise {
let modal: CoreIonLoadingElement | undefined;
try {
-
await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: module.name });
modal = await CoreDomUtils.showModalLoading();
await this.removeModuleStoredData(module, courseId);
-
- done && done();
-
} catch (error) {
if (error) {
CoreDomUtils.showErrorModal(error);
@@ -571,44 +549,6 @@ export class CoreCourseHelperProvider {
await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm);
}
- /**
- * Helper function to prefetch a module, showing a confirmation modal if the size is big.
- * This function is meant to be called from a context menu option. It will also modify some data like the prefetch icon.
- *
- * @param instance The component instance that has the context menu.
- * @param module Module to be prefetched
- * @param courseId Course ID the module belongs to.
- * @param done Function to call when done. It will close the context menu.
- * @return Promise resolved when done.
- */
- async contextMenuPrefetch(
- instance: ComponentWithContextMenu,
- module: CoreCourseModuleData,
- courseId: number,
- done?: () => void,
- ): Promise {
- const initialIcon = instance.prefetchStatusIcon;
- instance.prefetchStatusIcon = CoreConstants.ICON_DOWNLOADING; // Show spinner since this operation might take a while.
-
- try {
- // We need to call getDownloadSize, the package might have been updated.
- const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(module, courseId, true);
-
- await CoreDomUtils.confirmDownloadSize(size);
-
- await CoreCourseModulePrefetchDelegate.prefetchModule(module, courseId, true);
-
- // Success, close menu.
- done && done();
- } catch (error) {
- instance.prefetchStatusIcon = initialIcon;
-
- if (!instance.isDestroyed) {
- CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
- }
- }
- }
-
/**
* Check whether a course is accessed using guest access.
*
@@ -1045,87 +985,6 @@ export class CoreCourseHelperProvider {
await CoreFilepool.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId);
}
- /**
- * Fill the Context Menu for a certain module.
- *
- * @param instance The component instance that has the context menu.
- * @param module Module to be prefetched
- * @param courseId Course ID the module belongs to.
- * @param invalidateCache Invalidates the cache first.
- * @param component Component of the module.
- * @return Promise resolved when done.
- */
- async fillContextMenu(
- instance: ComponentWithContextMenu,
- module: CoreCourseModuleData,
- courseId: number,
- invalidateCache?: boolean,
- component?: string,
- ): Promise {
- const siteId = CoreSites.getCurrentSiteId();
-
- const moduleInfo = await this.getModulePrefetchInfo(module, courseId, invalidateCache, component);
-
- instance.size = moduleInfo.sizeReadable;
- instance.prefetchStatusIcon = moduleInfo.statusIcon;
- instance.prefetchStatus = moduleInfo.status;
- instance.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable);
-
- if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) {
- // Module is downloadable, get the text to display to prefetch.
- if (moduleInfo.downloadTime && moduleInfo.downloadTime > 0) {
- instance.prefetchText = Translate.instant('core.lastdownloaded') + ': ' + moduleInfo.downloadTimeReadable;
- } else {
- // Module not downloaded, show a default text.
- instance.prefetchText = Translate.instant('core.download');
- }
- }
-
- if (moduleInfo.status == CoreConstants.DOWNLOADING) {
- // Set this to empty to prevent "remove file" option showing up while downloading.
- instance.size = '';
- }
-
- if (!instance.contextMenuStatusObserver && component) {
- instance.contextMenuStatusObserver = CoreEvents.on(
- CoreEvents.PACKAGE_STATUS_CHANGED,
- (data) => {
- if (data.componentId == module.id && data.component == component) {
- this.fillContextMenu(instance, module, courseId, false, component);
- }
- },
- siteId,
- );
- }
-
- if (!instance.contextFileStatusObserver && component) {
- // Debounce the update size function to prevent too many calls when downloading or deleting a whole activity.
- const debouncedUpdateSize = CoreUtils.debounce(async () => {
- const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);
-
- instance.size = moduleSize > 0 ? CoreTextUtils.bytesToSize(moduleSize, 2) : '';
- }, 1000);
-
- instance.contextFileStatusObserver = CoreEvents.on(
- CoreEvents.COMPONENT_FILE_ACTION,
- (data) => {
- if (data.component != component || data.componentId != module.id) {
- // The event doesn't belong to this component, ignore.
- return;
- }
-
- if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) {
- return;
- }
-
- // Update the module size.
- debouncedUpdateSize();
- },
- siteId,
- );
- }
- }
-
/**
* Get a course. It will first check the user courses, and fallback to another WS if not enrolled.
*
@@ -1482,12 +1341,9 @@ export class CoreCourseHelperProvider {
async getModulePrefetchInfo(
module: CoreCourseModuleData,
courseId: number,
- invalidateCache?: boolean,
- component?: string,
+ invalidateCache = false,
+ component = '',
): Promise {
-
- const siteId = CoreSites.getCurrentSiteId();
-
if (invalidateCache) {
// Currently, some modules pass invalidateCache=false because they already invalidate data in downloadResourceIfNeeded.
// If this function is changed to do more actions if invalidateCache=true, please review those modules.
@@ -1499,7 +1355,7 @@ export class CoreCourseHelperProvider {
const results = await Promise.all([
CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId),
CoreCourseModulePrefetchDelegate.getModuleStatus(module, courseId),
- CoreUtils.ignoreErrors(CoreFilepool.getPackageData(siteId, component || '', module.id)),
+ this.getModulePackageLastDownloaded(module, component),
]);
// Treat stored size.
@@ -1526,33 +1382,51 @@ export class CoreCourseHelperProvider {
break;
}
- // Treat download time.
- if (!results[2] || !results[2].downloadTime || !CoreFileHelper.isStateDownloaded(results[2].status || '')) {
- // Not downloaded.
- return {
- size,
- sizeReadable,
- status,
- statusIcon,
- downloadTime: 0,
- downloadTimeReadable: '',
- };
- }
-
- const now = CoreTimeUtils.timestamp();
- const downloadTime = results[2].downloadTime;
- let downloadTimeReadable = '';
- if (now - results[2].downloadTime < 7 * 86400) {
- downloadTimeReadable = moment(results[2].downloadTime * 1000).fromNow();
- } else {
- downloadTimeReadable = moment(results[2].downloadTime * 1000).calendar();
- }
+ const packageData = results[2];
return {
size,
sizeReadable,
status,
statusIcon,
+ downloadTime: packageData.downloadTime,
+ downloadTimeReadable: packageData.downloadTimeReadable,
+ };
+ }
+
+ /**
+ * Get prefetch info for a module.
+ *
+ * @param module Module to get the info from.
+ * @param component Component of the module.
+ * @return Promise resolved with the info.
+ */
+ async getModulePackageLastDownloaded(
+ module: CoreCourseModuleData,
+ component = '',
+ ): Promise {
+ const siteId = CoreSites.getCurrentSiteId();
+ const packageData = await CoreUtils.ignoreErrors(CoreFilepool.getPackageData(siteId, component, module.id));
+
+ // Treat download time.
+ if (!packageData || !packageData.downloadTime || !CoreFileHelper.isStateDownloaded(packageData.status || '')) {
+ // Not downloaded.
+ return {
+ downloadTime: 0,
+ downloadTimeReadable: '',
+ };
+ }
+
+ const now = CoreTimeUtils.timestamp();
+ const downloadTime = packageData.downloadTime;
+ let downloadTimeReadable = '';
+ if (now - downloadTime < 7 * 86400) {
+ downloadTimeReadable = moment(downloadTime * 1000).fromNow();
+ } else {
+ downloadTimeReadable = moment(downloadTime * 1000).calendar();
+ }
+
+ return {
downloadTime,
downloadTimeReadable,
};
@@ -2223,14 +2097,3 @@ export type CoreCourseOpenModuleOptions = {
sectionId?: number; // Section the module belongs to.
modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module.
};
-
-type ComponentWithContextMenu = {
- prefetchStatusIcon?: string;
- isDestroyed?: boolean;
- size?: string;
- prefetchStatus?: string;
- prefetchText?: string;
- downloadTimeReadable?: string;
- contextMenuStatusObserver?: CoreEventObserver;
- contextFileStatusObserver?: CoreEventObserver;
-};
diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts
index 38c68d3df..5a4833f15 100644
--- a/src/core/features/course/services/handlers/default-format.ts
+++ b/src/core/features/course/services/handlers/default-format.ts
@@ -130,9 +130,13 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler {
navOptions.params = navOptions.params || {};
Object.assign(navOptions.params, { course: course });
- // Don't return the .push promise, we don't want to display a loading modal during the page transition.
- const currentTab = CoreNavigator.getCurrentMainMenuTab();
- const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
+ // When replace is true, disable route depth.
+ let routeDepth = 0;
+ if (!navOptions.replace) {
+ // Don't return the .push promise, we don't want to display a loading modal during the page transition.
+ const currentTab = CoreNavigator.getCurrentMainMenuTab();
+ routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`);
+ }
const deepPath = '/deep'.repeat(routeDepth);
CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions);
diff --git a/src/core/features/course/services/module-delegate.ts b/src/core/features/course/services/module-delegate.ts
index d32c1be3e..6c9a73055 100644
--- a/src/core/features/course/services/module-delegate.ts
+++ b/src/core/features/course/services/module-delegate.ts
@@ -202,10 +202,10 @@ export interface CoreCourseModuleMainComponent {
* Refresh the data.
*
* @param refresher Refresher.
- * @param done Function to call when done.
+ * @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
- doRefresh(refresher?: IonRefresher, done?: () => void): Promise;
+ doRefresh(refresher?: IonRefresher | null, showErrors?: boolean): Promise;
}
/**
diff --git a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html
index 51cb1e650..c63576239 100644
--- a/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html
+++ b/src/core/features/courses/components/course-list-item/core-courses-course-list-item.html
@@ -4,7 +4,7 @@
![]()
-
+
@@ -31,7 +31,8 @@
[class.item-disabled]="course.visible == 0">
-
+
diff --git a/src/core/features/grades/grades.module.ts b/src/core/features/grades/grades.module.ts
index 6669a9161..8935c6ee8 100644
--- a/src/core/features/grades/grades.module.ts
+++ b/src/core/features/grades/grades.module.ts
@@ -22,7 +22,7 @@ import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-ro
import { CoreUserDelegate } from '@features/user/services/user-delegate';
import { PARTICIPANTS_PAGE_NAME } from '@features/user/user.module';
import { CoreGradesProvider } from './services/grades';
-import { CoreGradesHelperProvider } from './services/grades-helper';
+import { CoreGradesHelperProvider, GRADES_PAGE_NAME } from './services/grades-helper';
import { CoreGradesCourseOptionHandler } from './services/handlers/course-option';
import { CoreGradesOverviewLinkHandler } from './services/handlers/overview-link';
import { CoreGradesUserHandler } from './services/handlers/user';
@@ -33,8 +33,6 @@ export const CORE_GRADES_SERVICES: Type[] = [
CoreGradesHelperProvider,
];
-export const GRADES_PAGE_NAME = 'grades';
-
const mainMenuChildrenRoutes: Routes = [
{
path: GRADES_PAGE_NAME,
diff --git a/src/core/features/grades/lang.json b/src/core/features/grades/lang.json
index 504a4ae34..456ff43e8 100644
--- a/src/core/features/grades/lang.json
+++ b/src/core/features/grades/lang.json
@@ -9,6 +9,7 @@
"fail": "Fail",
"feedback": "Feedback",
"grade": "Grade",
+ "gradebook": "Gradebook",
"gradeitem": "Grade item",
"gradepass": "Grade to pass",
"grades": "Grades",
diff --git a/src/core/features/grades/pages/course/course.html b/src/core/features/grades/pages/course/course.html
index c484de9f0..6cbbae4e4 100644
--- a/src/core/features/grades/pages/course/course.html
+++ b/src/core/features/grades/pages/course/course.html
@@ -31,7 +31,7 @@
[attr.tabindex]="row.expandable && showSummary && 0" [attr.aria-expanded]="row.expanded"
[attr.aria-label]="rowAriaLabel(row)" [attr.aria-controls]="row.detailsid"
(ariaButtonClick)="row.expandable && showSummary && toggleRow(row)" [class]="row.rowclass"
- [class.core-grades-grade-clickable]="row.expandable && showSummary">
+ [class.core-grades-grade-clickable]="row.expandable && showSummary" [id]="'grade-'+row.id">
|
@@ -71,40 +71,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ 'core.grades.weight' | translate}}
diff --git a/src/core/features/grades/pages/course/course.page.ts b/src/core/features/grades/pages/course/course.page.ts
index fe22fc527..22fadfa71 100644
--- a/src/core/features/grades/pages/course/course.page.ts
+++ b/src/core/features/grades/pages/course/course.page.ts
@@ -13,8 +13,8 @@
// limitations under the License.
import { ActivatedRoute } from '@angular/router';
-import { AfterViewInit, Component, ElementRef, OnDestroy } from '@angular/core';
-import { IonRefresher } from '@ionic/angular';
+import { AfterViewInit, Component, ElementRef, OnDestroy, Optional } from '@angular/core';
+import { IonContent, IonRefresher } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreGrades } from '@features/grades/services/grades';
@@ -44,6 +44,7 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
courseId!: number;
userId!: number;
+ gradeId?: number;
expandLabel!: string;
collapseLabel!: string;
title?: string;
@@ -53,10 +54,16 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
totalColumnsSpan?: number;
withinSplitView?: boolean;
- constructor(protected route: ActivatedRoute, protected element: ElementRef) {
+ constructor(
+ protected route: ActivatedRoute,
+ protected element: ElementRef,
+ @Optional() protected content?: IonContent,
+ ) {
try {
this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId', { route });
this.userId = CoreNavigator.getRouteNumberParam('userId', { route }) ?? CoreSites.getCurrentSiteUserId();
+ this.gradeId = CoreNavigator.getRouteNumberParam('gradeId', { route });
+
this.expandLabel = Translate.instant('core.expand');
this.collapseLabel = Translate.instant('core.collapse');
@@ -116,13 +123,14 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
* Toggle whether a row is expanded or collapsed.
*
* @param row Row.
+ * @param expand If defined, force expand or collapse.
*/
- toggleRow(row: CoreGradesFormattedTableRow): void {
+ toggleRow(row: CoreGradesFormattedTableRow, expand?: boolean): void {
if (!this.rows || !this.columns) {
return;
}
- row.expanded = !row.expanded;
+ row.expanded = expand ?? !row.expanded;
let colspan: number = this.columns.length + (row.colspan ?? 0) - 1;
for (let i = this.rows.indexOf(row) - 1; i >= 0; i--) {
@@ -155,6 +163,22 @@ export class CoreGradesCoursePage implements AfterViewInit, OnDestroy {
private async fetchInitialGrades(): Promise {
try {
await this.fetchGrades();
+
+ if (this.gradeId && this.rows) {
+ const row = this.rows.find((row) => row.id == this.gradeId);
+
+ if (row) {
+ this.toggleRow(row, true);
+ await CoreUtils.nextTick();
+
+ CoreDomUtils.scrollToElementBySelector(
+ this.element.nativeElement,
+ this.content,
+ '#grade-' + row.id,
+ );
+ this.gradeId = undefined;
+ }
+ }
} catch (error) {
CoreDomUtils.showErrorModalDefault(error, 'Error loading course');
diff --git a/src/core/features/grades/pages/course/course.scss b/src/core/features/grades/pages/course/course.scss
index 32bb2b00a..1d73d15e2 100644
--- a/src/core/features/grades/pages/course/course.scss
+++ b/src/core/features/grades/pages/course/course.scss
@@ -80,8 +80,8 @@
}
core-mod-icon {
- padding: 0.1rem;
- --size: 16px;
+ padding: 3px;
+ --size: 10px;
}
diff --git a/src/core/features/grades/services/grades-helper.ts b/src/core/features/grades/services/grades-helper.ts
index c343c2c71..cff4e9b98 100644
--- a/src/core/features/grades/services/grades-helper.ts
+++ b/src/core/features/grades/services/grades-helper.ts
@@ -35,7 +35,8 @@ import { CoreNavigator } from '@services/navigator';
import { makeSingleton, Translate } from '@singletons';
import { CoreError } from '@classes/errors/error';
import { CoreCourseHelper } from '@features/course/services/course-helper';
-import { GRADES_PAGE_NAME } from '../grades.module';
+
+export const GRADES_PAGE_NAME = 'grades';
/**
* Service that provides some features regarding grades information.
@@ -456,6 +457,38 @@ export class CoreGradesHelperProvider {
}).map((row) => this.formatGradeRow(row)));
}
+ /**
+ * Get module grades to display.
+ *
+ * @param courseId Course Id.
+ * @param moduleId Module Id.
+ * @return Formatted table rows.
+ */
+ async getModuleGrades(courseId: number, moduleId: number): Promise {
+ const table = await CoreGrades.getCourseGradesTable(courseId);
+
+ if (!table.tabledata) {
+ return [];
+ }
+
+ // Find href containing "/mod/xxx/xxx.php".
+ const regex = /href="([^"]*\/mod\/[^"|^/]*\/[^"|^.]*\.php[^"]*)/;
+
+ return await Promise.all(table.tabledata.filter((row) => {
+ if (row.itemname && row.itemname.content) {
+ const matches = row.itemname.content.match(regex);
+
+ if (matches && matches.length) {
+ const hrefParams = CoreUrlUtils.extractUrlParams(matches[1]);
+
+ return hrefParams && parseInt(hrefParams.id) === moduleId;
+ }
+ }
+
+ return false;
+ }).map((row) => this.formatGradeRowForTable(row)));
+ }
+
/**
* Go to view grades.
*
@@ -497,9 +530,12 @@ export class CoreGradesHelperProvider {
const gradeId = item.id;
await CoreUtils.ignoreErrors(
- CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}/${gradeId}`, { siteId }),
+ CoreNavigator.navigateToSitePath(
+ `/${GRADES_PAGE_NAME}/${courseId}`,
+ { params: { gradeId }, siteId },
+ ),
);
- } catch (error) {
+ } catch {
try {
// Cannot get grade items or there's no need to.
if (userId && userId != currentUserId) {
@@ -519,7 +555,7 @@ export class CoreGradesHelperProvider {
// Open the course with the grades tab selected.
await CoreCourseHelper.getAndOpenCourse(courseId, { selectedTab: 'CoreGrades' }, siteId);
- } catch (error) {
+ } catch {
// Cannot get course for some reason, just open the grades page.
await CoreNavigator.navigateToSitePath(`/${GRADES_PAGE_NAME}/${courseId}`, { siteId });
}
@@ -565,7 +601,7 @@ export class CoreGradesHelperProvider {
row.iconAlt = Translate.instant('core.grades.aggregatesum');
} else if (text.indexOf('/outcomes') > -1 || text.indexOf('fa-tasks') > -1) {
row.itemtype = 'outcome';
- row.icon = 'fas-chart-pie';
+ row.icon = 'fas-tasks';
row.iconAlt = Translate.instant('core.grades.outcome');
} else if (text.indexOf('i/folder') > -1 || text.indexOf('fa-folder') > -1) {
row.itemtype = 'category';
diff --git a/src/core/features/grades/services/grades.ts b/src/core/features/grades/services/grades.ts
index 80b588e8a..c7aa1aa7c 100644
--- a/src/core/features/grades/services/grades.ts
+++ b/src/core/features/grades/services/grades.ts
@@ -199,7 +199,7 @@ export class CoreGradesProvider {
const table = await site.read('gradereport_user_get_grades_table', params, preSets);
if (!table?.tables?.[0]) {
- throw new CoreError('Coudln\'t get course grades table');
+ throw new CoreError('Couldn\'t get course grades table');
}
return table.tables[0];
diff --git a/src/core/features/grades/services/handlers/overview-link.ts b/src/core/features/grades/services/handlers/overview-link.ts
index 241e2fc19..2205e29ea 100644
--- a/src/core/features/grades/services/handlers/overview-link.ts
+++ b/src/core/features/grades/services/handlers/overview-link.ts
@@ -15,10 +15,10 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
-import { GRADES_PAGE_NAME } from '@features/grades/grades.module';
import { CoreNavigator } from '@services/navigator';
import { makeSingleton } from '@singletons';
import { CoreGrades } from '../grades';
+import { GRADES_PAGE_NAME } from '../grades-helper';
/**
* Handler to treat links to overview courses grades.
diff --git a/src/core/features/grades/services/handlers/user.ts b/src/core/features/grades/services/handlers/user.ts
index 945972f4e..ade60adf5 100644
--- a/src/core/features/grades/services/handlers/user.ts
+++ b/src/core/features/grades/services/handlers/user.ts
@@ -14,7 +14,6 @@
import { Injectable } from '@angular/core';
import { COURSE_PAGE_NAME } from '@features/course/course.module';
-import { GRADES_PAGE_NAME } from '@features/grades/grades.module';
import { CoreGrades } from '@features/grades/services/grades';
import { CoreUserProfile } from '@features/user/services/user';
@@ -29,6 +28,7 @@ import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
+import { GRADES_PAGE_NAME } from '../grades-helper';
/**
* Profile grades handler.
diff --git a/src/core/features/login/pages/credentials/credentials.html b/src/core/features/login/pages/credentials/credentials.html
index ce0c56988..b9a85c9d3 100644
--- a/src/core/features/login/pages/credentials/credentials.html
+++ b/src/core/features/login/pages/credentials/credentials.html
@@ -73,11 +73,11 @@
{{ 'core.login.potentialidps' | translate }}
-
+
{{provider.name}}
-
+
diff --git a/src/core/features/login/pages/reconnect/reconnect.html b/src/core/features/login/pages/reconnect/reconnect.html
index 6d7ae2d7a..3a37bd1f0 100644
--- a/src/core/features/login/pages/reconnect/reconnect.html
+++ b/src/core/features/login/pages/reconnect/reconnect.html
@@ -87,11 +87,11 @@
{{ 'core.login.potentialidps' | translate }}
-
-
+
+
{{provider.name}}
-
+
diff --git a/src/core/features/siteplugins/components/course-format/course-format.ts b/src/core/features/siteplugins/components/course-format/course-format.ts
index d2bc5c401..45eaf0266 100644
--- a/src/core/features/siteplugins/components/course-format/course-format.ts
+++ b/src/core/features/siteplugins/components/course-format/course-format.ts
@@ -15,7 +15,7 @@
import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
-import { CoreCourseFormatComponent } from '@features/course/components/format/format';
+import { CoreCourseFormatComponent } from '@features/course/components/course-format/course-format';
import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
diff --git a/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html
index 89dfe24d8..d4f915a10 100644
--- a/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html
+++ b/src/core/features/siteplugins/components/module-index/core-siteplugins-module-index.html
@@ -1,28 +1,8 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
;
initResult?: CoreSitePluginsContent | null;
preSets?: CoreSiteWSPreSets;
-
- // Data for context menu.
- externalUrl?: string;
description?: string;
- refreshIcon?: string;
+
+ /**
+ * @deprecated since 4.0, use module.url instead.
+ */
+ externalUrl?: string;
+ /**
+ * @deprecated since 4.0. It won't be populated anymore.
+ */
+ refreshIcon = CoreConstants.ICON_REFRESH;
+ /**
+ * @deprecated since 4.0.. It won't be populated anymore.
+ */
prefetchStatus?: string;
+ /**
+ * @deprecated since 4.0. It won't be populated anymore.
+ */
prefetchStatusIcon?: string;
+ /**
+ * @deprecated since 4.0. It won't be populated anymore.
+ */
prefetchText?: string;
+ /**
+ * @deprecated since 4.0. It won't be populated anymore.
+ */
size?: string;
- contextMenuStatusObserver?: CoreEventObserver;
- contextFileStatusObserver?: CoreEventObserver;
+
displayOpenInBrowser = true;
displayDescription = true;
displayRefresh = true;
displayPrefetch = true;
displaySize = true;
+ displayGrades = false;
+ // @TODO: // Currently display blogs is not an option since it may change soon adding new summary handlers.
+ displayBlog = false;
+
ptrEnabled = true;
isDestroyed = false;
@@ -80,8 +103,6 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
* Component being initialized.
*/
ngOnInit(): void {
- this.refreshIcon = CoreConstants.ICON_LOADING;
-
if (!this.module) {
return;
}
@@ -110,6 +131,7 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
this.displayRefresh = !CoreUtils.isFalseOrZero(handlerSchema.displayrefresh);
this.displayPrefetch = !CoreUtils.isFalseOrZero(handlerSchema.displayprefetch);
this.displaySize = !CoreUtils.isFalseOrZero(handlerSchema.displaysize);
+ this.displayGrades = CoreUtils.isTrueOrOne(handlerSchema.displaygrades); // False by default.
this.ptrEnabled = !CoreUtils.isFalseOrZero(handlerSchema.ptrenabled);
}
@@ -122,71 +144,114 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
* Refresh the data.
*
* @param refresher Refresher.
- * @param done Function to call when done.
* @return Promise resolved when done.
*/
- async doRefresh(refresher?: IonRefresher | null, done?: () => void): Promise {
- if (this.content) {
- this.refreshIcon = CoreConstants.ICON_LOADING;
- }
-
+ async doRefresh(refresher?: IonRefresher | null): Promise {
try {
await this.content?.refreshContent(false);
} finally {
refresher?.complete();
- done && done();
}
}
/**
* Function called when the data of the site plugin content is loaded.
*/
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
contentLoaded(refresh: boolean): void {
- this.refreshIcon = CoreConstants.ICON_REFRESH;
-
- // Check if there is a prefetch handler for this type of module.
- if (CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(this.module.modname)) {
- CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component);
- }
+ return;
}
/**
* Function called when starting to load the data of the site plugin content.
*/
contentLoading(): void {
- this.refreshIcon = CoreConstants.ICON_LOADING;
+ return;
}
/**
* Expand the description.
+ *
+ * @deprecated since 4.0
*/
expandDescription(): void {
- if (!this.description) {
+ this.openModuleSummary();
+ }
+
+ /**
+ * Opens a module summary page.
+ */
+ async openModuleSummary(): Promise {
+ if (!this.module) {
return;
}
- CoreTextUtils.viewText(Translate.instant('core.description'), this.description, {
- component: this.component,
- componentId: this.module.id,
- filter: true,
- contextLevel: 'module',
- instanceId: this.module.id,
- courseId: this.courseId,
+ const data = await CoreDomUtils.openSideModal({
+ component: CoreCourseModuleSummaryComponent,
+ componentProps: {
+ moduleId: this.module.id,
+ module: this.module,
+ description: this.description,
+ component: this.component,
+ courseId: this.courseId,
+ displayOptions: {
+ displayOpenInBrowser: this.displayOpenInBrowser,
+ displayDescription: this.displayDescription,
+ displayRefresh: this.displayRefresh,
+ displayPrefetch: this.displayPrefetch,
+ displaySize: this.displaySize,
+ displayBlog: this.displayBlog,
+ displayGrades: this.displayGrades,
+ },
+ },
});
+
+ if (data && data.action == 'refresh' && this.content?.dataLoaded) {
+ this.content?.refreshContent(true);
+ }
}
/**
* Prefetch the module.
+ *
+ * @deprecated since 4.0
*/
- prefetch(): void {
- CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId);
+ async prefetch(): Promise {
+ try {
+ // We need to call getDownloadSize, the package might have been updated.
+ const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(this.module, this.courseId, true);
+
+ await CoreDomUtils.confirmDownloadSize(size);
+
+ await CoreCourseModulePrefetchDelegate.prefetchModule(this.module, this.courseId, true);
+ } catch (error) {
+ if (!this.isDestroyed) {
+ CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
+ }
+ }
}
/**
* Confirm and remove downloaded files.
+ *
+ * @deprecated since 4.0
*/
- removeFiles(): void {
- CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId);
+ async removeFiles(): Promise {
+ let modal: CoreIonLoadingElement | undefined;
+
+ try {
+ await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: this.module.name });
+
+ modal = await CoreDomUtils.showModalLoading();
+
+ await CoreCourseHelper.removeModuleStoredData(this.module, this.courseId);
+ } catch (error) {
+ if (error) {
+ CoreDomUtils.showErrorModal(error);
+ }
+ } finally {
+ modal?.dismiss();
+ }
}
/**
diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts
index eded0cd07..29ba79e0a 100644
--- a/src/core/features/siteplugins/services/siteplugins.ts
+++ b/src/core/features/siteplugins/services/siteplugins.ts
@@ -871,6 +871,7 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo
displayrefresh?: boolean;
displayprefetch?: boolean;
displaysize?: boolean;
+ displaygrades?: boolean;
coursepagemethod?: string;
ptrenabled?: boolean;
supportedfeatures?: Record;
diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss
index f11236c58..c0d3691c8 100644
--- a/src/theme/theme.base.scss
+++ b/src/theme/theme.base.scss
@@ -297,6 +297,21 @@ button,
ion-button {
margin: 4px 8px;
+
+ ion-spinner[slot=start],
+ img[slot=start] {
+ @include margin-horizontal(-0.3em, 0.3em);
+ }
+
+ ion-spinner[slot=end],
+ img[slot=end] {
+ @include margin-horizontal(-0.3em, 0.3em);
+ }
+
+ ion-spinner[slot] {
+ width: 20px;
+ color: inherit;
+ }
}
ion-button.button-outline {
@@ -460,6 +475,14 @@ ion-alert {
}
}
+ion-loading {
+ --border-radius: var(--huge-radius);
+
+ .loading-wrapper {
+ border-radius: var(--border-radius) !important;
+ }
+}
+
// Ionic list.
ion-list {
padding: 0 !important;
@@ -1465,6 +1488,16 @@ ion-grid.core-no-grid > ion-row {
}
}
+ion-header.no-title {
+ --core-header-toolbar-border-width: 0;
+ --core-header-toolbar-background: transparent;
+
+ ion-toolbar .button.button-clear,
+ ion-toolbar .button.button-solid {
+ --background: var(--ion-background-color);
+ }
+}
+
ion-header[collapsible] {
@include core-transition(all, 500ms);
diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss
index 6f2e2b837..edcdad2f9 100644
--- a/src/theme/theme.light.scss
+++ b/src/theme/theme.light.scss
@@ -233,6 +233,9 @@
}
--core-loading-spinner: var(--brand);
+ ion-loading {
+ --spinner-color: var(--core-loading-spinner);
+ }
ion-spinner, ion-refresher {
--ion-color-base: var(--core-loading-spinner);
--ion-color-primary: var(--core-loading-spinner);
diff --git a/upgrade.txt b/upgrade.txt
index 4755f12ea..ebb311bcb 100644
--- a/upgrade.txt
+++ b/upgrade.txt
@@ -20,6 +20,10 @@ information provided here is intended especially for developers.
- Most of the functions or callbacks that handle redirects/deeplinks have been modified to accept an object instead of just path + options. E.g.: CoreLoginHelper.isSiteLoggedOut, CoreLoginHelper.openBrowserForSSOLogin, CoreLoginHelper.openBrowserForOAuthLogin, CoreLoginHelper.prepareForSSOLogin, CoreApp.storeRedirect, CoreSites.loadSite.
- Course preview page route has changed from course/:courseId/preview to course/:courseId/summary to match with the page name and characteristics.
- The parameters of the following functions in CoreCourseHelper have changed: navigateToModuleByInstance, navigateToModule, openModule.
+- fillContextMenu, expandDescription, gotoBlog, prefetch and removeFiles functions have been removed from CoreCourseModuleMainResourceComponent.
+- contextMenuPrefetch and fillContextMenu have been removed from CoreCourseHelper.
+
+
=== 3.9.5 ===
|