+
-
+
diff --git a/src/core/components/loading/loading.ts b/src/core/components/loading/loading.ts
index 7e2557888..df7b9ef2c 100644
--- a/src/core/components/loading/loading.ts
+++ b/src/core/components/loading/loading.ts
@@ -17,6 +17,7 @@ import { Component, Input, OnInit, OnChanges, SimpleChange, ViewChild, ElementRe
import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons/core.singletons';
+import { coreShowHideAnimation } from '@classes/animations';
/**
* Component to show a loading spinner and message while data is being loaded.
@@ -42,7 +43,7 @@ import { Translate } from '@singletons/core.singletons';
selector: 'core-loading',
templateUrl: 'core-loading.html',
styleUrls: ['loading.scss'],
- // @todo animations: [coreShowHideAnimation],
+ animations: [coreShowHideAnimation],
})
export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
diff --git a/src/core/components/navbar-buttons/navbar-buttons.scss b/src/core/components/navbar-buttons/navbar-buttons.scss
new file mode 100644
index 000000000..5d9e00932
--- /dev/null
+++ b/src/core/components/navbar-buttons/navbar-buttons.scss
@@ -0,0 +1,3 @@
+:host {
+ display: none !important;
+}
diff --git a/src/core/components/navbar-buttons/navbar-buttons.ts b/src/core/components/navbar-buttons/navbar-buttons.ts
new file mode 100644
index 000000000..7697224a4
--- /dev/null
+++ b/src/core/components/navbar-buttons/navbar-buttons.ts
@@ -0,0 +1,269 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
+import { CoreLogger } from '@singletons/logger';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreContextMenuComponent } from '../context-menu/context-menu';
+
+const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
+
+/**
+ * Component to add buttons to the app's header without having to place them inside the header itself. This is meant for
+ * pages that are loaded inside a sub ion-nav, so they don't have a header.
+ *
+ * If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
+ * position. If no start/end is specified, then the buttons will be added to the first
found in the header.
+ *
+ * If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header.
+ *
+ * You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
+ *
+ * IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
+ *
+ * Example usage:
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+@Component({
+ selector: 'core-navbar-buttons',
+ template: '',
+ styleUrls: ['navbar-buttons.scss'],
+})
+export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
+
+ // If the hidden input is true, hide all buttons.
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('hidden') set hidden(value: boolean) {
+ if (typeof value == 'string' && value == '') {
+ value = true;
+ }
+ this.allButtonsHidden = value;
+ this.showHideAllElements();
+ }
+
+ protected element: HTMLElement;
+ protected allButtonsHidden = false;
+ protected forceHidden = false;
+ protected logger: CoreLogger;
+ protected movedChildren?: Node[];
+ protected instanceId: string;
+ protected mergedContextMenu?: CoreContextMenuComponent;
+
+ constructor(element: ElementRef) {
+ this.element = element.nativeElement;
+ this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
+ this.instanceId = CoreDomUtils.instance.storeInstanceByElement(this.element, this);
+ }
+
+ /**
+ * Component being initialized.
+ */
+ async ngOnInit(): Promise {
+ try {
+ const header = await this.searchHeader();
+ if (header) {
+ // Search the right buttons container (start, end or any).
+ let selector = 'ion-buttons';
+
+ let slot = this.element.getAttribute('slot');
+ // Take the slot from the parent if it has.
+ if (!slot && this.element.parentElement) {
+ slot = this.element.parentElement.getAttribute('slot');
+ }
+ if (slot) {
+ selector += '[slot="' + slot + '"]';
+ }
+
+ const buttonsContainer = header.querySelector(selector);
+ if (buttonsContainer) {
+ this.mergeContextMenus(buttonsContainer);
+
+ const prepend = this.element.hasAttribute('prepend');
+
+ this.movedChildren = CoreDomUtils.instance.moveChildren(this.element, buttonsContainer, prepend);
+ this.showHideAllElements();
+
+ } else {
+ this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
+ }
+ }
+ } catch (error) {
+ // Header not found.
+ this.logger.warn(error);
+ }
+ }
+
+ /**
+ * Force or unforce hiding all buttons. If this is true, it will override the "hidden" input.
+ *
+ * @param value The value to set.
+ */
+ forceHide(value: boolean): void {
+ this.forceHidden = value;
+
+ this.showHideAllElements();
+ }
+
+ /**
+ * If both button containers have a context menu, merge them into a single one.
+ *
+ * @param buttonsContainer The container where the buttons will be moved.
+ * @todo
+ */
+ protected mergeContextMenus(buttonsContainer: HTMLElement): void {
+ // Check if both button containers have a context menu.
+ const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
+ if (!mainContextMenu) {
+ return;
+ }
+
+ const secondaryContextMenu = this.element.querySelector('core-context-menu');
+ if (!secondaryContextMenu) {
+ return;
+ }
+
+ // Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
+ const mainContextMenuInstance: CoreContextMenuComponent = CoreDomUtils.instance.getInstanceByElement(mainContextMenu);
+ const secondaryContextMenuInstance: CoreContextMenuComponent =
+ CoreDomUtils.instance.getInstanceByElement(secondaryContextMenu);
+
+ // Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
+ if (mainContextMenuInstance && secondaryContextMenuInstance) {
+ this.mergedContextMenu = secondaryContextMenuInstance;
+
+ this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance);
+
+ // Remove the empty context menu from the DOM.
+ secondaryContextMenu.parentElement?.removeChild(secondaryContextMenu);
+ }
+ }
+
+ /**
+ * Search the ion-header where the buttons should be added.
+ *
+ * @param retries Number of retries so far.
+ * @return Promise resolved with the header element.
+ */
+ protected async searchHeader(retries: number = 0): Promise {
+ let parentPage: HTMLElement = this.element;
+
+ while (parentPage) {
+ if (!parentPage.parentElement) {
+ // No parent, stop.
+ break;
+ }
+
+ // Get the next parent page.
+ parentPage = CoreDomUtils.instance.closest(parentPage.parentElement, '.ion-page');
+ if (parentPage) {
+ // Check if the page has a header. If it doesn't, search the next parent page.
+ const header = this.searchHeaderInPage(parentPage);
+ if (header && getComputedStyle(header, null).display != 'none') {
+ return header;
+ }
+ }
+ }
+
+ // Header not found.
+ if (retries < 5) {
+ // If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
+ // Try again after a while.
+ return new Promise((resolve, reject): void => {
+ setTimeout(() => {
+ // eslint-disable-next-line promise/catch-or-return
+ this.searchHeader(retries + 1).then(resolve, reject);
+ }, 200);
+ });
+ }
+
+ // We've waited enough time, reject.
+ throw Error('Header not found.');
+ }
+
+ /**
+ * Search ion-header inside a page. The header should be a direct child.
+ *
+ * @param page Page to search in.
+ * @return Header element. Undefined if not found.
+ */
+ protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined {
+ for (let i = 0; i < page.children.length; i++) {
+ const child = page.children[i];
+ if (child.tagName == 'ION-HEADER') {
+ return child;
+ }
+ }
+ }
+
+ /**
+ * Show or hide all the elements.
+ */
+ protected showHideAllElements(): void {
+ // Show or hide all moved children.
+ if (this.movedChildren) {
+ this.movedChildren.forEach((child: Node) => {
+ this.showHideElement(child);
+ });
+ }
+
+ // Show or hide all the context menu items that were merged to another context menu.
+ if (this.mergedContextMenu) {
+ if (this.forceHidden || this.allButtonsHidden) {
+ this.mergedContextMenu.removeMergedItems();
+ } else {
+ this.mergedContextMenu.restoreMergedItems();
+ }
+ }
+ }
+
+ /**
+ * Show or hide an element.
+ *
+ * @param element Element to show or hide.
+ */
+ protected showHideElement(element: Node): void {
+ // Check if it's an HTML Element
+ if (element instanceof Element) {
+ element.classList.toggle(BUTTON_HIDDEN_CLASS, !!this.forceHidden || !!this.allButtonsHidden);
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ CoreDomUtils.instance.removeInstanceById(this.instanceId);
+
+ // This component was destroyed, remove all the buttons that were moved.
+ // The buttons can be moved outside of the current page, that's why we need to manually destroy them.
+ // There's no need to destroy context menu items that were merged because they weren't moved from their DOM position.
+ if (this.movedChildren) {
+ this.movedChildren.forEach((child) => {
+ if (child.parentElement) {
+ child.parentElement.removeChild(child);
+ }
+ });
+ }
+
+ if (this.mergedContextMenu) {
+ this.mergedContextMenu.removeMergedItems();
+ }
+ }
+
+}
diff --git a/src/core/components/progress-bar/core-progress-bar.html b/src/core/components/progress-bar/core-progress-bar.html
new file mode 100644
index 000000000..6edeb841e
--- /dev/null
+++ b/src/core/components/progress-bar/core-progress-bar.html
@@ -0,0 +1,5 @@
+= 0">
+
+ {{ 'core.percentagenumber' | translate: {$a: text} }}
+
diff --git a/src/core/components/progress-bar/progress-bar.scss b/src/core/components/progress-bar/progress-bar.scss
new file mode 100644
index 000000000..e82f88fa1
--- /dev/null
+++ b/src/core/components/progress-bar/progress-bar.scss
@@ -0,0 +1,33 @@
+:host {
+ display: flex;
+
+ .core-progress-text {
+ line-height: 40px;
+ font-size: 1rem;
+ color: var(--text-color);
+ width: 55px;
+ text-align: center;
+ }
+
+ progress {
+ -webkit-appearance: none;
+ appearance: none;
+ height: var(--height);
+ margin: 16px 0;
+ padding: 0;
+ display: block;
+ width: calc(100% - 55px);
+
+ &[value]::-webkit-progress-bar {
+ background-color: var(--background);
+ border-radius: 0;
+ border: 0;
+ box-shadow: none;
+ }
+
+ &[value]::-webkit-progress-value {
+ background-color: var(--color);
+ border-radius: 0;
+ }
+ }
+}
diff --git a/src/core/components/progress-bar/progress-bar.ts b/src/core/components/progress-bar/progress-bar.ts
new file mode 100644
index 000000000..39267d2f0
--- /dev/null
+++ b/src/core/components/progress-bar/progress-bar.ts
@@ -0,0 +1,71 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, Input, OnChanges, SimpleChange, ChangeDetectionStrategy } from '@angular/core';
+import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
+
+/**
+ * Component to show a progress bar and its value.
+ *
+ * Example usage:
+ *
+ */
+@Component({
+ selector: 'core-progress-bar',
+ templateUrl: 'core-progress-bar.html',
+ styleUrls: ['progress-bar.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class CoreProgressBarComponent implements OnChanges {
+
+ @Input() progress!: number | string; // Percentage from 0 to 100.
+ @Input() text?: string; // Percentage in text to be shown at the right. If not defined, progress will be used.
+ width?: SafeStyle;
+ protected textSupplied = false;
+
+ constructor(private sanitizer: DomSanitizer) { }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: { [name: string]: SimpleChange }): void {
+ if (changes.text && typeof changes.text.currentValue != 'undefined') {
+ // User provided a custom text, don't use default.
+ this.textSupplied = true;
+ }
+
+ if (changes.progress) {
+ // Progress has changed.
+ if (typeof this.progress == 'string') {
+ this.progress = parseInt(this.progress, 10);
+ }
+
+ if (this.progress < 0 || isNaN(this.progress)) {
+ this.progress = -1;
+ }
+
+ if (this.progress != -1) {
+ // Remove decimals.
+ this.progress = Math.floor(this.progress);
+
+ if (!this.textSupplied) {
+ this.text = String(this.progress);
+ }
+
+ this.width = this.sanitizer.bypassSecurityTrustStyle(this.progress + '%');
+ }
+ }
+ }
+
+}
diff --git a/src/core/components/recaptcha/core-recaptchamodal.html b/src/core/components/recaptcha/core-recaptchamodal.html
index e49d8989b..604fefa7c 100644
--- a/src/core/components/recaptcha/core-recaptchamodal.html
+++ b/src/core/components/recaptcha/core-recaptchamodal.html
@@ -4,11 +4,11 @@
-
+
-
\ No newline at end of file
+
diff --git a/src/core/components/tabs/core-tabs.html b/src/core/components/tabs/core-tabs.html
index 731d8f3e6..fd8cbfd53 100644
--- a/src/core/components/tabs/core-tabs.html
+++ b/src/core/components/tabs/core-tabs.html
@@ -1,9 +1,9 @@
-
+
{{ 'core.settings.spaceusage' | translate }}
-
-
-
-
+
+
+
+
+
diff --git a/src/core/features/settings/pages/synchronization/synchronization.html b/src/core/features/settings/pages/synchronization/synchronization.html
index 587cf2b4a..3ac2cd268 100644
--- a/src/core/features/settings/pages/synchronization/synchronization.html
+++ b/src/core/features/settings/pages/synchronization/synchronization.html
@@ -5,10 +5,11 @@
{{ 'core.settings.synchronization' | translate }}
-
-
-
-
+
+
+
+
+
diff --git a/src/core/features/settings/services/settings.helper.ts b/src/core/features/settings/services/settings.helper.ts
index 6f9022b6b..54deb5a30 100644
--- a/src/core/features/settings/services/settings.helper.ts
+++ b/src/core/features/settings/services/settings.helper.ts
@@ -24,7 +24,7 @@ import { CoreConstants } from '@/core/constants';
import { CoreConfig } from '@services/config';
// import { CoreFilterProvider } from '@features/filter/providers/filter';
import { CoreDomUtils } from '@services/utils/dom';
-// import { CoreCourseProvider } from '@features/course/providers/course';
+import { CoreCourse } from '@features/course/services/course';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreError } from '@classes/errors/error';
@@ -58,7 +58,6 @@ export class CoreSettingsHelperProvider {
constructor() {
// protected filterProvider: CoreFilterProvider,
- // protected courseProvider: CoreCourseProvider,
if (!CoreConstants.CONFIG.forceColorScheme) {
// Update color scheme when a user enters or leaves a site, or when the site info is updated.
@@ -116,7 +115,7 @@ export class CoreSettingsHelperProvider {
promises.push(site.deleteFolder().then(() => {
filepoolService.clearAllPackagesStatus(siteId);
filepoolService.clearFilepool(siteId);
- // this.courseProvider.clearAllCoursesStatus(siteId);
+ CoreCourse.instance.clearAllCoursesStatus(siteId);
siteInfo.spaceUsage = 0;
diff --git a/src/core/features/sitehome/lang/en.json b/src/core/features/sitehome/lang/en.json
new file mode 100644
index 000000000..acf7f742f
--- /dev/null
+++ b/src/core/features/sitehome/lang/en.json
@@ -0,0 +1,4 @@
+{
+ "sitehome": "Site home",
+ "sitenews": "Site announcements"
+}
diff --git a/src/core/features/sitehome/pages/index/index.html b/src/core/features/sitehome/pages/index/index.html
new file mode 100644
index 000000000..ba85e91f8
--- /dev/null
+++ b/src/core/features/sitehome/pages/index/index.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.courses.availablecourses' | translate}}
+
+
+
+
+
+
+ News (TODO)
+
+
+
+
+
+
+
+
+ {{ 'core.courses.categories' | translate}}
+
+
+
+
+
+
+
+
+ {{ 'core.courses.mycourses' | translate}}
+
+
+
+
+
+
+ {{ 'core.courses.searchcourses' | translate}}
+
+
diff --git a/src/core/features/sitehome/pages/index/index.page.module.ts b/src/core/features/sitehome/pages/index/index.page.module.ts
new file mode 100644
index 000000000..59e2a1986
--- /dev/null
+++ b/src/core/features/sitehome/pages/index/index.page.module.ts
@@ -0,0 +1,47 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule, Routes } from '@angular/router';
+import { IonicModule } from '@ionic/angular';
+import { TranslateModule } from '@ngx-translate/core';
+
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CoreComponentsModule } from '@components/components.module';
+
+import { CoreSiteHomeIndexPage } from './index.page';
+
+const routes: Routes = [
+ {
+ path: '',
+ component: CoreSiteHomeIndexPage,
+ },
+];
+
+@NgModule({
+ imports: [
+ RouterModule.forChild(routes),
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreDirectivesModule,
+ CoreComponentsModule,
+ ],
+ declarations: [
+ CoreSiteHomeIndexPage,
+ ],
+ exports: [RouterModule],
+})
+export class CoreSiteHomeIndexPageModule {}
diff --git a/src/core/features/sitehome/pages/index/index.page.ts b/src/core/features/sitehome/pages/index/index.page.ts
new file mode 100644
index 000000000..a9aa029c1
--- /dev/null
+++ b/src/core/features/sitehome/pages/index/index.page.ts
@@ -0,0 +1,214 @@
+// (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 { ActivatedRoute } from '@angular/router';
+import { IonRefresher, NavController } from '@ionic/angular';
+
+import { CoreSite, CoreSiteConfig } from '@classes/site';
+import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
+import { CoreDomUtils } from '@services/utils/dom';
+import { CoreSites } from '@services/sites';
+import { CoreSiteHome } from '@features/sitehome/services/sitehome';
+import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses';
+import { CoreEventObserver, CoreEvents } from '@singletons/events';
+import { CoreCourseHelper } from '@features/course/services/course.helper';
+
+/**
+ * Page that displays site home index.
+ */
+@Component({
+ selector: 'page-core-sitehome-index',
+ templateUrl: 'index.html',
+})
+export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
+
+ // @todo @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent;
+
+ dataLoaded = false;
+ section?: CoreCourseSection & {
+ hasContent?: boolean;
+ };
+
+ hasContent = false;
+ items: string[] = [];
+ siteHomeId?: number;
+ currentSite?: CoreSite;
+ searchEnabled = false;
+ downloadEnabled = false;
+ downloadCourseEnabled = false;
+ downloadCoursesEnabled = false;
+ downloadEnabledIcon = 'far-square';
+
+ protected updateSiteObserver?: CoreEventObserver;
+
+ constructor(
+ protected route: ActivatedRoute,
+ protected navCtrl: NavController,
+ // @todo private prefetchDelegate: CoreCourseModulePrefetchDelegate,
+ ) {}
+
+ /**
+ * Page being initialized.
+ */
+ ngOnInit(): void {
+ const navParams = this.route.snapshot.queryParams;
+
+ this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
+ this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
+ this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
+
+ // Refresh the enabled flags if site is updated.
+ this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
+ this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
+ this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
+ this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
+
+ this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled);
+ }, CoreSites.instance.getCurrentSiteId());
+
+ this.currentSite = CoreSites.instance.getCurrentSite()!;
+ this.siteHomeId = this.currentSite.getSiteHomeId();
+
+ const module = navParams['module'];
+ if (module) {
+ // @todo const modParams = navParams.get('modParams');
+ // CoreCourseHelper.instance.openModule(module, this.siteHomeId, undefined, modParams);
+ }
+
+ this.loadContent().finally(() => {
+ this.dataLoaded = true;
+ });
+ }
+
+ /**
+ * Convenience function to fetch the data.
+ *
+ * @return Promise resolved when done.
+ */
+ protected async loadContent(): Promise
{
+ this.hasContent = false;
+
+ const config = this.currentSite!.getStoredConfig() || { numsections: 1, frontpageloggedin: undefined };
+
+ this.items = await CoreSiteHome.instance.getFrontPageItems(config.frontpageloggedin);
+ this.hasContent = this.items.length > 0;
+
+ try {
+ const sections = await CoreCourse.instance.getSections(this.siteHomeId!, false, true);
+
+ // Check "Include a topic section" setting from numsections.
+ this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
+ if (this.section) {
+ this.section.hasContent = false;
+ this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section);
+ /* @todo this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules(
+ [this.section],
+ this.siteHomeId,
+ undefined,
+ undefined,
+ true,
+ ) || this.hasContent;*/
+ }
+
+ // Add log in Moodle.
+ CoreCourse.instance.logView(
+ this.siteHomeId!,
+ undefined,
+ undefined,
+ this.currentSite!.getInfo()?.sitename,
+ ).catch(() => {
+ // Ignore errors.
+ });
+ } catch (error) {
+ CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
+ }
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param refresher Refresher.
+ */
+ doRefresh(refresher?: CustomEvent): void {
+ const promises: Promise[] = [];
+
+ promises.push(CoreCourse.instance.invalidateSections(this.siteHomeId!));
+ promises.push(this.currentSite!.invalidateConfig().then(async () => {
+ // Config invalidated, fetch it again.
+ const config: CoreSiteConfig = await this.currentSite!.getConfig();
+ this.currentSite!.setConfig(config);
+
+ return;
+ }));
+
+ if (this.section && this.section.modules) {
+ // Invalidate modules prefetch data.
+ // @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
+ }
+
+ // @todo promises.push(this.courseBlocksComponent.invalidateBlocks());
+
+ Promise.all(promises).finally(async () => {
+ const p2: Promise[] = [];
+
+ p2.push(this.loadContent());
+ // @todo p2.push(this.courseBlocksComponent.loadContent());
+
+ await Promise.all(p2).finally(() => {
+ refresher?.detail.complete();
+ });
+ });
+ }
+
+ /**
+ * Toggle 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 });
+ }
+
+ /**
+ * Open page to manage courses storage.
+ */
+ manageCoursesStorage(): void {
+ // @todo this.navCtrl.navigateForward(['/courses/storage']);
+ }
+
+ /**
+ * Go to search courses.
+ */
+ openSearch(): void {
+ this.navCtrl.navigateForward(['/courses/search']);
+ }
+
+ /**
+ * Component being destroyed.
+ */
+ ngOnDestroy(): void {
+ this.updateSiteObserver?.off();
+ }
+
+}
diff --git a/src/core/features/sitehome/services/handlers/index.link.ts b/src/core/features/sitehome/services/handlers/index.link.ts
new file mode 100644
index 000000000..aa97435c4
--- /dev/null
+++ b/src/core/features/sitehome/services/handlers/index.link.ts
@@ -0,0 +1,75 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { Params } from '@angular/router';
+import { CoreSites } from '@services/sites';
+import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
+import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks.helper';
+import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks.delegate';
+import { CoreSiteHome } from '../sitehome';
+
+/**
+ * Handler to treat links to site home index.
+ */
+Injectable();
+export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase {
+
+ name = 'CoreSiteHomeIndexLinkHandler';
+ featureName = 'CoreMainMenuDelegate_CoreSiteHome';
+ pattern = /\/course\/view\.php.*([?&]id=\d+)/;
+
+ /**
+ * 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}
+ * @param courseId Course ID related to the URL. Optional but recommended.
+ * @return List of (or promise resolved with list of) actions.
+ */
+ getActions(): CoreContentLinksAction[] | Promise {
+ return [{
+ action: (siteId: string): void => {
+ CoreContentLinksHelper.instance.goInSite('sitehome', [], siteId);
+ },
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param siteId The site ID.
+ * @param url The URL to treat.
+ * @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param courseId Course ID related to the URL. Optional but recommended.
+ * @return Whether the handler is enabled for the URL and site.
+ */
+ async isEnabled(siteId: string, url: string, params: Params, courseId?: number): Promise {
+ courseId = parseInt(params.id, 10);
+ if (!courseId) {
+ return false;
+ }
+
+ const site = await CoreSites.instance.getSite(siteId);
+ if (courseId != site.getSiteHomeId()) {
+ // The course is not site home.
+ return false;
+ }
+
+ return CoreSiteHome.instance.isAvailable(siteId).then(() => true).catch(() => false);
+ }
+
+}
diff --git a/src/core/features/sitehome/services/handlers/sitehome.home.ts b/src/core/features/sitehome/services/handlers/sitehome.home.ts
new file mode 100644
index 000000000..3848f7b55
--- /dev/null
+++ b/src/core/features/sitehome/services/handlers/sitehome.home.ts
@@ -0,0 +1,66 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreSites } from '@services/sites';
+import { CoreHomeHandler, CoreHomeHandlerToDisplay } from '@features/mainmenu/services/home.delegate';
+import { CoreSiteHome } from '../sitehome';
+
+/**
+ * Handler to add site home into home page.
+ */
+Injectable();
+export class CoreSiteHomeHomeHandler implements CoreHomeHandler {
+
+ name = 'CoreSiteHomeDashboard';
+ priority = 1200;
+
+ /**
+ * Check if the handler is enabled on a site level.
+ *
+ * @return Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): Promise {
+ 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 {
+ return CoreSiteHome.instance.isAvailable(siteId);
+ }
+
+ /**
+ * Returns the data needed to render the handler.
+ *
+ * @return Data needed to render the handler.
+ */
+ getDisplayData(): CoreHomeHandlerToDisplay {
+ const site = CoreSites.instance.getCurrentSite();
+ const displaySiteHome = site?.getInfo() && site?.getInfo()?.userhomepage === 0;
+
+ return {
+ title: 'core.sitehome.sitehome',
+ page: 'sitehome',
+ class: 'core-sitehome-dashboard-handler',
+ icon: 'fas-home',
+ selectPriority: displaySiteHome ? 1100 : 900,
+ };
+ }
+
+}
diff --git a/src/core/features/sitehome/services/sitehome.ts b/src/core/features/sitehome/services/sitehome.ts
new file mode 100644
index 000000000..6714dfd6e
--- /dev/null
+++ b/src/core/features/sitehome/services/sitehome.ts
@@ -0,0 +1,196 @@
+// (C) Copyright 2015 Moodle Pty Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+
+import { CoreSites } from '@services/sites';
+import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
+import { makeSingleton } from '@singletons/core.singletons';
+import { CoreCourse, CoreCourseSection } from '../../course/services/course';
+import { CoreCourses } from '../../courses/services/courses';
+
+/**
+ * Items with index 1 and 3 were removed on 2.5 and not being supported in the app.
+ */
+export enum FrontPageItemNames {
+ NEWS_ITEMS = 0,
+ LIST_OF_CATEGORIES = 2,
+ COMBO_LIST = 3,
+ ENROLLED_COURSES = 5,
+ LIST_OF_COURSE = 6,
+ COURSE_SEARCH_BOX = 7,
+}
+
+/**
+ * Service that provides some features regarding site home.
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class CoreSiteHomeProvider {
+
+ /**
+ * Get the news forum for the Site Home.
+ *
+ * @param siteHomeId Site Home ID.
+ * @return Promise resolved with the forum if found, rejected otherwise.
+ */
+ getNewsForum(): void {
+ // @todo params and logic.
+ }
+
+ /**
+ * Invalidate the WS call to get the news forum for the Site Home.
+ *
+ * @param siteHomeId Site Home ID.
+ * @return Promise resolved when invalidated.
+ */
+ invalidateNewsForum(): void {
+ // @todo params and logic.
+ }
+
+ /**
+ * Returns whether or not the frontpage is available for the current site.
+ *
+ * @param siteId The site ID. If not defined, current site.
+ * @return Promise resolved with boolean: whether it's available.
+ */
+ async isAvailable(siteId?: string): Promise {
+ try {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ // First check if it's disabled.
+ if (this.isDisabledInSite(site)) {
+ return false;
+ }
+
+ // Use a WS call to check if there's content in the site home.
+ const siteHomeId = site.getSiteHomeId();
+ const preSets: CoreSiteWSPreSets = { emergencyCache: false };
+
+ try {
+ const sections: CoreCourseSection[] =
+ await CoreCourse.instance.getSections(siteHomeId, false, true, preSets, site.id);
+
+ if (!sections || !sections.length) {
+ throw Error('No sections found');
+ }
+
+ const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length));
+
+ if (hasContent) {
+ // There's a section with content.
+ return true;
+ }
+ } catch {
+ // Ignore errors.
+ }
+
+ const config = site.getStoredConfig();
+ if (config && config.frontpageloggedin) {
+ const items = await this.getFrontPageItems(config.frontpageloggedin);
+
+ // There are items to show.
+ return items.length > 0;
+ }
+ } catch {
+ // Ignore errors.
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if Site Home is disabled in a certain site.
+ *
+ * @param siteId Site Id. If not defined, use current site.
+ * @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
+ */
+ async isDisabled(siteId?: string): Promise {
+ const site = await CoreSites.instance.getSite(siteId);
+
+ return this.isDisabledInSite(site);
+ }
+
+ /**
+ * Check if Site Home is disabled in a certain site.
+ *
+ * @param site Site. If not defined, use current site.
+ * @return Whether it's disabled.
+ */
+ isDisabledInSite(site: CoreSite): boolean {
+ site = site || CoreSites.instance.getCurrentSite();
+
+ return site.isFeatureDisabled('CoreMainMenuDelegate_CoreSiteHome');
+ }
+
+ /**
+ * Get the nams of the valid frontpage items.
+ *
+ * @param frontpageItemIds CSV string with indexes of site home components.
+ * @return Valid names for each item.
+ */
+ async getFrontPageItems(frontpageItemIds?: string): Promise {
+ if (!frontpageItemIds) {
+ return [];
+ }
+
+ const items = frontpageItemIds.split(',');
+
+ const filteredItems: string[] = [];
+
+ for (const item of items) {
+ let itemNumber = parseInt(item, 10);
+
+ let add = false;
+ switch (itemNumber) {
+ case FrontPageItemNames['NEWS_ITEMS']:
+ // @todo
+ add = true;
+ break;
+ case FrontPageItemNames['LIST_OF_CATEGORIES']:
+ case FrontPageItemNames['COMBO_LIST']:
+ case FrontPageItemNames['LIST_OF_COURSE']:
+ add = CoreCourses.instance.isGetCoursesByFieldAvailable();
+ if (add && itemNumber == FrontPageItemNames['COMBO_LIST']) {
+ itemNumber = FrontPageItemNames['LIST_OF_CATEGORIES'];
+ }
+ break;
+ case FrontPageItemNames['ENROLLED_COURSES']:
+ if (!CoreCourses.instance.isMyCoursesDisabledInSite()) {
+ const courses = await CoreCourses.instance.getUserCourses();
+
+ add = courses.length > 0;
+ }
+ break;
+ case FrontPageItemNames['COURSE_SEARCH_BOX']:
+ add = !CoreCourses.instance.isSearchCoursesDisabledInSite();
+ break;
+ default:
+ break;
+ }
+
+ // Do not add an item twice.
+ if (add && filteredItems.indexOf(FrontPageItemNames[itemNumber]) < 0) {
+ filteredItems.push(FrontPageItemNames[itemNumber]);
+ }
+ }
+
+ return filteredItems;
+ }
+
+}
+
+export class CoreSiteHome extends makeSingleton(CoreSiteHomeProvider) {}
+
diff --git a/src/core/features/sitehome/sitehome-init.module.ts b/src/core/features/sitehome/sitehome-init.module.ts
new file mode 100644
index 000000000..7739e17aa
--- /dev/null
+++ b/src/core/features/sitehome/sitehome-init.module.ts
@@ -0,0 +1,53 @@
+// (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 { Routes } from '@angular/router';
+
+import { CoreSiteHomeIndexLinkHandler } from './services/handlers/index.link';
+import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks.delegate';
+import { CoreSiteHomeHomeHandler } from './services/handlers/sitehome.home';
+import { CoreHomeDelegate } from '@features/mainmenu/services/home.delegate';
+import { CoreHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module';
+
+const routes: Routes = [
+ {
+ path: 'sitehome',
+ loadChildren: () =>
+ import('@features/sitehome/pages/index/index.page.module').then(m => m.CoreSiteHomeIndexPageModule),
+ },
+];
+
+@NgModule({
+ imports: [CoreHomeRoutingModule.forChild(routes)],
+ exports: [CoreHomeRoutingModule],
+ providers: [
+ CoreSiteHomeIndexLinkHandler,
+ CoreSiteHomeHomeHandler,
+ ],
+})
+export class CoreSiteHomeInitModule {
+
+ constructor(
+ contentLinksDelegate: CoreContentLinksDelegate,
+ homeDelegate: CoreHomeDelegate,
+ siteHomeIndexLinkHandler: CoreSiteHomeIndexLinkHandler,
+ siteHomeDashboardHandler: CoreSiteHomeHomeHandler,
+ ) {
+ contentLinksDelegate.registerHandler(siteHomeIndexLinkHandler);
+ homeDelegate.registerHandler(siteHomeDashboardHandler);
+
+ }
+
+}
diff --git a/src/core/services/groups.ts b/src/core/services/groups.ts
index b82596a84..1959cd999 100644
--- a/src/core/services/groups.ts
+++ b/src/core/services/groups.ts
@@ -21,6 +21,8 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseBase } from '@/types/global';
+const ROOT_CACHE_KEY = 'mmGroups:';
+
/*
* Service to handle groups.
*/
@@ -31,7 +33,6 @@ export class CoreGroupsProvider {
static readonly NOGROUPS = 0;
static readonly SEPARATEGROUPS = 1;
static readonly VISIBLEGROUPS = 2;
- protected readonly ROOT_CACHE_KEY = 'mmGroups:';
/**
* Check if group mode of an activity is enabled.
@@ -65,12 +66,12 @@ export class CoreGroupsProvider {
userId?: number,
siteId?: string,
ignoreCache?: boolean,
- ): Promise {
+ ): Promise {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
- const params = {
+ const params: CoreGroupGetActivityAllowedGroupsWSParams = {
cmid: cmId,
userid: userId,
};
@@ -84,7 +85,7 @@ export class CoreGroupsProvider {
preSets.emergencyCache = false;
}
- const response: CoreGroupGetActivityAllowedGroupsResponse =
+ const response: CoreGroupGetActivityAllowedGroupsWSResponse =
await site.read('core_group_get_activity_allowed_groups', params, preSets);
if (!response || !response.groups) {
@@ -102,7 +103,7 @@ export class CoreGroupsProvider {
* @return Cache key.
*/
protected getActivityAllowedGroupsCacheKey(cmId: number, userId: number): string {
- return this.ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId;
+ return ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId;
}
/**
@@ -115,7 +116,7 @@ export class CoreGroupsProvider {
* @return Promise resolved when the groups are retrieved. If not allowed, empty array will be returned.
*/
async getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean):
- Promise {
+ Promise {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Get real groupmode, in case it's forced by the course.
@@ -157,7 +158,7 @@ export class CoreGroupsProvider {
groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS;
groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS;
- let result: CoreGroupGetActivityAllowedGroupsResponse;
+ let result: CoreGroupGetActivityAllowedGroupsWSResponse;
if (groupInfo.separateGroups || groupInfo.visibleGroups) {
result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
} else {
@@ -195,7 +196,7 @@ export class CoreGroupsProvider {
*/
async getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise {
const site = await CoreSites.instance.getSite(siteId);
- const params = {
+ const params: CoreGroupGetActivityGroupmodeWSParams = {
cmid: cmId,
};
const preSets: CoreSiteWSPreSets = {
@@ -208,7 +209,7 @@ export class CoreGroupsProvider {
preSets.emergencyCache = false;
}
- const response: CoreGroupGetActivityGroupModeResponse =
+ const response: CoreGroupGetActivityGroupModeWSResponse =
await site.read('core_group_get_activity_groupmode', params, preSets);
if (!response || typeof response.groupmode == 'undefined') {
@@ -225,7 +226,7 @@ export class CoreGroupsProvider {
* @return Cache key.
*/
protected getActivityGroupModeCacheKey(cmId: number): string {
- return this.ROOT_CACHE_KEY + 'groupmode:' + cmId;
+ return ROOT_CACHE_KEY + 'groupmode:' + cmId;
}
/**
@@ -274,7 +275,7 @@ export class CoreGroupsProvider {
async getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise {
const site = await CoreSites.instance.getSite(siteId);
userId = userId || site.getUserId();
- const data = {
+ const data: CoreGroupGetCourseUserGroupsWSParams = {
userid: userId,
courseid: courseId,
};
@@ -283,7 +284,7 @@ export class CoreGroupsProvider {
updateFrequency: CoreSite.FREQUENCY_RARELY,
};
- const response: CoreGroupGetCourseUserGroupsResponse =
+ const response: CoreGroupGetCourseUserGroupsWSResponse =
await site.read('core_group_get_course_user_groups', data, preSets);
if (!response || !response.groups) {
@@ -299,7 +300,7 @@ export class CoreGroupsProvider {
* @return Prefix Cache key.
*/
protected getUserGroupsInCoursePrefixCacheKey(): string {
- return this.ROOT_CACHE_KEY + 'courseGroups:';
+ return ROOT_CACHE_KEY + 'courseGroups:';
}
/**
@@ -474,24 +475,48 @@ export type CoreGroupInfo = {
/**
* WS core_group_get_activity_allowed_groups response type.
*/
-export type CoreGroupGetActivityAllowedGroupsResponse = {
+export type CoreGroupGetActivityAllowedGroupsWSResponse = {
groups: CoreGroup[]; // List of groups.
canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups.
warnings?: CoreWSExternalWarning[];
};
+/**
+ * Params of core_group_get_activity_groupmode WS.
+ */
+type CoreGroupGetActivityGroupmodeWSParams = {
+ cmid: number; // Course module id.
+};
+
/**
* Result of WS core_group_get_activity_groupmode.
*/
-export type CoreGroupGetActivityGroupModeResponse = {
+export type CoreGroupGetActivityGroupModeWSResponse = {
groupmode: number; // Group mode: 0 for no groups, 1 for separate groups, 2 for visible groups.
warnings?: CoreWSExternalWarning[];
};
+/**
+ * Params of core_group_get_activity_allowed_groups WS.
+ */
+type CoreGroupGetActivityAllowedGroupsWSParams = {
+ cmid: number; // Course module id.
+ userid?: number; // Id of user, empty for current user.
+};
+
+/**
+ * Params of core_group_get_course_user_groups WS.
+ */
+type CoreGroupGetCourseUserGroupsWSParams = {
+ courseid?: number; // Id of course (empty or 0 for all the courses where the user is enrolled).
+ userid?: number; // Id of user (empty or 0 for current user).
+ groupingid?: number; // Returns only groups in the specified grouping.
+};
+
/**
* Result of WS core_group_get_course_user_groups.
*/
-export type CoreGroupGetCourseUserGroupsResponse = {
+export type CoreGroupGetCourseUserGroupsWSResponse = {
groups: {
id: number; // Group record id.
name: string; // Multilang compatible name, course unique.
diff --git a/src/core/services/ws.ts b/src/core/services/ws.ts
index cd7b37868..967b48947 100644
--- a/src/core/services/ws.ts
+++ b/src/core/services/ws.ts
@@ -990,6 +990,13 @@ export type CoreStatusWithWarningsWSResponse = {
warnings?: CoreWSExternalWarning[];
};
+/**
+ * Special response structure of many webservices that contains only warnings.
+ */
+export type CoreWarningsWSResponse = {
+ warnings?: CoreWSExternalWarning[];
+};
+
/**
* Structure of files returned by WS.
*/
diff --git a/src/core/singletons/events.ts b/src/core/singletons/events.ts
index eeb8f554c..e58dc9740 100644
--- a/src/core/singletons/events.ts
+++ b/src/core/singletons/events.ts
@@ -228,3 +228,11 @@ export type CoreEventLoadPageMainMenuData = {
redirectPage: string;
redirectParams?: Params;
};
+
+/**
+ * Data passed to COURSE_STATUS_CHANGED event.
+ */
+export type CoreEventCourseStatusChanged = {
+ courseId: number; // Course Id.
+ status: string;
+};
diff --git a/src/theme/app.scss b/src/theme/app.scss
index 568622430..f48f74a42 100644
--- a/src/theme/app.scss
+++ b/src/theme/app.scss
@@ -10,6 +10,10 @@ ion-toolbar .in-toolbar.button-clear {
--color: var(--ion-color-primary-contrast);
}
+ion-toolbar .core-navbar-button-hidden {
+ display: none !important;
+}
+
// Ionic icon.
ion-icon {
&.icon-slash::after,
@@ -64,6 +68,11 @@ ion-alert.core-alert-network-error .alert-head {
right: unset;
left: -15%;
}
+ion-alert.core-nohead {
+ .alert-head {
+ padding-bottom: 0;
+ }
+}
// Ionic item divider.
ion-item-divider {
@@ -76,6 +85,16 @@ ion-list.list-md {
padding-bottom: 0;
}
+// Header.
+ion-tabs.hide-header ion-header {
+ display: none;
+}
+ion-toolbar {
+ ion-spinner {
+ margin: 10px;
+ }
+}
+
// Modals.
.core-modal-fullscreen .modal-wrapper {
position: absolute;
diff --git a/src/theme/variables.scss b/src/theme/variables.scss
index 20a65148c..e35b04f3c 100644
--- a/src/theme/variables.scss
+++ b/src/theme/variables.scss
@@ -103,6 +103,10 @@
ion-toolbar {
--color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
--background: var(--custom-toolbar-background, var(--ion-color-primary));
+
+ ion-spinner {
+ --color: var(--custom-toolbar-color, var(--ion-color-primary-contrast));
+ }
}
ion-action-sheet {
@@ -135,13 +139,32 @@
--color: var(--core-color);
}
+ core-progress-bar {
+ --height: var(--custom-progress-bar-height, 8px);
+ --color: var(--custom-progress-color, var(--core-color));
+ --text-color: var(--custom-progress-text-color, var(--gray-darker));
+ --background: var(--custom-progress-background, var(--gray-lighter));
+ }
+
--selected-item-color: var(--custom-selected-item-color, var(--core-color));
--selected-item-border-width: var(--custom-selected-item-border-width, 5px);
--drop-shadow: var(--custom-drop-shadow, 0, 0, 0, 0.2);
--core-login-background: var(--custom-login-background, var(--white));
- --core-login-text-color: var(--custom-login-text-color, var(--black));
+ --core-login-text-color: var(--custom-login-text-color, var(--black));
+
+ --core-course-color-0: var(--custom-course-color-0, #81ecec);
+ --core-course-color-1: var(--custom-course-color-1, #74b9ff);
+ --core-course-color-2: var(--custom-course-color-2, #a29bfe);
+ --core-course-color-3: var(--custom-course-color-3, #dfe6e9);
+ --core-course-color-4: var(--custom-course-color-4, #00b894);
+ --core-course-color-5: var(--custom-course-color-5, #0984e3);
+ --core-course-color-6: var(--custom-course-color-6, #b2bec3);
+ --core-course-color-7: var(--custom-course-color-7, #fdcb6e);
+ --core-course-color-8: var(--custom-course-color-9, #fd79a8);
+ --core-course-color-9: var(--custom-course-color-90, #6c5ce7);
+ --core-star-color: var(--custom-star-color, var(--core-color));
}
/*
@@ -196,6 +219,10 @@
}
}
+ core-progress-bar {
+ --text-color: var(--custom-progress-text-color, var(--gray-lighter));
+ }
+
--core-login-background: var(--custom-login-background, #3a3a3a);
--core-login-text-color: var(--custom-login-text-color, white);
}