From 29f6d6cd396c35a3c27a3bb7869cd3b9fcb866c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 8 Nov 2021 16:19:08 +0100 Subject: [PATCH] MOBILE-3806 course: Merge course progress and course list items components --- .../myoverview/addon-block-myoverview.html | 10 +- .../addon-block-recentlyaccessedcourses.html | 20 +- .../addon-block-starredcourses.html | 22 +- .../core-courses-course-list-item.html | 157 +++++++++--- .../course-list-item/course-list-item.scss | 176 +++++++++++++- .../course-list-item/course-list-item.ts | 229 ++++++++++++++++-- .../course-progress/course-progress.scss | 6 - .../course-progress/course-progress.ts | 1 - .../courses/pages/categories/categories.html | 18 +- src/core/features/courses/services/courses.ts | 7 +- src/theme/theme.base.scss | 4 + 11 files changed, 536 insertions(+), 114 deletions(-) diff --git a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html index 2394d5a8b..884437ea6 100644 --- a/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html +++ b/src/addons/block/myoverview/components/myoverview/addon-block-myoverview.html @@ -90,11 +90,11 @@
- - - + + + diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html index d4a503b64..07c5b2027 100644 --- a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html @@ -8,13 +8,13 @@ (click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate"> - + {{prefetchCoursesData.badge}} - + +
@@ -25,16 +25,12 @@ -
+
- +
diff --git a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html index 32fbdc9fb..8de46a904 100644 --- a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html +++ b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html @@ -8,13 +8,13 @@ (click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate"> - + {{prefetchCoursesData.badge}} - + +
@@ -25,17 +25,13 @@ -
+
- +
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 425d8e3f3..772b58c0b 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 @@ -1,47 +1,124 @@ - - - - + + + + + -

- - -

-

- - - - | - - - - -

-

- + + +

+ + + + | + + + + +

+

+ + + {{ 'core.courses.aria:favourite' | translate }} + {{ 'core.courses.aria:coursename' | translate }} + + +

+ + + + + + + + + + + +

+

- - - - -
- + + +
+ +
+ + + + +

+ {{ 'core.courses.aria:coursecategory' | translate }} + + + + | + + + + +

+

+ + + {{ 'core.courses.aria:favourite' | translate }} + {{ 'core.courses.aria:coursename' | translate }} + + +

+
+ + + +
+
+ +
+ +
+
+
+ + + +
+
- + +
+ + + + + + + + + + +
+
diff --git a/src/core/features/courses/components/course-list-item/course-list-item.scss b/src/core/features/courses/components/course-list-item/course-list-item.scss index 4807e1b59..928f993e2 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.scss +++ b/src/core/features/courses/components/course-list-item/course-list-item.scss @@ -1,13 +1,12 @@ @import "~theme/globals"; -:host { +.core-course-list-item { .course-icon { color: white; background: var(--gray-light); padding: 8px; font-size: 24px; border-radius: 50%; - margin-inline-end: 16px; -webkit-transition: all 50ms ease-in-out; transition: all 50ms ease-in-out; } @@ -22,4 +21,177 @@ -webkit-transition: all 50ms ease-in-out; transition: all 50ms ease-in-out; } + + .core-course-thumb { + @include margin(12px, 16px, 12px, null); + align-self: flex-start; + } + + .core-course-summary { + margin-top: 12px; + } +} + +.item-heading ion-icon { + margin-right: 4px; + color: var(--core-star-color); +} + +ion-card { + --vertical-margin: 12px; + + display: flex; + flex-direction: column; + align-self: stretch; + height: calc(100% - var(--vertical-margin) - var(--vertical-margin)); + margin-top: var(--vertical-margin); + margin-bottom: var(--vertical-margin); + + @for $i from 0 to length($core-course-image-background) { + &[course-color="#{$i}"] .core-course-thumb { + background: var(--core-course-color-#{$i}); + } + } + + ion-row { + min-height: var(--a11y-min-target-size); + ion-col .core-button-spinner { + min-width: calc(var(--a11y-min-target-size) + 16px); + } + } + + .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: var(--ion-item-background); + } + + img { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + margin: auto; + } + } + + @if ($core-course-hide-thumb-on-cards) { + .core-course-thumb { + display: none; + } + } + + @if ($core-course-thumb-on-cards-background) { + .core-course-thumb { + background: $core-course-thumb-on-cards-background !important; + } + } + + .core-course-additional-info { + margin-bottom: 8px; + } + + .core-course-header { + flex-grow: 1; + display: flex; + flex-direction: column; + + --inner-padding-end: 0px; + + &::part(native) { + flex-grow: 1; + align-items: self-start; + } + + &.core-course-only-title { + &::part(native) { + flex-grow: 1; + } + + } + + .core-course-title { + margin: 12px 0; + flex-grow: 1; + width: 100%; + max-width: 100%; + } + + .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; + } + } + + @if ($core-course-hide-progress-on-cards) { + .core-course-progress { + display: none; + } + } +} + +button { + z-index: 1; +} + +:host-context(.core-horizontal-scroll) { + @include horizontal_scroll_item(80%, 250px, 300px); + + ion-card { + .core-course-thumb { + padding-top: 30%; + } + + ion-item.core-course-header { + --padding-start: 4px; + + .core-course-title { + margin: 7px 0; + + .item-heading ion-icon { + margin-right: 2px; + } + } + .core-button-spinner { + min-height: 40px; + min-width: 40px; + + ion-spinner { + width: 20px; + height: 20px; + } + } + .item-button[icon-only] { + min-width: 40px; + width: 40px; + padding: 8px; + } + + } + } } diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 6d4da5a97..98e98ba99 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -12,15 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { CoreConstants } from '@/core/constants'; import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { CoreCourseProvider, CoreCourse } from '@features/course/services/course'; import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; +import { CoreUser } from '@features/user/services/user'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; +import { Translate } from '@singletons'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; -import { CoreCourseListItem, CoreCourses } from '../../services/courses'; -import { CoreCoursesHelper } from '../../services/courses-helper'; +import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses'; +import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; +import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; /** * This directive is meant to display an item for a list of courses. @@ -37,10 +41,10 @@ import { CoreCoursesHelper } from '../../services/courses-helper'; export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, OnChanges { @Input() course!: CoreCourseListItem; // The course to render. - @Input() showDownload = false; // If true, will show download button. + @Input() layout: 'listwithenrol'|'summarycard'|'list'|'card' = 'listwithenrol'; - icons: CoreCoursesEnrolmentIcons[] = []; + enrolmentIcons: CoreCoursesEnrolmentIcons[] = []; isEnrolled = false; prefetchCourseData: CorePrefetchStatusInfo = { icon: '', @@ -49,8 +53,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On loading: true, }; - protected courseStatusObserver?: CoreEventObserver; + showSpinner = false; + downloadCourseEnabled = false; + courseOptionMenuEnabled = false; + progress = -1; + completionUserTracked: boolean | undefined = false; + + protected courseStatus = CoreConstants.NOT_DOWNLOADED; protected isDestroyed = false; + protected courseStatusObserver?: CoreEventObserver; + protected siteUpdatedObserver?: CoreEventObserver; /** * @inheritdoc @@ -58,48 +70,71 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On async ngOnInit(): Promise { CoreCoursesHelper.loadCourseColorAndImage(this.course); - this.isEnrolled = this.course.progress !== undefined; + // Assume is enroled if mode is not listwithenrol. + this.isEnrolled = this.layout != 'listwithenrol' || this.course.progress !== undefined; if (!this.isEnrolled) { try { const course = await CoreCourses.getUserCourse(this.course.id); - this.course.progress = course.progress; - this.course.completionusertracked = course.completionusertracked; + this.course = Object.assign(this.course, course); + this.updateCourseFields(); this.isEnrolled = true; - - if (this.showDownload) { - this.initPrefetchCourse(); - } } catch { this.isEnrolled = false; } } - if (!this.isEnrolled) { - this.icons = []; + if (this.isEnrolled) { + if (this.showDownload) { + this.initPrefetchCourse(); + } + + this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); + + if (this.downloadCourseEnabled) { + this.initPrefetchCourse(); + } + + // This field is only available from 3.6 onwards. + this.courseOptionMenuEnabled = (this.layout != 'listwithenrol' && this.layout != 'summarycard') && + 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.isDownloadCourseDisabledInSite(); + + if (!wasEnabled && this.downloadCourseEnabled) { + // Download course is enabled now, initialize it. + this.initPrefetchCourse(); + } + }, CoreSites.getCurrentSiteId()); + } else if ('enrollmentmethods' in this.course) { + this.enrolmentIcons = []; this.course.enrollmentmethods.forEach((instance) => { if (instance === 'self') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.selfenrolment', icon: 'fas-key', }); } else if (instance === 'guest') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.allowguests', icon: 'fas-unlock', }); } else if (instance === 'paypal') { - this.icons.push({ + this.enrolmentIcons.push({ label: 'core.courses.paypalaccepted', icon: 'fab-paypal', }); } }); - if (this.icons.length == 0) { - this.icons.push({ + if (this.enrolmentIcons.length == 0) { + this.enrolmentIcons.push({ label: 'core.courses.notenrollable', icon: 'fas-lock', }); @@ -114,6 +149,16 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On if (this.showDownload && this.isEnrolled) { this.initPrefetchCourse(); } + + this.updateCourseFields(); + } + + /** + * Helper function to update course fields. + */ + protected updateCourseFields(): void { + this.progress = 'progress' in this.course ? this.course.progress || -1 : -1; + this.completionUserTracked = 'completionusertracked' in this.course && this.course.completionusertracked; } /** @@ -179,6 +224,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On protected updateCourseStatus(status: string): void { const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status); + this.courseStatus = status; this.prefetchCourseData.status = statusData.status; this.prefetchCourseData.icon = statusData.icon; this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable; @@ -188,11 +234,11 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On /** * Prefetch the course. * - * @param e Click event. + * @param event Click event. */ - async prefetchCourse(e?: Event): Promise { - e?.preventDefault(); - e?.stopPropagation(); + async prefetchCourse(event?: Event): Promise { + event?.preventDefault(); + event?.stopPropagation(); try { await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course); @@ -203,12 +249,149 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On } } + /** + * Delete the course. + */ + async deleteCourse(): Promise { + try { + await CoreDomUtils.showDeleteConfirm('core.course.confirmdeletestoreddata'); + } catch (error) { + if (CoreDomUtils.isCanceledError(error)) { + throw error; + } + + return; + } + + const modal = await CoreDomUtils.showModalLoading(); + + try { + await CoreCourseHelper.deleteCourseFiles(this.course.id); + } catch (error) { + CoreDomUtils.showErrorModalDefault(error, Translate.instant('core.errordeletefile')); + } finally { + modal.dismiss(); + } + } + + /** + * Show the context menu. + * + * @param event Click Event. + */ + async showCourseOptionsMenu(event: Event): Promise { + event.preventDefault(); + event.stopPropagation(); + + const popoverData = await CoreDomUtils.openPopover({ + component: CoreCoursesCourseOptionsMenuComponent, + componentProps: { + course: this.course, + prefetch: this.prefetchCourseData, + }, + event: event, + }); + + switch (popoverData) { + case 'download': + if (!this.prefetchCourseData.loading) { + this.prefetchCourse(event); + } + 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. + */ + protected async setCourseHidden(hide: boolean): Promise { + this.showSpinner = true; + + // We should use null to unset the preference. + try { + await CoreUser.updateUserPreference( + 'block_myoverview_hidden_course_' + this.course.id, + hide ? '1' : undefined, + ); + + this.course.hidden = hide; + + ( this.course).hidden = hide; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_HIDDEN, + value: hide, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course visibility.'); + } + } finally { + this.showSpinner = false; + } + } + + /** + * Favourite/Unfavourite the course from the course list. + * + * @param favourite True to favourite and false to unfavourite. + */ + protected async setCourseFavourite(favourite: boolean): Promise { + this.showSpinner = true; + + try { + await CoreCourses.setFavouriteCourse(this.course.id, favourite); + + this.course.isfavourite = favourite; + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.course.id, + course: this.course, + action: CoreCoursesProvider.ACTION_STATE_CHANGED, + state: CoreCoursesProvider.STATE_FAVOURITE, + value: favourite, + }, CoreSites.getCurrentSiteId()); + + } catch (error) { + if (!this.isDestroyed) { + CoreDomUtils.showErrorModalDefault(error, 'Error changing course favourite attribute.'); + } + } finally { + this.showSpinner = false; + } + } + /** * @inheritdoc */ ngOnDestroy(): void { this.isDestroyed = true; this.courseStatusObserver?.off(); + this.siteUpdatedObserver?.off(); } } diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss index 07024b58f..28a70f437 100644 --- a/src/core/features/courses/components/course-progress/course-progress.scss +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -151,9 +151,3 @@ } } } - -: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 index ece9a9b50..626c6c9a8 100644 --- a/src/core/features/courses/components/course-progress/course-progress.ts +++ b/src/core/features/courses/components/course-progress/course-progress.ts @@ -221,7 +221,6 @@ export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy, On component: CoreCoursesCourseOptionsMenuComponent, componentProps: { course: this.course, - courseStatus: this.courseStatus, prefetch: this.prefetchCourseData, }, event: e, diff --git a/src/core/features/courses/pages/categories/categories.html b/src/core/features/courses/pages/categories/categories.html index 7ecd32f4b..495001569 100644 --- a/src/core/features/courses/pages/categories/categories.html +++ b/src/core/features/courses/pages/categories/categories.html @@ -10,11 +10,10 @@ - + [content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()" iconAction="toggle" + [(toggle)]="downloadEnabled"> + @@ -28,12 +27,12 @@

- + +

+ [contextInstanceId]="currentCategory.id">

@@ -45,8 +44,7 @@
- +

diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index fa9a973c9..c28874747 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -19,7 +19,7 @@ import { makeSingleton } from '@singletons'; import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { CoreEvents } from '@singletons/events'; import { CoreWSError } from '@classes/errors/wserror'; -import { CoreCourseWithImageAndColor } from './courses-helper'; +import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper'; const ROOT_CACHE_KEY = 'mmCourses:'; @@ -1384,7 +1384,10 @@ export type CoreCourseSearchedData = CoreCourseBasicSearchedData & { /** * Course to render as list item. */ -export type CoreCourseListItem = CoreCourseSearchedData & CoreCourseWithImageAndColor & { +export type CoreCourseListItem = ((CoreCourseSearchedData & CoreCourseWithImageAndColor) | +CoreCourseAnyCourseDataWithExtraInfoAndOptions) & { + isfavourite?: boolean; // If the user marked this course a favourite. + hidden?: boolean; // If the user hide the course from the dashboard. completionusertracked?: boolean; // If the user is completion tracked. progress?: number | null; // Progress percentage. }; diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index c4c8a8d00..c7150accd 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -1163,3 +1163,7 @@ iframe { display: none !important; } } + +ion-grid.core-no-grid > ion-row { + display: block; +} \ No newline at end of file