diff --git a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss index 1f7d02104..77fad00dd 100644 --- a/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss +++ b/src/addons/block/recentlyaccesseditems/components/recentlyaccesseditems/recentlyaccesseditems.scss @@ -6,6 +6,14 @@ ion-card { height: auto; } + + .ion-text-wrap ion-label { + .item-heading, h2, p { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } } .core-course-module-handler { diff --git a/src/core/components/download-refresh/download-refresh.ts b/src/core/components/download-refresh/download-refresh.ts index e0ca3cea4..26487f98b 100644 --- a/src/core/components/download-refresh/download-refresh.ts +++ b/src/core/components/download-refresh/download-refresh.ts @@ -20,7 +20,8 @@ import { CoreAnimations } from '@components/animations'; * Component to show a download button with refresh option, the spinner and the status of it. * * Usage: - * + * + * */ @Component({ selector: 'core-download-refresh', diff --git a/src/core/features/course/pages/index/index.scss b/src/core/features/course/pages/index/index.scss index da91e59c2..bd9456dc2 100644 --- a/src/core/features/course/pages/index/index.scss +++ b/src/core/features/course/pages/index/index.scss @@ -6,6 +6,7 @@ min-height: var(--core-courseimage-on-course-size); width: var(--core-courseimage-on-course-size); min-width: var(--core-courseimage-on-course-size); + --border-radius: var(--core-courseimage-radius); } @if ($core-show-courseimage-on-course) { 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 77d4e330e..625fd2ad1 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,106 +1,92 @@ - - - - - - - - - - - - - - - - - - - - - - -

- -

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

+ + +

+ +

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

+ + + {{ 'core.courses.aria:coursecategory' | 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 47e14a6f0..cabfd5a53 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,67 +1,148 @@ @import "~theme/globals"; - -ion-card.core-course-list-item { - .course-icon { - color: white; - background: var(--gray-200); - padding: 8px; - font-size: 24px; - border-radius: 50%; - -webkit-transition: all 50ms ease-in-out; - transition: all 50ms ease-in-out; - } - +:host { @for $i from 0 to length($core-course-image-background) { - ion-icon[course-color="#{$i}"] { - color: var(--core-course-color-#{$i}); + &.course-color-#{$i} { + --course-color: var(--core-course-color-#{$i}); + --course-color-tint: var(--core-course-color-#{$i}-tint); } } - ion-avatar { - -webkit-transition: all 50ms ease-in-out; - transition: all 50ms ease-in-out; - } + --avatar-radius: var(--core-courseimage-radius); + --avatar-size: 60px; + --avatar-margin: 16px; - .core-course-thumb { - @include margin(12px, 16px, 12px, null); - align-self: flex-start; - } + --card-vertical-margin: 8px; + --card-radius: 12px; - .core-course-summary { - margin-top: 12px; + --shortname-size: 12px; + + --button-size: 44px; + --button-padding: 10px; + --button-radius: 50%; + --button-background: rgba(255,255,255,0.5); + + @include media-breakpoint-down(md) { + --avatar-size: 48px; + --avatar-margin: 12px; } } + +:host-context(body.dark) { + --button-background: rgba(0, 0, 0, 0.3); +} + +// Common styles. .item-heading ion-icon { margin-right: 4px; color: var(--core-star-color); } -ion-card.core-course-list-card { - --vertical-margin: 12px; - --border-radius: 16px; +button { + z-index: 1; +} + +ion-card { + --border-radius: var(--card-radius); +} + +.core-button-spinner { + margin: 0; + position: absolute; + @include position(8px, 8px, null, null); 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); + align-items: center; - @for $i from 0 to length($core-course-image-background) { - &[course-color="#{$i}"] .core-course-thumb { - background: var(--core-course-color-#{$i}); + ion-spinner { + margin-top: 4px; + } + + ::ng-deep ion-button { + --border-radius: var(--button-radius); + --background: var(--button-background); + --padding-top: var(--button-padding); + --padding-end: var(--button-padding); + --padding-bottom: var(--button-padding); + --padding-start: var(--button-padding); + width: var(--button-size); + height: var(--button-size); + margin: 0; + &::part(native) { + background: var(--background); } } - ion-row { - min-height: var(--a11y-min-target-size); - ion-col .core-button-spinner { - min-width: calc(var(--a11y-min-target-size) + 16px); + core-download-refresh ::ng-deep .core-icon-downloaded { + display: none; + } +} + +.core-course-enrol-icons, .core-icon-downloaded { + @include margin-horizontal(4px, 0px); +} + +.core-course-shortname { + font-size: var(--shortname-size); + color: var(--dark); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +ion-chip { + margin-top: 8px; + margin-left: 0; + margin-right: 0; + max-width: 100%; +} + + +// List layout. +ion-card.core-course-list-item { + ion-icon.course-icon { + padding: 12px; + font-size: calc(var(--avatar-size) - 24px); + border-radius: var(--avatar-radius); + color: var(--course-color-tint); + background: var(--course-color, transparent); + } + + ion-avatar { + --border-radius: var(--avatar-radius); + width: var(--avatar-size); + height: var(--avatar-size); + img { + background: transparent; } } + ion-item::part(native) { + --padding-start: var(--avatar-margin); + } + .core-course-thumb { + @include margin(var(--avatar-margin), var(--avatar-margin), var(--avatar-margin), null); + } + + .core-course-maininfo { + margin-right: 32px; + } +} + + +// Card layout. +ion-card.core-course-list-card { + display: flex; + flex-direction: column; + + height: calc(100% - var(--card-vertical-margin) - var(--card-vertical-margin)); + margin-top: var(--card-vertical-margin); + margin-bottom: var(--card-vertical-margin); + + .core-course-thumb { + background: var(--course-color, white); padding-top: 40%; width: 100%; overflow: hidden; @@ -70,8 +151,20 @@ ion-card.core-course-list-card { position: relative; background-position: center; background-size: cover; - -webkit-transition: all 50ms ease-in-out; - transition: all 50ms ease-in-out; + + ion-icon.course-icon { + color: white; + opacity: 50%; + position: absolute; + left: 0; + right: 0; + top: 50%; + bottom: 0; + width: 80px; + height: 80px; + margin: 0 auto; + transform: translateY(-50%); + } &.core-course-color-img { background: var(--ion-item-background); @@ -87,6 +180,57 @@ ion-card.core-course-list-card { } } + .core-course-shortname { + margin-bottom: 8px; + } + + ion-item { + flex-grow: 1; + display: flex; + flex-direction: column; + + &::part(native) { + flex-grow: 1; + align-items: self-start; + } + + > ion-label{ + margin: 16px 0; + + display: flex; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + flex-grow: 1; + + // Clamp one line with ellipsis on tablet view, and 2 in mobile. + .item-heading { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + @include media-breakpoint-down(md) { + .item-heading { + // Addition lines for 2 line or multiline ellipsis + display: -webkit-box !important; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + white-space: normal; + } + } + + .core-course-maininfo { + flex-grow: 1; + max-width: 100%; + } + + .core-course-progress { + align-self: stretch; + } + } + } + @if ($core-course-hide-thumb-on-cards) { .core-course-thumb { display: none; @@ -99,56 +243,6 @@ ion-card.core-course-list-card { } } - .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; @@ -156,10 +250,6 @@ ion-card.core-course-list-card { } } -button { - z-index: 1; -} - :host-context(.core-horizontal-scroll) { @include horizontal_scroll_item(80%, 250px, 300px); @@ -168,31 +258,21 @@ button { padding-top: 30%; } - ion-item.core-course-header { - --padding-start: 4px; + ion-item { + --padding-start: 8px; + --inner-padding-end: 8px; - .core-course-title { - margin: 7px 0; + > ion-label { + margin: 8px 0; + + .item-heading { + display: block; + } .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 5a5f5fa24..46a7defb0 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 @@ -13,7 +13,7 @@ // limitations under the License. import { CoreConstants } from '@/core/constants'; -import { Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { Component, ElementRef, 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'; @@ -21,6 +21,7 @@ import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { Translate } from '@singletons'; +import { CoreColors } from '@singletons/colors'; import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses'; import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; @@ -64,11 +65,17 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On protected courseStatusObserver?: CoreEventObserver; protected siteUpdatedObserver?: CoreEventObserver; + protected element: HTMLElement; + + constructor(element: ElementRef) { + this.element = element.nativeElement; + } + /** * @inheritdoc */ async ngOnInit(): Promise { - CoreCoursesHelper.loadCourseColorAndImage(this.course); + this.setCourseColor(); // Assume is enroled if mode is not listwithenrol. this.isEnrolled = this.layout != 'listwithenrol' || this.course.progress !== undefined; @@ -142,6 +149,22 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On } } + /** + * Set course color. + */ + protected async setCourseColor(): Promise { + await CoreCoursesHelper.loadCourseColorAndImage(this.course); + + if (this.course.color) { + this.element.style.setProperty('--course-color', this.course.color); + + const tint = CoreColors.lighter(this.course.color, 50); + this.element.style.setProperty('--course-color-tint', tint); + } else { + this.element.classList.add('course-color-' + this.course.colorNumber); + } + } + /** * @inheritdoc */ 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 index 7adb8fcc3..d89c865da 100644 --- 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 @@ -33,8 +33,8 @@
+ [statusTranslatable]="prefetchCourseData.statusTranslatable" [canTrustDownload]="false" + [loading]="prefetchCourseData.loading" (action)="prefetchCourse()">
diff --git a/src/core/services/app.ts b/src/core/services/app.ts index 8edb6bd62..68d84392d 100644 --- a/src/core/services/app.ts +++ b/src/core/services/app.ts @@ -647,7 +647,7 @@ export class CoreAppProvider { if (this.isAndroid()) { const rgb = CoreColors.hexToRGB(color); if (rgb.red != 255 || rgb.green != 255 || rgb.blue != 255) { - color = CoreColors.darker(color); + color = CoreColors.darker(color, 10); } } diff --git a/src/core/singletons/colors.ts b/src/core/singletons/colors.ts index 40c2cdffe..b86c8b500 100644 --- a/src/core/singletons/colors.ts +++ b/src/core/singletons/colors.ts @@ -25,10 +25,15 @@ interface ColorComponents { * Ionic color names. */ export enum CoreIonicColorNames { + BRAND = 'brand', + PRIMARY = 'primary', + SECONDARY = 'secondary', SUCCESS = 'success', - INFO = 'info', + WARNING = 'warning', DANGER = 'danger', + INFO = 'info', DARK = 'dark', + MEDIUM = 'medium', LIGHT = 'light', NONE = '', }; @@ -49,17 +54,35 @@ export class CoreColors { } /** - * Returns the same color 10% darker to be used as status bar on Android. + * Returns the same color % darker. * * @param color Color to get darker. * @return Darker Hex RGB color. */ - static darker(color: string, percent: number = 10): string { - percent = 1 - (percent / 100); + static darker(color: string, percent: number = 48): string { + const inversePercent = 1 - (percent / 100); const components = CoreColors.hexToRGB(color); - components.red = Math.floor(components.red * percent); - components.green = Math.floor(components.green * percent); - components.blue = Math.floor(components.blue * percent); + components.red = Math.floor(components.red * inversePercent); + components.green = Math.floor(components.green * inversePercent); + components.blue = Math.floor(components.blue * inversePercent); + + return CoreColors.RGBToHex(components); + } + + /** + * Returns the same color % lighter. + * + * @param color Color to get lighter. + * @return Lighter Hex RGB color. + */ + static lighter(color: string, percent: number = 80): string { + percent = percent / 100; + const inversePercent = 1 - percent; + + const components = CoreColors.hexToRGB(color); + components.red = Math.floor(255 * percent + components.red * inversePercent); + components.green = Math.floor(255 * percent + components.green * inversePercent); + components.blue = Math.floor(255 * percent + components.blue * inversePercent); return CoreColors.RGBToHex(components); } diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index b0086039e..8ffe17e5d 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -31,8 +31,8 @@ core-format-text { opacity: 1; background-color: rgba(0,0,0,.1); overflow: hidden; - border-radius: 5px; display: block; + border-radius: var(--small-radius); .core-format-text-loader { position: absolute; @@ -179,7 +179,7 @@ core-format-text { position: absolute; @include position(null, 10px, 10px, null); color: var(--ion-text-color); - border-radius: 5px; + border-radius: var(--small-radius); background-color: var(--core-format-text-viewer-icon-background); display: flex; diff --git a/src/theme/globals.mixins.scss b/src/theme/globals.mixins.scss index d62c8d2c8..0082bb1e5 100644 --- a/src/theme/globals.mixins.scss +++ b/src/theme/globals.mixins.scss @@ -220,13 +220,6 @@ } } - .ion-text-wrap ion-label { - .item-heading, h2, p { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } } diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 71386568d..e6027e705 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -174,13 +174,13 @@ ion-app.ios ion-header ion-title { text-overflow: ellipsis; } -.item.ion-text-wrap ion-label core-format-text .core-format-text-content > *, +.item.ion-text-wrap > ion-label core-format-text .core-format-text-content > *, .fake-ion-item.ion-text-wrap core-format-text .core-format-text-content > * { white-space: normal; overflow: inherit; } -.item.ion-text-wrap ion-label { +.item.ion-text-wrap > ion-label { white-space: normal !important; } @@ -301,6 +301,10 @@ button, min-width: var(--a11y-min-target-size); } +ion-button.button.button-clear.button-has-icon-only { + --border-radius: 50%; +} + // Clear buttons will be black. ion-button.button-clear { --primary: var(--primary); @@ -848,7 +852,14 @@ ion-select-popover ion-item.core-select-option-title { ion-chip { line-height: 1.1; - @include padding-horizontal(16px); + font-size: 12px; + padding: 4px 8px; + height: 24px; + + ion-icon { + font-size: 16px; + @include margin(0, 8px, 0, 0); + } &.ion-color { background: var(--ion-color-tint); diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index 77c211e36..7eaa2bb89 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -271,6 +271,7 @@ --core-send-message-input-color: var(--gray-900); --core-courseimage-on-course-size: 72px; + --core-courseimage-radius: var(--medium-radius); --core-course-module-navigation-max-height: 56px; --core-course-module-navigation-background: var(--contrast-background); @@ -322,6 +323,7 @@ @for $i from 0 to length($core-course-image-background) { --core-course-color-#{$i}: #{nth($core-course-image-background, $i + 1)}; + --core-course-color-#{$i}-tint: #{get-color-tint(nth($core-course-image-background, $i + 1))}; } @for $i from 0 to length($core-dd-question-colors) {