From cc6e87ea5c9bede5a7efa0100606291712f15e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 20 Nov 2020 11:59:36 +0100 Subject: [PATCH] MOBILE-3594 sitehome: Add my courses page --- .../core-download-refresh.html | 19 +- .../download-refresh/download-refresh.ts | 3 +- .../courses/components/components.module.ts | 11 +- .../core-courses-course-options-menu.html | 26 ++ .../course-options-menu.ts | 59 ++++ .../core-courses-course-progress.html | 57 ++++ .../course-progress/course-progress.scss | 163 ++++++++++ .../course-progress/course-progress.ts | 283 ++++++++++++++++++ src/core/features/courses/courses.module.ts | 8 +- .../courses/pages/my-courses/my-courses.html | 48 +++ .../my-courses/my-courses.page.module.ts | 51 ++++ .../pages/my-courses/my-courses.page.ts | 215 +++++++++++++ src/theme/app.scss | 15 + src/theme/variables.scss | 16 + 14 files changed, 963 insertions(+), 11 deletions(-) create mode 100644 src/core/features/courses/components/course-options-menu/core-courses-course-options-menu.html create mode 100644 src/core/features/courses/components/course-options-menu/course-options-menu.ts create mode 100644 src/core/features/courses/components/course-progress/core-courses-course-progress.html create mode 100644 src/core/features/courses/components/course-progress/course-progress.scss create mode 100644 src/core/features/courses/components/course-progress/course-progress.ts create mode 100644 src/core/features/courses/pages/my-courses/my-courses.html create mode 100644 src/core/features/courses/pages/my-courses/my-courses.page.module.ts create mode 100644 src/core/features/courses/pages/my-courses/my-courses.page.ts diff --git a/src/core/components/download-refresh/core-download-refresh.html b/src/core/components/download-refresh/core-download-refresh.html index 633654ff2..a58e42784 100644 --- a/src/core/components/download-refresh/core-download-refresh.html +++ b/src/core/components/download-refresh/core-download-refresh.html @@ -1,20 +1,25 @@ - + + class="core-animate-show-hide" [attr.aria-label]="(statusTranslatable || 'core.download') | translate"> - + (click)="download($event, true)" color="dark" class="core-animate-show-hide" + attr.aria-label]="(statusTranslatable || 'core.refresh') | translate"> + - + + + - \ No newline at end of file + diff --git a/src/core/components/download-refresh/download-refresh.ts b/src/core/components/download-refresh/download-refresh.ts index 0b96d9102..393202f27 100644 --- a/src/core/components/download-refresh/download-refresh.ts +++ b/src/core/components/download-refresh/download-refresh.ts @@ -19,7 +19,7 @@ import { CoreConstants } from '@/core/constants'; * Component to show a download button with refresh option, the spinner and the status of it. * * Usage: - * + * */ @Component({ selector: 'core-download-refresh', @@ -29,6 +29,7 @@ import { CoreConstants } from '@/core/constants'; export class CoreDownloadRefreshComponent { @Input() status?: string; // Download status. + @Input() statusTranslatable?: string; // Download status translatable string. @Input() enabled = false; // Whether the download is enabled. @Input() loading = true; // Force loading status when is not downloading. @Input() canTrustDownload = false; // If false, refresh will be shown if downloaded. diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts index 513b7aee1..6a74f8671 100644 --- a/src/core/features/courses/components/components.module.ts +++ b/src/core/features/courses/components/components.module.ts @@ -22,10 +22,14 @@ import { CoreDirectivesModule } from '@directives/directives.module'; import { CorePipesModule } from '@pipes/pipes.module'; import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item'; +import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress'; +import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu'; @NgModule({ declarations: [ CoreCoursesCourseListItemComponent, + CoreCoursesCourseProgressComponent, + CoreCoursesCourseOptionsMenuComponent, ], imports: [ CommonModule, @@ -35,10 +39,13 @@ import { CoreCoursesCourseListItemComponent } from './course-list-item/course-li CoreDirectivesModule, CorePipesModule, ], - providers: [ - ], exports: [ CoreCoursesCourseListItemComponent, + CoreCoursesCourseProgressComponent, + CoreCoursesCourseOptionsMenuComponent, + ], + entryComponents: [ + CoreCoursesCourseOptionsMenuComponent, ], }) export class CoreCoursesComponentsModule {} diff --git a/src/core/features/courses/components/course-options-menu/core-courses-course-options-menu.html b/src/core/features/courses/components/course-options-menu/core-courses-course-options-menu.html new file mode 100644 index 000000000..95e5294e3 --- /dev/null +++ b/src/core/features/courses/components/course-options-menu/core-courses-course-options-menu.html @@ -0,0 +1,26 @@ + + + +

{{ prefetch.statusTranslatable | translate }}

+
+ + +

{{ 'addon.storagemanager.deletecourse' | translate }}

+
+ + +

{{ 'core.courses.hidecourse' | translate }}

+
+ + +

{{ 'core.courses.show' | translate }}

+
+ + +

{{ 'core.courses.addtofavourites' | translate }}

+
+ + +

{{ 'core.courses.removefromfavourites' | translate }}

+
+ diff --git a/src/core/features/courses/components/course-options-menu/course-options-menu.ts b/src/core/features/courses/components/course-options-menu/course-options-menu.ts new file mode 100644 index 000000000..db2e6ca64 --- /dev/null +++ b/src/core/features/courses/components/course-options-menu/course-options-menu.ts @@ -0,0 +1,59 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit } from '@angular/core'; +import { NavParams, PopoverController } from '@ionic/angular'; +import { CoreCourses } from '../../services/courses'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper'; +import { CorePrefetchStatusInfo } from '@features/course/services/course.helper'; + +/** + * This component is meant to display a popover with the course options. + */ +@Component({ + selector: 'core-courses-course-options-menu', + templateUrl: 'core-courses-course-options-menu.html', +}) +export class CoreCoursesCourseOptionsMenuComponent implements OnInit { + + course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course. + prefetch!: CorePrefetchStatusInfo; // The prefecth info. + + downloadCourseEnabled = false; + + constructor( + navParams: NavParams, + protected popoverController: PopoverController, + ) { + this.course = navParams.get('course') || {}; + this.prefetch = navParams.get('prefetch') || {}; + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + } + + /** + * Do an action over the course. + * + * @param action Action name to take. + */ + action(action: string): void { + this.popoverController.dismiss(action); + } + +} diff --git a/src/core/features/courses/components/course-progress/core-courses-course-progress.html b/src/core/features/courses/components/course-progress/core-courses-course-progress.html new file mode 100644 index 000000000..84acd224b --- /dev/null +++ b/src/core/features/courses/components/course-progress/core-courses-course-progress.html @@ -0,0 +1,57 @@ + +
+ +
+ + +

+ + + + | + + + + +

+

+ + +

+
+ +
+ +
+ +
+ + + + + + + +
+
+ + + + +
diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss new file mode 100644 index 000000000..688838d8c --- /dev/null +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -0,0 +1,163 @@ +:host { + ion-card { + display: flex; + flex-direction: column; + align-self: stretch; + height: calc(100% - 20px); + + &[course-color="0"] .core-course-thumb { + background: var(--core-course-image-background-0); + } + &[course-color="1"] .core-course-thumb { + background: var(--core-course-image-background-1); + } + &[course-color="2"] .core-course-thumb { + background: var(--core-course-image-background-2); + } + &[course-color="3"] .core-course-thumb { + background: var(--core-course-image-background-3); + } + &[course-color="4"] .core-course-thumb { + background: var(--core-course-image-background-4); + } + &[course-color="5"] .core-course-thumb { + background: var(--core-course-image-background-5); + } + &[course-color="6"] .core-course-thumb { + background: var(--core-course-image-background-6); + } + &[course-color="7"] .core-course-thumb { + background: var(--core-course-image-background-7); + } + &[course-color="8"] .core-course-thumb { + background: var(--core-course-image-background-8); + } + &[course-color="9"] .core-course-thumb { + background: var(--core-course-image-background-9); + } + + .core-course-thumb { + padding-top: 40%; + width: 100%; + overflow: hidden; + cursor: pointer; + pointer-events: auto; + position: relative; + background-position: center; + background-size: cover; + -webkit-transition: all 50ms ease-in-out; + transition: all 50ms ease-in-out; + + &.core-course-color-img { + background: white; + } + + img { + position: absolute; + top: 0; + bottom: 0; + margin: auto; + } + } + + .core-course-additional-info { + margin-bottom: 8px; + } + + .core-course-header { + padding-top: 8px; + padding-bottom: 8px; + .core-course-title { + margin: 5px 0; + flex-grow: 1; + + h2 ion-icon { + margin-right: 4px; + color: var(--core-star-color); + } + } + + &.core-course-more-than-title { + padding-bottom: 0; + } + + .core-button-spinner { + margin: 0; + } + .core-button-spinner ion-spinner { + vertical-align: top; // the better option for most scenarios + vertical-align: -webkit-baseline-middle; // the best for those that support it + } + + .core-button-spinner .core-icon-downloaded { + font-size: 28.8px; + margin-top: 8px; + vertical-align: top; + } + + .item-button[icon-only] { + min-width: 50px; + width: 50px; + } + } + } + + button { + z-index: 1; + } +} + +// @todo +:host-context(.core-horizontal-scroll) { + /*@include horizontal_scroll_item(80%, 250px, 300px);*/ + + ion-card { + .core-course-thumb { + padding-top: 30%; + } + + .core-course-link { + /*@include padding(4px, 0px, 4px, 8px);*/ + .core-course-additional-info { + font-size: 1.2rem; + } + + .core-course-title { + margin: 3px 0; + + h2 { + font-size: 1.5rem; + ion-icon { + margin-right: 2px; + } + } + + &.core-course-with-buttons { + max-width: calc(100% - 40px); + } + } + .core-button-spinner { + min-height: 40px; + min-width: 40px; + + ion-spinner { + width: 20px; + height: 20px; + } + } + .item-button[icon-only] { + min-width: 40px; + width: 40px; + font-size: 1.5rem; + padding: 8px; + } + + } + } +} + +:host-context(body.version-3-1) { + .core-course-thumb{ + display: none; + } +} diff --git a/src/core/features/courses/components/course-progress/course-progress.ts b/src/core/features/courses/components/course-progress/course-progress.ts new file mode 100644 index 000000000..446902294 --- /dev/null +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -0,0 +1,283 @@ +// (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 } from '@angular/core'; +import { PopoverController } from '@ionic/angular'; +import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +// import { CoreUserProvider } from '@core/user/providers/user'; +import { CoreCourses } from '@features/courses/services/courses'; +import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; +import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course.helper'; +import { Translate } from '@singletons/core.singletons'; +import { CoreConstants } from '@/core/constants'; +import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper'; +import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; + +/** + * This component is meant to display a course for a list of courses with progress. + * + * Example usage: + * + * + * + */ +@Component({ + selector: 'core-courses-course-progress', + templateUrl: 'core-courses-course-progress.html', + styleUrls: ['course-progress.scss'], +}) +export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy { + + @Input() course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course to render. + @Input() showAll = false; // If true, will show all actions, options, star and progress. + @Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown. + + courseStatus = CoreConstants.NOT_DOWNLOADED; + isDownloading = false; + prefetchCourseData: CorePrefetchStatusInfo = { + icon: '', + statusTranslatable: 'core.loading', + status: '', + loading: true, + }; + + showSpinner = false; + downloadCourseEnabled = false; + courseOptionMenuEnabled = false; + + protected isDestroyed = false; + protected courseStatusObserver?: CoreEventObserver; + protected siteUpdatedObserver?: CoreEventObserver; + + constructor( + protected popoverCtrl: PopoverController, + ) { } + + /** + * Component being initialized. + */ + ngOnInit(): void { + + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + + if (this.downloadCourseEnabled) { + this.initPrefetchCourse(); + } + + // This field is only available from 3.6 onwards. + this.courseOptionMenuEnabled = this.showAll && typeof this.course.isfavourite != 'undefined'; + + // Refresh the enabled flag if site is updated. + this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + const wasEnabled = this.downloadCourseEnabled; + + this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); + + if (!wasEnabled && this.downloadCourseEnabled) { + // Download course is enabled now, initialize it. + this.initPrefetchCourse(); + } + }, CoreSites.instance.getCurrentSiteId()); + } + + /** + * Initialize prefetch course. + */ + async initPrefetchCourse(): Promise { + if (typeof this.courseStatusObserver != 'undefined') { + // Already initialized. + return; + } + + // Listen for status change in course. + this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => { + if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + this.updateCourseStatus(data.status); + } + }, CoreSites.instance.getCurrentSiteId()); + + // Determine course prefetch icon. + const status = await CoreCourse.instance.getCourseStatus(this.course.id); + + this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status); + this.courseStatus = status; + + if (this.prefetchCourseData.loading) { + // Course is being downloaded. Get the download promise. + const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id); + if (promise) { + // There is a download promise. If it fails, show an error. + promise.catch((error) => { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + }); + } else { + // No download, this probably means that the app was closed while downloading. Set previous status. + CoreCourse.instance.setCoursePreviousStatus(this.course.id); + } + } + + } + + /** + * Open a course. + */ + openCourse(): void { + CoreCourseHelper.instance.openCourse(this.course); + } + + /** + * Prefetch the course. + * + * @param e Click event. + */ + prefetchCourse(e: Event): void { + e.preventDefault(); + e.stopPropagation(); + + /* @ todo try { + CoreCourseHelper.instance.confirmAndPrefetchCourse(this.prefetchCourseData, this.course); + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + }*/ + } + + /** + * Delete the course. + */ + async deleteCourse(): Promise { + try { + await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (CoreDomUtils.instance.isCanceledError(error)) { + throw error; + } + + return; + } + + const modal = await CoreDomUtils.instance.showModalLoading(); + + try { + await CoreCourseHelper.instance.deleteCourseFiles(this.course.id); + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Update the course status icon and title. + * + * @param status Status to show. + */ + protected updateCourseStatus(status: string): void { + this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status); + + this.courseStatus = status; + } + + /** + * Show the context menu. + * + * @param e Click Event. + * @todo + */ + async showCourseOptionsMenu(e: Event): Promise { + e.preventDefault(); + e.stopPropagation(); + + const popover = await this.popoverCtrl.create({ + component: CoreCoursesCourseOptionsMenuComponent, + componentProps: { + course: this.course, + courseStatus: this.courseStatus, + prefetch: this.prefetchCourseData, + }, + event: e, + }); + popover.present(); + + const action = await popover.onDidDismiss(); + + if (action.data) { + switch (action.data) { + case 'download': + if (!this.prefetchCourseData.loading) { + this.prefetchCourse(e); + } + break; + case 'delete': + if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') { + this.deleteCourse(); + } + break; + case 'hide': + this.setCourseHidden(true); + break; + case 'show': + this.setCourseHidden(false); + break; + case 'favourite': + this.setCourseFavourite(true); + break; + case 'unfavourite': + this.setCourseFavourite(false); + break; + default: + break; + } + } + + } + + /** + * Hide/Unhide the course from the course list. + * + * @param hide True to hide and false to show. + * @todo + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected setCourseHidden(hide: boolean): void { + return; + } + + /** + * Favourite/Unfavourite the course from the course list. + * + * @param favourite True to favourite and false to unfavourite. + * @todo + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected setCourseFavourite(favourite: boolean): void { + return; + } + + /** + * Component destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + + this.siteUpdatedObserver?.off(); + this.courseStatusObserver?.off(); + } + +} diff --git a/src/core/features/courses/courses.module.ts b/src/core/features/courses/courses.module.ts index b1a4e1cac..8845e88b5 100644 --- a/src/core/features/courses/courses.module.ts +++ b/src/core/features/courses/courses.module.ts @@ -32,7 +32,7 @@ const routes: Routes = [ children: [ { path: '', - redirectTo: 'all', + redirectTo: 'my', pathMatch: 'full', }, { @@ -57,6 +57,12 @@ const routes: Routes = [ import('@features/courses/pages/search/search.page.module') .then(m => m.CoreCoursesSearchPageModule), }, + { + path: 'my', + loadChildren: () => + import('@features/courses/pages/my-courses/my-courses.page.module') + .then(m => m.CoreCoursesMyCoursesPageModule), + }, ], }, ]; diff --git a/src/core/features/courses/pages/my-courses/my-courses.html b/src/core/features/courses/pages/my-courses/my-courses.html new file mode 100644 index 000000000..dd07c029f --- /dev/null +++ b/src/core/features/courses/pages/my-courses/my-courses.html @@ -0,0 +1,48 @@ + + + + + + {{ 'core.courses.mycourses' | translate }} + + + + + + + + + + + {{downloadAllCoursesBadge}} + + + + + + + + + + + + + + + + + + + + + +

{{ 'core.courses.searchcoursesadvice' | translate }}

+
+
+
diff --git a/src/core/features/courses/pages/my-courses/my-courses.page.module.ts b/src/core/features/courses/pages/my-courses/my-courses.page.module.ts new file mode 100644 index 000000000..2dd0d13a5 --- /dev/null +++ b/src/core/features/courses/pages/my-courses/my-courses.page.module.ts @@ -0,0 +1,51 @@ +// (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 { FormsModule } from '@angular/forms'; + +import { CoreComponentsModule } from '@components/components.module'; +import { CoreDirectivesModule } from '@directives/directives.module'; + +import { CoreCoursesMyCoursesPage } from './my-courses.page'; +import { CoreCoursesComponentsModule } from '../../components/components.module'; + +const routes: Routes = [ + { + path: '', + component: CoreCoursesMyCoursesPage, + }, +]; + +@NgModule({ + imports: [ + RouterModule.forChild(routes), + CommonModule, + FormsModule, + IonicModule, + TranslateModule.forChild(), + CoreComponentsModule, + CoreDirectivesModule, + CoreCoursesComponentsModule, + ], + declarations: [ + CoreCoursesMyCoursesPage, + ], + exports: [RouterModule], +}) +export class CoreCoursesMyCoursesPageModule { } diff --git a/src/core/features/courses/pages/my-courses/my-courses.page.ts b/src/core/features/courses/pages/my-courses/my-courses.page.ts new file mode 100644 index 000000000..c9b9aa1ae --- /dev/null +++ b/src/core/features/courses/pages/my-courses/my-courses.page.ts @@ -0,0 +1,215 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; +import { NavController, IonSearchbar, IonRefresher } from '@ionic/angular'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; +import { CoreSites } from '@services/sites'; +import { CoreDomUtils } from '@services/utils/dom'; +import { + CoreCoursesProvider, + CoreCoursesMyCoursesUpdatedEventData, + CoreCourses, +} from '../../services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper'; +import { CoreCourseHelper } from '@features/course/services/course.helper'; +import { CoreConstants } from '@/core/constants'; +// import { CoreCourseOptionsDelegate } from '@core/course/services/options-delegate'; + +/** + * Page that displays the list of courses the user is enrolled in. + */ +@Component({ + selector: 'page-core-courses-my-courses', + templateUrl: 'my-courses.html', +}) +export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy { + + @ViewChild(IonSearchbar) searchbar!: IonSearchbar; + + courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = []; + filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = []; + searchEnabled = false; + filter = ''; + showFilter = false; + coursesLoaded = false; + downloadAllCoursesIcon = CoreConstants.NOT_DOWNLOADED_ICON; + downloadAllCoursesLoading = false; + downloadAllCoursesBadge = ''; + downloadAllCoursesEnabled = false; + + protected myCoursesObserver: CoreEventObserver; + protected siteUpdatedObserver: CoreEventObserver; + protected isDestroyed = false; + protected courseIds = ''; + + constructor( + protected navCtrl: NavController, + ) { + // Update list if user enrols in a course. + this.myCoursesObserver = CoreEvents.on( + CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, + (data: CoreCoursesMyCoursesUpdatedEventData) => { + + if (data.action == CoreCoursesProvider.ACTION_ENROL) { + this.fetchCourses(); + } + }, + + CoreSites.instance.getCurrentSiteId(), + ); + + // Refresh the enabled flags if site is updated. + this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { + this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + }, CoreSites.instance.getCurrentSiteId()); + } + + /** + * Component being initialized. + */ + ngOnInit(): void { + this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite(); + this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); + + this.fetchCourses().finally(() => { + this.coursesLoaded = true; + }); + } + + /** + * Fetch the user courses. + * + * @return Promise resolved when done. + */ + protected async fetchCourses(): Promise { + try { + const courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = await CoreCourses.instance.getUserCourses(); + const courseIds = courses.map((course) => course.id); + + this.courseIds = courseIds.join(','); + + await CoreCoursesHelper.instance.loadCoursesExtraInfo(courses); + + if (CoreCourses.instance.canGetAdminAndNavOptions()) { + const options = await CoreCourses.instance.getCoursesAdminAndNavOptions(courseIds); + courses.forEach((course) => { + course.navOptions = options.navOptions[course.id]; + course.admOptions = options.admOptions[course.id]; + }); + } + + this.courses = courses; + this.filteredCourses = this.courses; + this.filter = ''; + } catch (error) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true); + } + } + + /** + * Refresh the courses. + * + * @param refresher Refresher. + */ + refreshCourses(refresher: CustomEvent): void { + const promises: Promise[] = []; + + promises.push(CoreCourses.instance.invalidateUserCourses()); + // @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions()); + if (this.courseIds) { + promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds)); + } + + Promise.all(promises).finally(() => { + this.fetchCourses().finally(() => { + refresher?.detail.complete(); + }); + }); + } + + /** + * Show or hide the filter. + */ + switchFilter(): void { + this.filter = ''; + this.showFilter = !this.showFilter; + this.filteredCourses = this.courses; + if (this.showFilter) { + setTimeout(() => { + this.searchbar.setFocus(); + }, 500); + } + } + + /** + * The filter has changed. + * + * @param Received Event. + */ + filterChanged(event?: Event): void { + const target = event?.target || null; + const newValue = target ? String(target.value).trim().toLowerCase() : null; + if (!newValue || !this.courses) { + this.filteredCourses = this.courses; + } else { + // Use displayname if avalaible, or fullname if not. + if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') { + this.filteredCourses = this.courses.filter((course) => course.displayname!.toLowerCase().indexOf(newValue) > -1); + } else { + this.filteredCourses = this.courses.filter((course) => course.fullname.toLowerCase().indexOf(newValue) > -1); + } + } + } + + /** + * Prefetch all the courses. + * + * @return Promise resolved when done. + */ + async prefetchCourses(): Promise { + this.downloadAllCoursesLoading = true; + + try { + await CoreCourseHelper.instance.confirmAndPrefetchCourses(this.courses, (progress) => { + this.downloadAllCoursesBadge = progress.count + ' / ' + progress.total; + }); + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); + } + } + + this.downloadAllCoursesBadge = ''; + this.downloadAllCoursesLoading = false; + } + + /** + * Go to search courses. + */ + openSearch(): void { + this.navCtrl.navigateForward(['/courses/search']); + } + + /** + * Page destroyed. + */ + ngOnDestroy(): void { + this.isDestroyed = true; + this.myCoursesObserver?.off(); + this.siteUpdatedObserver?.off(); + } + +} diff --git a/src/theme/app.scss b/src/theme/app.scss index 568622430..45ee03c21 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -64,6 +64,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 +81,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 2a7b7a9e0..1bda05033 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 { @@ -149,6 +153,18 @@ --core-login-background: var(--custom-login-background, var(--white)); --core-login-text-color: var(--custom-login-text-color, var(--black)); + + --core-course-image-background-0: var(--custom-course-image-background-0, #81ecec); + --core-course-image-background-1: var(--custom-course-image-background-1, #74b9ff); + --core-course-image-background-2: var(--custom-course-image-background-2, #a29bfe); + --core-course-image-background-3: var(--custom-course-image-background-3, #dfe6e9); + --core-course-image-background-4: var(--custom-course-image-background-4, #00b894); + --core-course-image-background-5: var(--custom-course-image-background-5, #0984e3); + --core-course-image-background-6: var(--custom-course-image-background-6, #b2bec3); + --core-course-image-background-7: var(--custom-course-image-background-7, #fdcb6e); + --core-course-image-background-8: var(--custom-course-image-background-9, #fd79a8); + --core-course-image-background-9: var(--custom-course-image-background-90, #6c5ce7); + --core-star-color: var(--custom-star-color, var(--core-color)); } /*