From 1dd5eba1de1b0416c2758087ffc75eff163aee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 24 Jan 2022 17:46:31 +0100 Subject: [PATCH 1/7] MOBILE-3915 course: Improve course summary info --- .../components/format/core-course-format.html | 96 +++++++++++-------- .../course/components/format/format.scss | 19 +--- .../course/components/format/format.ts | 40 ++++++-- src/theme/theme.light.scss | 2 +- 4 files changed, 88 insertions(+), 69 deletions(-) diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 7e549a3c2..81b7ad122 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -9,6 +9,53 @@ + + + + + + + + +

+ + +

+

{{ course.displayname || course.fullname }}

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

+ + +

+ + {{ 'core.course.hiddenfromstudents' | translate }} + + + {{ 'core.notavailable' | translate }} + + + + + +
+
+
+
+ [contextInstanceId]="course.id" [clean]="true" [singleLine]="true"> {{ 'core.course.sections' | translate }} @@ -27,39 +74,6 @@
- - - -
- -
- - - - - - - - {{ 'core.course.hiddenfromstudents' | translate }} - - - {{ 'core.notavailable' | translate }} - - - - - - - -
-
-
@@ -88,12 +102,12 @@ - + - + @@ -112,7 +126,7 @@ [class.item-dimmed]="section.visible === 0 || section.uservisible === false">

- +

@@ -123,7 +137,7 @@ {{ 'core.notavailable' | translate }} - +

@@ -132,15 +146,15 @@ - + + (completionChanged)="onCompletionChange($event)" [showActivityDates]="course.showactivitydates" + [showCompletionConditions]="course.showcompletionconditions"> diff --git a/src/core/features/course/components/format/format.scss b/src/core/features/course/components/format/format.scss index e7d109381..25f43001b 100644 --- a/src/core/features/course/components/format/format.scss +++ b/src/core/features/course/components/format/format.scss @@ -16,23 +16,8 @@ } .core-course-thumb { - display: none; - height: var(--core-courseimage-on-course-height); - width: 100%; - overflow: hidden; - cursor: pointer; - pointer-events: auto; - position: relative; - background: var(--ion-item-background); - border-bottom: 1px solid var(--stroke); - - img { - position: absolute; - top: 0; - bottom: 0; - margin: auto; - width: 100%; - } + height: var(--core-courseimage-on-course-size); + width: var(--core-courseimage-on-course-size); } @if ($core-show-courseimage-on-course) { diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index 7d93a2084..c4121074f 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -39,6 +39,7 @@ import { CoreCourseModuleData, CoreCourseModuleCompletionData, CoreCourseSection, + CoreCourseSectionWithStatus, } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -46,6 +47,7 @@ import { IonContent, IonRefresher } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector'; import { CoreBlockHelper } from '@features/block/services/block-helper'; +import { CoreNavigator } from '@services/navigator'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -66,7 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called. - @Input() course?: CoreCourseAnyCourseData; // The course to render. + @Input() course!: CoreCourseAnyCourseData; // The course to render. @Input() sections?: CoreCourseSection[]; // List of course sections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @@ -116,9 +118,17 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } /** - * Component being initialized. + * @inheritdoc */ ngOnInit(): void { + if (this.course === undefined) { + CoreDomUtils.showErrorModal('Course not set'); + + CoreNavigator.back(); + + return; + } + // Listen for select course tab events to select the right section if needed. this.selectTabObserver = CoreEvents.on(CoreEvents.SELECT_COURSE_TAB, (data) => { if (data.name) { @@ -207,7 +217,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @return Promise resolved when done. */ protected async loadCourseFormatComponent(): Promise { - this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course!); + this.courseFormatComponent = await CoreCourseFormatDelegate.getCourseFormatComponent(this.course); } /** @@ -216,7 +226,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @return Promise resolved when done. */ protected async loadCourseSummaryComponent(): Promise { - this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course!); + this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course); } /** @@ -225,7 +235,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @return Promise resolved when done. */ protected async loadSectionSelectorComponent(): Promise { - this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course!); + this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course); } /** @@ -234,7 +244,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @return Promise resolved when done. */ protected async loadSingleSectionComponent(): Promise { - this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course!); + this.singleSectionComponent = await CoreCourseFormatDelegate.getSingleSectionComponent(this.course); } /** @@ -243,7 +253,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * @return Promise resolved when done. */ protected async loadAllSectionsComponent(): Promise { - this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course!); + this.allSectionsComponent = await CoreCourseFormatDelegate.getAllSectionsComponent(this.course); } /** @@ -262,7 +272,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (!newSection) { // Section not found, calculate which one to use. - newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections); + newSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections); } this.sectionChanged(newSection); @@ -289,7 +299,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (!this.loaded) { // No section specified, not found or not visible, get current section. - const section = await CoreCourseFormatDelegate.getCurrentSection(this.course!, sections); + const section = await CoreCourseFormatDelegate.getCurrentSection(this.course, sections); this.loaded = true; this.sectionChanged(section); @@ -368,7 +378,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (!previousValue || previousValue.id != newSection.id) { // First load or section changed, add log in Moodle. CoreUtils.ignoreErrors( - CoreCourse.logView(this.course!.id, newSection.section, undefined, this.course!.fullname), + CoreCourse.logView(this.course.id, newSection.section, undefined, this.course.fullname), ); } @@ -558,4 +568,14 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.progress = this.course.progress; } + /** + * Open the course summary + */ + openCourseSummary(): void { + CoreNavigator.navigateToSitePath( + '/course/' + this.course.id + '/preview', + { params: { course: this.course, avoidOpenCourse: true } }, + ); + } + } diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index ff855bb82..ee6036013 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -301,7 +301,7 @@ --core-send-message-input-background: var(--gray-200); --core-send-message-input-color: var(--gray-900); - --core-courseimage-on-course-height: 150px; + --core-courseimage-on-course-size: 72px; --core-course-module-navigation-max-height: 56px; --core-course-module-navigation-background: var(--contrast-background); From 86365d260d2c6211a27c6c9e62ae8258fba98a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 17 Nov 2021 11:46:37 +0100 Subject: [PATCH 2/7] MOBILE-3915 course: Implement new course index --- .../course/components/components.module.ts | 6 +- .../course-index.html} | 25 ++++++-- .../course-index.scss} | 0 .../course-index.ts} | 26 +++++--- .../components/format/core-course-format.html | 62 +++++-------------- .../course/components/format/format.ts | 52 +++++----------- .../module-completion-legacy.ts | 46 +++++++------- src/core/features/course/lang.json | 2 +- src/core/features/course/services/course.ts | 2 +- .../course/services/format-delegate.ts | 23 ------- 10 files changed, 95 insertions(+), 149 deletions(-) rename src/core/features/course/components/{section-selector/section-selector.html => course-index/course-index.html} (61%) rename src/core/features/course/components/{section-selector/section-selector.scss => course-index/course-index.scss} (100%) rename src/core/features/course/components/{section-selector/section-selector.ts => course-index/course-index.ts} (77%) diff --git a/src/core/features/course/components/components.module.ts b/src/core/features/course/components/components.module.ts index 516a43111..739dc216e 100644 --- a/src/core/features/course/components/components.module.ts +++ b/src/core/features/course/components/components.module.ts @@ -20,7 +20,7 @@ import { CoreCourseFormatComponent } from './format/format'; import { CoreCourseModuleComponent } from './module/module'; import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion'; import { CoreCourseModuleDescriptionComponent } from './module-description/module-description'; -import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector'; +import { CoreCourseCourseIndexComponent } from './course-index/course-index'; import { CoreCourseTagAreaComponent } from './tag-area/tag-area'; import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module'; import { CoreCourseModuleCompletionLegacyComponent } from './module-completion-legacy/module-completion-legacy'; @@ -37,7 +37,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- CoreCourseModuleDescriptionComponent, CoreCourseModuleInfoComponent, CoreCourseModuleManualCompletionComponent, - CoreCourseSectionSelectorComponent, + CoreCourseCourseIndexComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, CoreCourseModuleNavigationComponent, @@ -54,7 +54,7 @@ import { CoreCourseModuleNavigationComponent } from './module-navigation/module- CoreCourseModuleDescriptionComponent, CoreCourseModuleInfoComponent, CoreCourseModuleManualCompletionComponent, - CoreCourseSectionSelectorComponent, + CoreCourseCourseIndexComponent, CoreCourseTagAreaComponent, CoreCourseUnsupportedModuleComponent, CoreCourseModuleNavigationComponent, diff --git a/src/core/features/course/components/section-selector/section-selector.html b/src/core/features/course/components/course-index/course-index.html similarity index 61% rename from src/core/features/course/components/section-selector/section-selector.html rename to src/core/features/course/components/course-index/course-index.html index 82b2bf8b0..242cf97c4 100644 --- a/src/core/features/course/components/section-selector/section-selector.html +++ b/src/core/features/course/components/course-index/course-index.html @@ -1,7 +1,7 @@ -

{{ 'core.course.sections' | translate }}

+

{{ 'core.course.courseindex' | translate }}

@@ -13,10 +13,10 @@ - + [attr.aria-hidden]="section.uservisible === false" button sticky="true"> @@ -39,7 +39,24 @@ - + + + + + + + + + + +

+ + +

+
+
+
diff --git a/src/core/features/course/components/section-selector/section-selector.scss b/src/core/features/course/components/course-index/course-index.scss similarity index 100% rename from src/core/features/course/components/section-selector/section-selector.scss rename to src/core/features/course/components/course-index/course-index.scss diff --git a/src/core/features/course/components/section-selector/section-selector.ts b/src/core/features/course/components/course-index/course-index.ts similarity index 77% rename from src/core/features/course/components/section-selector/section-selector.ts rename to src/core/features/course/components/course-index/course-index.ts index e71790478..3b51cad30 100644 --- a/src/core/features/course/components/section-selector/section-selector.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -14,7 +14,7 @@ import { Component, Input, OnInit } from '@angular/core'; -import { CoreCourseSection } from '@features/course/services/course-helper'; +import { CoreCourseModuleData, CoreCourseSection, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking, @@ -25,14 +25,14 @@ import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; /** - * Component to display course section selector in a modal. + * Component to display course index modal. */ @Component({ - selector: 'core-course-section-selector', - templateUrl: 'section-selector.html', - styleUrls: ['section-selector.scss'], + selector: 'core-course-course-index', + templateUrl: 'course-index.html', + styleUrls: ['course-index.scss'], }) -export class CoreCourseSectionSelectorComponent implements OnInit { +export class CoreCourseCourseIndexComponent implements OnInit { @Input() sections?: SectionWithProgress[]; @Input() selected?: CoreCourseSection; @@ -41,7 +41,7 @@ export class CoreCourseSectionSelectorComponent implements OnInit { stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; /** - * Component being initialized. + * @inheritdoc */ ngOnInit(): void { @@ -52,7 +52,7 @@ export class CoreCourseSectionSelectorComponent implements OnInit { const formatOptions = CoreUtils.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value'); - if (!formatOptions || formatOptions.coursedisplay != 1 || formatOptions.completionusertracked === false) { + if (!formatOptions || formatOptions.completionusertracked === false) { return; } @@ -60,11 +60,16 @@ export class CoreCourseSectionSelectorComponent implements OnInit { let complete = 0; let total = 0; section.modules.forEach((module) => { + console.error(module); if (!module.uservisible || module.completiondata === undefined || module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE) { + module.completionStatus = undefined; + return; } + module.completionStatus = module.completiondata.state; + total++; if (module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE || module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { @@ -98,6 +103,9 @@ export class CoreCourseSectionSelectorComponent implements OnInit { } -type SectionWithProgress = CoreCourseSection & { +type SectionWithProgress = Omit & { progress?: number; + modules: (CoreCourseModuleData & { + completionStatus?: CoreCourseModuleCompletionStatus; + })[]; }; diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 81b7ad122..9ecb91ece 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -1,8 +1,8 @@ - + @@ -32,46 +32,6 @@
- - - -

- - -

- - {{ 'core.course.hiddenfromstudents' | translate }} - - - {{ 'core.notavailable' | translate }} - - - - - -
-
-
- - - -
- - - - - {{ 'core.course.sections' | translate }} - - -
@@ -98,7 +58,7 @@
+ *ngIf="displayCourseIndex && sections?.length"> @@ -115,17 +75,25 @@ +
+ + + + + {{'core.course.courseindex' | translate }} + + +
- - + + -

+

diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index c4121074f..e7efb6bc4 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -26,7 +26,6 @@ import { Type, ElementRef, } from '@angular/core'; -import { ModalOptions } from '@ionic/core'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; @@ -45,7 +44,7 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { IonContent, IonRefresher } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; -import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector'; +import { CoreCourseCourseIndexComponent } from '../course-index/course-index'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreNavigator } from '@services/navigator'; @@ -80,7 +79,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // All the possible component classes. courseFormatComponent?: Type; courseSummaryComponent?: Type; - sectionSelectorComponent?: Type; singleSectionComponent?: Type; allSectionsComponent?: Type; @@ -88,7 +86,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { showSectionId = 0; data: Record = {}; // Data to pass to the components. - displaySectionSelector = false; + displayCourseIndex = false; displayBlocks = false; hasBlocks = false; selectedSection?: CoreCourseSection; @@ -97,17 +95,11 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; loaded = false; - hasSeveralSections?: boolean; imageThumb?: string; progress?: number; - sectionSelectorModalOptions: ModalOptions = { - component: CoreCourseSectionSelectorComponent, - componentProps: {}, - }; protected selectTabObserver?: CoreEventObserver; protected lastCourseFormat?: string; - protected sectionSelectorExpanded = false; constructor( protected content: IonContent, @@ -154,14 +146,12 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { */ async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise { this.setInputData(); - this.sectionSelectorModalOptions.componentProps!.course = this.course; - this.sectionSelectorModalOptions.componentProps!.sections = this.sections; if (changes.course && this.course) { // Course has changed, try to get the components. this.getComponents(); - this.displaySectionSelector = CoreCourseFormatDelegate.displaySectionSelector(this.course); + this.displayCourseIndex = CoreCourseFormatDelegate.displaySectionSelector(this.course); this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); @@ -174,7 +164,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } if (changes.sections && this.sections) { - this.sectionSelectorModalOptions.componentProps!.sections = this.sections; this.treatSections(this.sections); } } @@ -205,7 +194,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { await Promise.all([ this.loadCourseFormatComponent(), this.loadCourseSummaryComponent(), - this.loadSectionSelectorComponent(), this.loadSingleSectionComponent(), this.loadAllSectionsComponent(), ]); @@ -229,15 +217,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.courseSummaryComponent = await CoreCourseFormatDelegate.getCourseSummaryComponent(this.course); } - /** - * Load section selector component. - * - * @return Promise resolved when done. - */ - protected async loadSectionSelectorComponent(): Promise { - this.sectionSelectorComponent = await CoreCourseFormatDelegate.getSectionSelectorComponent(this.course); - } - /** * Load single section component. * @@ -264,7 +243,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { */ protected async treatSections(sections: CoreCourseSection[]): Promise { const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID; - this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); + const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); if (this.selectedSection) { // We have a selected section, but the list has changed. Search the section in the list. @@ -281,7 +260,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } // There is no selected section yet, calculate which one to load. - if (!this.hasSeveralSections) { + if (!hasSeveralSections) { // Always load "All sections" to display the section title. If it isn't there just load the section. this.loaded = true; this.sectionChanged(sections[0]); @@ -309,18 +288,18 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } /** - * Display the section selector modal. + * Display the course index modal. */ - async showSectionSelector(): Promise { - if (this.sectionSelectorExpanded) { - return; - } + async openCourseIndex(): Promise { + const data = await CoreDomUtils.openModal({ + component: CoreCourseCourseIndexComponent, + componentProps: { + course: this.course, + sections: this.sections, + selected: this.selectedSection, + }, + }); - this.sectionSelectorExpanded = true; - - const data = await CoreDomUtils.openModal(this.sectionSelectorModalOptions); - - this.sectionSelectorExpanded = false; if (data) { this.sectionChanged(data); } @@ -334,7 +313,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { sectionChanged(newSection: CoreCourseSection): void { const previousValue = this.selectedSection; this.selectedSection = newSection; - this.sectionSelectorModalOptions.componentProps!.selected = this.selectedSection; this.data.section = this.selectedSection; if (newSection.id != this.allSectionsId) { diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts index 1b9e0b453..6bf6d2414 100644 --- a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts +++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts @@ -52,30 +52,28 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC let langKey: string | undefined; let image: string | undefined; - if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { - image = 'completion-manual-n'; - langKey = 'core.completion-alt-manual-n'; - } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { - image = 'completion-manual-y'; - langKey = 'core.completion-alt-manual-y'; - } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { - image = 'completion-auto-n'; - langKey = 'core.completion-alt-auto-n'; - } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { - image = 'completion-auto-y'; - langKey = 'core.completion-alt-auto-y'; - } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { - image = 'completion-auto-pass'; - langKey = 'core.completion-alt-auto-pass'; - } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC && - this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { - image = 'completion-auto-fail'; - langKey = 'core.completion-alt-auto-fail'; + if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_MANUAL) { + if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { + image = 'completion-manual-n'; + langKey = 'core.completion-alt-manual-n'; + } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { + image = 'completion-manual-y'; + langKey = 'core.completion-alt-manual-y'; + } + } else if (this.completion.tracking === CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_AUTOMATIC) { + if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_INCOMPLETE) { + image = 'completion-auto-n'; + langKey = 'core.completion-alt-auto-n'; + } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { + image = 'completion-auto-y'; + langKey = 'core.completion-alt-auto-y'; + } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { + image = 'completion-auto-pass'; + langKey = 'core.completion-alt-auto-pass'; + } else if (this.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_FAIL) { + image = 'completion-auto-fail'; + langKey = 'core.completion-alt-auto-fail'; + } } if (image) { diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index f6f144b2d..d83be79fa 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -26,6 +26,7 @@ "confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?", "confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", + "courseindex": "Course index", "gotonextactivity": "Continue to next activity", "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.", "gotopreviousactivity": "Continue to previous activity", @@ -49,7 +50,6 @@ "overriddennotice": "Your final grade from this activity was manually adjusted.", "refreshcourse": "Refresh course", "section": "Section", - "sections": "Sections", "useactivityonbrowser": "You can still use it using your device's web browser.", "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 24984dfc3..170cbbef5 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -62,7 +62,7 @@ declare module '@singletons/events' { } /** - * Completion status valid values. + * Course Module completion status enumeration. */ export enum CoreCourseModuleCompletionStatus { COMPLETION_INCOMPLETE = 0, diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts index 295691a0e..dc2efc50b 100644 --- a/src/core/features/course/services/format-delegate.ts +++ b/src/core/features/course/services/format-delegate.ts @@ -125,15 +125,6 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { */ getCourseSummaryComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; - /** - * Return the Component to use to display the section selector inside the default course format. - * It's recommended to return the class of the component, but you can also return an instance of the component. - * - * @param course The course to render. - * @return Promise resolved with component to use, undefined if not found. - */ - getSectionSelectorComponent?(course: CoreCourseAnyCourseData): Promise | undefined>; - /** * Return the Component to use to display a single section. This component will only be used if the user is viewing a * single section. If all the sections are displayed at once then it won't be used. @@ -302,20 +293,6 @@ export class CoreCourseFormatDelegateService extends CoreDelegate | undefined> { - try { - return await this.executeFunctionOnEnabled>(course.format || '', 'getSectionSelectorComponent', [course]); - } catch (error) { - this.logger.error('Error getting section selector component', error); - } - } - /** * Get the component to use to display a single section. This component will only be used if the user is viewing * a single section. If all the sections are displayed at once then it won't be used. From 901a4454080b46e40a37694571e69b16c61782c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 24 Jan 2022 11:19:34 +0100 Subject: [PATCH 3/7] MOBILE-3915 course: Move Course summary info outside the tabs --- .../components/tabs-outlet/tabs-outlet.ts | 23 ++- src/core/directives/collapsible-header.ts | 171 +++++++++++------- .../side-blocks-button.scss | 1 + .../components/format/core-course-format.html | 37 +--- .../course/components/format/format.scss | 60 +----- .../course/components/format/format.ts | 95 +++++----- .../module-completion-legacy.ts | 40 +++- .../module-manual-completion.ts | 11 +- .../course/pages/contents/contents.html | 3 +- .../features/course/pages/index/index.html | 31 +++- .../course/pages/index/index.module.ts | 2 +- .../features/course/pages/index/index.scss | 26 +++ .../pages/index/{index.page.ts => index.ts} | 109 ++++++++--- src/core/features/course/services/course.ts | 5 + .../course/services/format-delegate.ts | 4 +- src/core/singletons/events.ts | 14 ++ src/theme/theme.base.scss | 27 ++- 17 files changed, 405 insertions(+), 254 deletions(-) create mode 100644 src/core/features/course/pages/index/index.scss rename src/core/features/course/pages/index/{index.page.ts => index.ts} (69%) diff --git a/src/core/components/tabs-outlet/tabs-outlet.ts b/src/core/components/tabs-outlet/tabs-outlet.ts index 54da61022..d7575f2fe 100644 --- a/src/core/components/tabs-outlet/tabs-outlet.ts +++ b/src/core/components/tabs-outlet/tabs-outlet.ts @@ -62,7 +62,7 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { + this.stackEventsSubscription = this.ionTabs.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => { if (!this.isCurrentView) { return; } + // Add tabid to the tab content element. + if (stackEvent.enteringView.element.id == '') { + const tab = this.tabs.find((tab) => tab.page == stackEvent.enteringView.url); + stackEvent.enteringView.element.id = tab?.id || ''; + } + this.showHideNavBarButtons(stackEvent.enteringView.element.tagName); await this.listenContentScroll(stackEvent.enteringView.element, stackEvent.enteringView.id); @@ -111,8 +117,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { - this.lastActiveComponent = this.ionTabs?.outlet.component; + this.outletActivatedSubscription = this.ionTabs.outlet.activateEvents.subscribe(() => { + this.lastActiveComponent = this.ionTabs.outlet.component; }); } @@ -140,8 +146,8 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent).ionViewDidEnter?.(); + if (this.existsInNavigationStack && this.ionTabs.outlet.isActivated) { + (this.ionTabs.outlet.component as Partial).ionViewDidEnter?.(); } // After the view has entered for the first time, we can assume that it'll always be in the navigation stack @@ -180,10 +186,9 @@ export class CoreTabsOutletComponent extends CoreTabsBaseComponent { - const instance = domUtils.getInstanceByElement(element); + const instance = CoreDomUtils.getInstanceByElement(element); if (instance) { const pagetagName = element.closest('.ion-page')?.tagName; diff --git a/src/core/directives/collapsible-header.ts b/src/core/directives/collapsible-header.ts index 24ba81a03..e0927de10 100644 --- a/src/core/directives/collapsible-header.ts +++ b/src/core/directives/collapsible-header.ts @@ -43,6 +43,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { protected headerSubHeadingFontSize = 0; protected contentSubHeadingFontSize = 0; protected subHeadingStartDifference = 0; + protected inContent = true; + protected title?: HTMLElement | null; + protected titleHeight = 0; + protected contentH1?: HTMLElement | null; constructor(el: ElementRef) { this.header = el.nativeElement; @@ -67,8 +71,6 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { /** * Gets the loading content id to wait for the loading to finish. * - * @TODO: If no core-loading is present, load directly. Take into account content needs to be initialized. - * * @return Promise resolved with Loading Id, if any. */ protected async getLoadingId(): Promise { @@ -80,6 +82,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { return; } + + const title = this.header.parentElement?.querySelector('.collapsible-title') || null; + + if (title) { + // Title already found, no need to wait for loading. + this.loadingObserver.off(); + this.setupRealTitle(); + + return; + } + } return this.content.querySelector('core-loading.core-loading-loaded:not(.core-loading-inline) .core-loading-content')?.id; @@ -89,6 +102,7 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { * Call this function when header is not collapsible. */ protected cannotCollapse(): void { + this.content = undefined; this.loadingObserver.off(); this.header.classList.add('core-header-collapsed'); } @@ -112,16 +126,25 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { await animation.finished; })); - const title = this.content.querySelector('.collapsible-title, h1'); - const contentH1 = this.content.querySelector('h1'); + let title = this.content.querySelector('.collapsible-title'); + if (!title) { + // Title is outside the ion-content. + title = this.header.parentElement?.querySelector('.collapsible-title') || null; + this.inContent = false; + } + this.contentH1 = title?.querySelector('h1'); const headerH1 = this.header.querySelector('h1'); - if (!title || !contentH1 || !headerH1) { + if (!title || !this.contentH1 || !headerH1 || !this.contentH1.parentElement) { this.cannotCollapse(); return; } - this.titleTopDifference = contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top; + this.title = title; + this.titleHeight = title.getBoundingClientRect().height; + + this.titleTopDifference = this.contentH1.getBoundingClientRect().top - headerH1.getBoundingClientRect().top; + if (this.titleTopDifference <= 0) { this.cannotCollapse(); @@ -141,17 +164,17 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { } const headerH1Styles = getComputedStyle(headerH1); - const contentH1Styles = getComputedStyle(contentH1); + const contentH1Styles = getComputedStyle(this.contentH1); if (Platform.isRTL) { // Checking position over parent because transition may not be finished. - const contentH1Position = contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right; + const contentH1Position = this.contentH1.getBoundingClientRect().right - this.content.getBoundingClientRect().right; const headerH1Position = headerH1.getBoundingClientRect().right - this.header.getBoundingClientRect().right; this.h1StartDifference = Math.round(contentH1Position - (headerH1Position - parseFloat(headerH1Styles.paddingRight))); } else { // Checking position over parent because transition may not be finished. - const contentH1Position = contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left; + const contentH1Position = this.contentH1.getBoundingClientRect().left - this.content.getBoundingClientRect().left; const headerH1Position = headerH1.getBoundingClientRect().left - this.header.getBoundingClientRect().left; this.h1StartDifference = Math.round(contentH1Position - (headerH1Position + parseFloat(headerH1Styles.paddingLeft))); @@ -165,10 +188,10 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { if (styleName != 'font-size' && styleName != 'font-family' && (styleName.startsWith('font-') || styleName.startsWith('letter-'))) { - contentH1.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName)); + this.contentH1?.style.setProperty(styleName, headerH1Styles.getPropertyValue(styleName)); } }); - contentH1.style.setProperty( + this.contentH1.style.setProperty( '--max-width', (parseFloat(headerH1Styles.width) -parseFloat(headerH1Styles.paddingLeft) @@ -176,47 +199,70 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { +'px'), ); - contentH1.setAttribute('aria-hidden', 'true'); + this.contentH1.setAttribute('aria-hidden', 'true'); + + // Clone element to let the other elements be static. + const contentH1Clone = this.contentH1.cloneNode(true) as HTMLElement; + contentH1Clone.classList.add('cloned'); + this.contentH1.parentElement.insertBefore(contentH1Clone, this.contentH1); + this.contentH1.style.setProperty( + 'top', + (contentH1Clone.getBoundingClientRect().top - + this.contentH1.parentElement.getBoundingClientRect().top + + parseInt(getComputedStyle(this.contentH1.parentElement).marginTop || '0', 10)) + 'px', + ); + this.contentH1.style.setProperty('position', 'absolute'); + + this.setupContent(); + } + + /** + * Setup content scroll. + * + * @param parentId Parent id to recalculate content + * @param retries Retries to find content in case it's loading. + */ + async setupContent(parentId?: string, retries = 5): Promise { + if (parentId) { + this.content = this.header.parentElement?.querySelector(`#${parentId} ion-content:not(.disable-scroll-y)`); + this.inContent = false; + if (!this.content && retries > 0) { + await CoreUtils.nextTick(); + await this.setupContent(parentId, --retries); + + return; + } + + this.onScroll(this.content?.scrollTop || 0); + } + + if (!this.title || !this.content) { + return; + } // Add something under the hood to change the page background. - let color = getComputedStyle(title).getPropertyValue('backgroundColor').trim(); + let color = getComputedStyle(this.title).getPropertyValue('backgroundColor').trim(); if (color == '') { - color = getComputedStyle(title).getPropertyValue('--background').trim(); + color = getComputedStyle(this.title).getPropertyValue('--background').trim(); } const underHeader = document.createElement('div'); underHeader.classList.add('core-underheader'); underHeader.style.setProperty('height', this.header.clientHeight + 'px'); underHeader.style.setProperty('background', color); - this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); - - this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); - - // Subheading. - const headerSubHeading = this.header.querySelector('h2,.subheading'); - const contentSubHeading = title.querySelector('h2,.subheading'); - if (headerSubHeading && contentSubHeading) { - const headerSubHeadingStyles = getComputedStyle(headerSubHeading); - this.headerSubHeadingFontSize = parseFloat(headerSubHeadingStyles.fontSize); - - const contentSubHeadingStyles = getComputedStyle(contentSubHeading); - this.contentSubHeadingFontSize = parseFloat(contentSubHeadingStyles.fontSize); - - if (Platform.isRTL) { - this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().right - - (headerSubHeading.getBoundingClientRect().right - parseFloat(headerSubHeadingStyles.paddingRight)); - } else { - this.subHeadingStartDifference = contentSubHeading.getBoundingClientRect().left - - (headerSubHeading.getBoundingClientRect().left + parseFloat(headerSubHeadingStyles.paddingLeft)); + if (this.inContent) { + this.content.shadowRoot?.querySelector('#background-content')?.prepend(underHeader); + this.content.style.setProperty('--offset-top', this.header.clientHeight + 'px'); + } else { + if (!this.header.closest('.ion-page')?.querySelector('.core-underheader')) { + this.header.closest('.ion-page')?.insertBefore(underHeader, this.header); } - - contentSubHeading.setAttribute('aria-hidden', 'true'); } this.content.scrollEvents = true; this.content.addEventListener('ionScroll', (e: CustomEvent): void => { if (e.target == this.content) { - this.onScroll(title, contentH1, contentSubHeading, e.detail); + this.onScroll(e.detail.scrollTop); } }); } @@ -224,54 +270,45 @@ export class CoreCollapsibleHeaderDirective implements OnDestroy { /** * On scroll function. * - * @param title Title on ion content. - * @param contentH1 Heading 1 of title, if found. - * @param scrollDetail Event details. + * @param scrollTop Scroll top measure. */ protected onScroll( - title: HTMLElement, - contentH1: HTMLElement, - contentSubheading: HTMLElement | null, - scrollDetail: ScrollDetail, + scrollTop: number, ): void { - const progress = CoreMath.clamp(scrollDetail.scrollTop / this.titleTopDifference, 0, 1); + if (!this.title || !this.contentH1) { + return; + } + + const progress = CoreMath.clamp(scrollTop / this.titleTopDifference, 0, 1); const collapsed = progress >= 1; + if (!this.inContent) { + this.title.style.transform = 'translateY(-' + scrollTop + 'px)'; + const height = this.titleHeight - scrollTop; + this.title.style.height = (height > 0 ? height : 0) + 'px'; + } + // Check total collapse. this.header.classList.toggle('core-header-collapsed', collapsed); - title.classList.toggle('collapsible-title-collapsed', collapsed); - title.classList.toggle('collapsible-title-collapse-started', scrollDetail.scrollTop > 0); - title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); - title.style.setProperty('--collapse-opacity', (1 - progress) +''); + this.title.classList.toggle('collapsible-title-collapsed', collapsed); + this.title.classList.toggle('collapsible-title-collapse-started', scrollTop > 0); + this.title.classList.toggle('collapsible-title-collapse-nowrap', progress > 0.5); + this.title.style.setProperty('--collapse-opacity', (1 - progress) +''); if (collapsed) { - contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; - contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px'); - - if (contentSubheading) { - contentSubheading.style.transform = 'translateX(-' + this.subHeadingStartDifference + 'px)'; - contentSubheading.style.setProperty('font-size', this.headerSubHeadingFontSize + 'px'); - } + this.contentH1.style.transform = 'translateX(-' + this.h1StartDifference + 'px)'; + this.contentH1.style.setProperty('font-size', this.headerH1FontSize + 'px'); return; } // Zoom font-size out. const newFontSize = this.contentH1FontSize - ((this.contentH1FontSize - this.headerH1FontSize) * progress); - contentH1.style.setProperty('font-size', newFontSize + 'px'); + this.contentH1.style.setProperty('font-size', newFontSize + 'px'); // Move. const newStart = - this.h1StartDifference * progress; - contentH1.style.transform = 'translateX(' + newStart + 'px)'; - - if (contentSubheading) { - const newFontSize = this.contentSubHeadingFontSize - - ((this.contentSubHeadingFontSize - this.headerSubHeadingFontSize) * progress); - contentSubheading.style.setProperty('font-size', newFontSize + 'px'); - - const newStart = - this.subHeadingStartDifference * progress; - contentSubheading.style.transform = 'translateX(' + newStart + 'px)'; - } + this.contentH1.style.transform = 'translateX(' + newStart + 'px)'; } /** diff --git a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss index 579196d50..76b104a1a 100644 --- a/src/core/features/block/components/side-blocks-button/side-blocks-button.scss +++ b/src/core/features/block/components/side-blocks-button/side-blocks-button.scss @@ -4,6 +4,7 @@ @include position(50%, 0px, null, null); position: fixed; z-index: 10; + transform: translateY(-50%); ion-button { margin: 0; diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 9ecb91ece..3249dad88 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -10,30 +10,6 @@ - - - - - - - -

- - -

-

{{ course.displayname || course.fullname }}

-
- - -
-
- - - -
-
-
@@ -72,15 +48,15 @@ - - - - + + + + - + {{'core.course.courseindex' | translate }} @@ -121,8 +97,7 @@ + [showActivityDates]="course.showactivitydates" [showCompletionConditions]="course.showcompletionconditions">
diff --git a/src/core/features/course/components/format/format.scss b/src/core/features/course/components/format/format.scss index 25f43001b..f6444fb72 100644 --- a/src/core/features/course/components/format/format.scss +++ b/src/core/features/course/components/format/format.scss @@ -1,55 +1,11 @@ -@import '~theme/globals.scss'; +.core-course-section-nav-buttons { + display: flex; + justify-content: flex-end; -:host { - - .core-format-progress-list { - margin-bottom: 0; - - .item { - background: transparent; - - .label { - margin-top: 0; - margin-bottom: 0; - } - } + core-format-text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + text-transform: none; } - - .core-course-thumb { - height: var(--core-courseimage-on-course-size); - width: var(--core-courseimage-on-course-size); - } - - @if ($core-show-courseimage-on-course) { - .core-course-thumb { - display: block; - } - } - - @if ($core-hide-progress-on-course) { - .core-course-progress { - display: none; - } - } - - - .core-button-selector-row { - display: flex; - core-combobox { - flex-grow: 1; - } - } - - .core-course-section-nav-buttons { - display: flex; - justify-content: flex-end; - - core-format-text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - text-transform: none; - } - } - } diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index e7efb6bc4..1cdc25197 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -68,7 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called. @Input() course!: CoreCourseAnyCourseData; // The course to render. - @Input() sections?: CoreCourseSection[]; // List of course sections. + @Input() sections: CoreCourseSectionWithStatus[] = []; // List of course sections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. @@ -95,10 +95,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID; stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; loaded = false; - imageThumb?: string; progress?: number; protected selectTabObserver?: CoreEventObserver; + protected completionObserver?: CoreEventObserver; protected lastCourseFormat?: string; constructor( @@ -139,6 +139,37 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.sectionChanged(section); } }); + + // The completion of any of the modules have changed. + this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { + if (data.completion.courseId != this.course.id) { + return; + } + + // Emit a new event for other components. + this.completionChanged.emit(data.completion); + + if (data.completion.valueused !== false || !this.course || !('progress' in this.course) || + typeof this.course.progress != 'number') { + return; + } + + // If the completion value is not used, the page won't be reloaded, so update the progress bar. + const completionModules = ( []) + .concat(...this.sections.map((section) => section.modules)) + .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) + .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); + + const moduleProgressPercent = 100 / (completionModules || 1); + // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar. + if (data.completion.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { + this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); + } else { + this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); + } + + this.updateProgress(); + }); } /** @@ -157,10 +188,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); this.updateProgress(); - - if ('overviewfiles' in this.course) { - this.imageThumb = this.course.overviewfiles?.[0]?.fileurl; - } } if (changes.sections && this.sections) { @@ -246,8 +273,9 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { const hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections); if (this.selectedSection) { + const selectedSection = this.selectedSection; // We have a selected section, but the list has changed. Search the section in the list. - let newSection = sections.find(section => this.compareSections(section, this.selectedSection!)); + let newSection = sections.find(section => this.compareSections(section, selectedSection)); if (!newSection) { // Section not found, calculate which one to use. @@ -317,22 +345,22 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { if (newSection.id != this.allSectionsId) { // Select next and previous sections to show the arrows. - const i = this.sections!.findIndex((value) => this.compareSections(value, this.selectedSection!)); + const i = this.sections.findIndex((value) => this.compareSections(value, newSection)); let j: number; for (j = i - 1; j >= 1; j--) { - if (this.canViewSection(this.sections![j])) { + if (this.canViewSection(this.sections[j])) { break; } } - this.previousSection = j >= 1 ? this.sections![j] : undefined; + this.previousSection = j >= 1 ? this.sections[j] : undefined; - for (j = i + 1; j < this.sections!.length; j++) { - if (this.canViewSection(this.sections![j])) { + for (j = i + 1; j < this.sections.length; j++) { + if (this.canViewSection(this.sections[j])) { break; } } - this.nextSection = j < this.sections!.length ? this.sections![j] : undefined; + this.nextSection = j < this.sections.length ? this.sections[j] : undefined; } else { this.previousSection = undefined; this.nextSection = undefined; @@ -463,7 +491,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } /** - * Component destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.selectTabObserver && this.selectTabObserver.off(); @@ -498,35 +526,6 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID; } - /** - * The completion of any of the modules have changed. - */ - onCompletionChange(completionData: CoreCourseModuleCompletionData): void { - // Emit a new event for other components. - this.completionChanged.emit(completionData); - - if (completionData.valueused !== false || !this.course || !('progress' in this.course) || - typeof this.course.progress != 'number') { - return; - } - - // If the completion value is not used, the page won't be reloaded, so update the progress bar. - const completionModules = ( []) - .concat(...this.sections!.map((section) => section.modules)) - .map((module) => module.completion && module.completion > 0 ? 1 : module.completion) - .reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0), 0); - - const moduleProgressPercent = 100 / (completionModules || 1); - // Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar. - if (completionData.state === CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE) { - this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent); - } else { - this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent); - } - - this.updateProgress(); - } - /** * Update course progress. */ @@ -546,14 +545,4 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { this.progress = this.course.progress; } - /** - * Open the course summary - */ - openCourseSummary(): void { - CoreNavigator.navigateToSitePath( - '/course/' + this.course.id + '/preview', - { params: { course: this.course, avoidOpenCourse: true } }, - ); - } - } diff --git a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts index 6bf6d2414..023967422 100644 --- a/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts +++ b/src/core/features/course/components/module-completion-legacy/module-completion-legacy.ts @@ -12,14 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { CoreUser } from '@features/user/services/user'; -import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course'; +import { + CoreCourseCompletionType, + CoreCourseModuleCompletionStatus, + CoreCourseModuleCompletionTracking, +} from '@features/course/services/course'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { Translate } from '@singletons'; import { CoreCourseModuleCompletionBaseComponent } from '@features/course/classes/module-completion'; import { CoreCourseHelper } from '@features/course/services/course-helper'; +import { CoreEventObserver, CoreEvents } from '@singletons/events'; /** * Component to handle activity completion in sites previous to 3.11. @@ -35,11 +40,29 @@ import { CoreCourseHelper } from '@features/course/services/course-helper'; templateUrl: 'core-course-module-completion-legacy.html', styleUrls: ['module-completion-legacy.scss'], }) -export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent { +export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleCompletionBaseComponent + implements OnInit, OnDestroy { completionImage?: string; completionDescription?: string; + protected completionObserver?: CoreEventObserver; + + /** + * @inheritdoc + */ + ngOnInit(): void { + this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { + if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) { + return; + } + + this.completion = data.completion; + this.calculateData(); + this.completionChanged.emit(this.completion); + }); + } + /** * @inheritdoc */ @@ -126,9 +149,16 @@ export class CoreCourseModuleCompletionLegacyComponent extends CoreCourseModuleC await CoreCourseHelper.changeManualCompletion(this.completion, event); - this.calculateData(); + // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead. + CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); + CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL }); + } - this.completionChanged.emit(this.completion); + /** + * @inheritdoc + */ + ngOnDestroy(): void { + this.completionObserver?.off(); } } diff --git a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts index ede613119..926349adf 100644 --- a/src/core/features/course/components/module-manual-completion/module-manual-completion.ts +++ b/src/core/features/course/components/module-manual-completion/module-manual-completion.ts @@ -13,6 +13,7 @@ // limitations under the License. import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; +import { CoreCourseCompletionType } from '@features/course/services/course'; import { CoreCourseHelper, CoreCourseModuleCompletionData } from '@features/course/services/course-helper'; import { CoreUser } from '@features/user/services/user'; @@ -34,14 +35,14 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan accessibleDescription: string | null = null; - protected manualChangedObserver?: CoreEventObserver; + protected completionObserver?: CoreEventObserver; /** * @inheritdoc */ ngOnInit(): void { - this.manualChangedObserver = CoreEvents.on(CoreEvents.MANUAL_COMPLETION_CHANGED, (data) => { - if (!this.completion || this.completion.cmid != data.completion.cmid) { + this.completionObserver = CoreEvents.on(CoreEvents.COMPLETION_CHANGED, (data) => { + if (!this.completion || this.completion.cmid != data.completion.cmid && data.type != CoreCourseCompletionType.MANUAL) { return; } @@ -98,14 +99,16 @@ export class CoreCourseModuleManualCompletionComponent implements OnInit, OnChan await CoreCourseHelper.changeManualCompletion(this.completion, event); + // @deprecated MANUAL_COMPLETION_CHANGED is deprecated since 4.0 use COMPLETION_CHANGED instead. CoreEvents.trigger(CoreEvents.MANUAL_COMPLETION_CHANGED, { completion: this.completion }); + CoreEvents.trigger(CoreEvents.COMPLETION_CHANGED, { completion: this.completion, type: CoreCourseCompletionType.MANUAL }); } /** * @inheritdoc */ ngOnDestroy(): void { - this.manualChangedObserver?.off(); + this.completionObserver?.off(); } } diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html index 676c330b5..88716c7ea 100644 --- a/src/core/features/course/pages/contents/contents.html +++ b/src/core/features/course/pages/contents/contents.html @@ -15,7 +15,8 @@ + [moduleId]="moduleId" (completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}" + *ngIf="dataLoaded"> diff --git a/src/core/features/course/pages/index/index.html b/src/core/features/course/pages/index/index.html index b1776e63f..8ca38de7f 100644 --- a/src/core/features/course/pages/index/index.html +++ b/src/core/features/course/pages/index/index.html @@ -1,4 +1,4 @@ - + @@ -12,4 +12,31 @@ - + + + + + + + +

+ + +

+

{{ title }}

+
+ + + + + +
+
+ + +
+
+ +
+ diff --git a/src/core/features/course/pages/index/index.module.ts b/src/core/features/course/pages/index/index.module.ts index 1e1d26938..314528baa 100644 --- a/src/core/features/course/pages/index/index.module.ts +++ b/src/core/features/course/pages/index/index.module.ts @@ -17,7 +17,7 @@ import { RouterModule, ROUTES, Routes } from '@angular/router'; import { resolveModuleRoutes } from '@/app/app-routing.module'; import { CoreSharedModule } from '@/core/shared.module'; -import { CoreCourseIndexPage } from './index.page'; +import { CoreCourseIndexPage } from '.'; import { COURSE_INDEX_ROUTES } from './index-routing.module'; function buildRoutes(injector: Injector): Routes { diff --git a/src/core/features/course/pages/index/index.scss b/src/core/features/course/pages/index/index.scss new file mode 100644 index 000000000..da91e59c2 --- /dev/null +++ b/src/core/features/course/pages/index/index.scss @@ -0,0 +1,26 @@ +@import '~theme/globals.scss'; + +:host { + .core-course-thumb { + height: var(--core-courseimage-on-course-size); + min-height: var(--core-courseimage-on-course-size); + width: var(--core-courseimage-on-course-size); + min-width: var(--core-courseimage-on-course-size); + } + + @if ($core-show-courseimage-on-course) { + .core-course-thumb { + display: block; + } + } + + @if ($core-hide-progress-on-course) { + .core-course-progress { + display: none; + } + } + + h1 { + font-size: 20px; + } +} diff --git a/src/core/features/course/pages/index/index.page.ts b/src/core/features/course/pages/index/index.ts similarity index 69% rename from src/core/features/course/pages/index/index.page.ts rename to src/core/features/course/pages/index/index.ts index 096de4843..2ecf4abab 100644 --- a/src/core/features/course/pages/index/index.page.ts +++ b/src/core/features/course/pages/index/index.ts @@ -26,6 +26,8 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreNavigator } from '@services/navigator'; import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreCollapsibleHeaderDirective } from '@directives/collapsible-header'; /** * Page that displays the list of courses the user is enrolled in. @@ -33,23 +35,28 @@ import { CONTENTS_PAGE_NAME } from '@features/course/course.module'; @Component({ selector: 'page-core-course-index', templateUrl: 'index.html', + styleUrls: ['index.scss'], }) export class CoreCourseIndexPage implements OnInit, OnDestroy { @ViewChild(CoreTabsOutletComponent) tabsComponent?: CoreTabsOutletComponent; + @ViewChild(CoreCollapsibleHeaderDirective) ionCollapsibleHeader?: CoreCollapsibleHeaderDirective; - title?: string; + title = ''; + category = ''; course?: CoreCourseAnyCourseData; tabs: CourseTab[] = []; loaded = false; + imageThumb?: string; + progress?: number; protected currentPagePath = ''; protected selectTabObserver: CoreEventObserver; protected firstTabName?: string; protected module?: CoreCourseModuleData; protected modParams?: Params; - protected isGuest?: boolean; - protected contentsTab: CoreTabsOutletTab = { + protected isGuest = false; + protected contentsTab: CoreTabsOutletTab & { pageParams: Params } = { page: CONTENTS_PAGE_NAME, title: 'core.course', pageParams: {}, @@ -60,10 +67,10 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { if (!data.name) { // If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet. if (data.sectionId) { - this.contentsTab.pageParams!.sectionId = data.sectionId; + this.contentsTab.pageParams.sectionId = data.sectionId; } if (data.sectionNumber) { - this.contentsTab.pageParams!.sectionNumber = data.sectionNumber; + this.contentsTab.pageParams.sectionNumber = data.sectionNumber; } // Select course contents. @@ -79,7 +86,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { } /** - * Component being initialized. + * @inheritdoc */ async ngOnInit(): Promise { // Increase route depth. @@ -87,12 +94,19 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { CoreNavigator.increaseRouteDepth(path.replace(/(\/deep)+/, '')); - // Get params. - this.course = CoreNavigator.getRouteParam('course'); + try { + this.course = CoreNavigator.getRequiredRouteParam('course'); + } catch (error) { + CoreDomUtils.showErrorModal(error); + CoreNavigator.back(); + + return; + } + this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); this.module = CoreNavigator.getRouteParam('module'); this.modParams = CoreNavigator.getRouteParam('modParams'); - this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest'); + this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); this.currentPagePath = CoreNavigator.getCurrentPath(); this.contentsTab.page = CoreTextUtils.concatenatePaths(this.currentPagePath, this.contentsTab.page); @@ -104,7 +118,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { }; if (this.module) { - this.contentsTab.pageParams!.moduleId = this.module.id; + this.contentsTab.pageParams.moduleId = this.module.id; } this.tabs.push(this.contentsTab); @@ -112,20 +126,23 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { await Promise.all([ this.loadCourseHandlers(), - this.loadTitle(), + this.loadBasinInfo(), ]); } /** * A tab was selected. */ - tabSelected(): void { - if (this.module) { - // Now that the first tab has been selected we can load the module. - CoreCourseHelper.openModule(this.module, this.course!.id, this.contentsTab.pageParams!.sectionId, this.modParams); + tabSelected(tabToSelect: CoreTabsOutletTab): void { + this.ionCollapsibleHeader?.setupContent(tabToSelect.id); - delete this.module; + if (!this.module || !this.course) { + return; } + // Now that the first tab has been selected we can load the module. + CoreCourseHelper.openModule(this.module, this.course.id, this.contentsTab.pageParams.sectionId, this.modParams); + + delete this.module; } /** @@ -134,8 +151,12 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { * @return Promise resolved when done. */ protected async loadCourseHandlers(): Promise { + if (!this.course) { + return; + } + // Load the course handlers. - const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course!, false, this.isGuest); + const handlers = await CoreCourseOptionsDelegate.getHandlersToDisplay(this.course, false, this.isGuest); let tabToLoad: number | undefined; @@ -169,23 +190,34 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { * * @return Promise resolved when done. */ - protected async loadTitle(): Promise { + protected async loadBasinInfo(): Promise { + if (!this.course) { + return; + } + // Get the title to display initially. - this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!); + this.title = CoreCourseFormatDelegate.getCourseTitle(this.course); + this.category = 'categoryname' in this.course ? this.course.categoryname : ''; + + if ('overviewfiles' in this.course) { + this.imageThumb = this.course.overviewfiles?.[0]?.fileurl; + } + + this.updateProgress(); // Load sections. - const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course!.id, false, true)); + const sections = await CoreUtils.ignoreErrors(CoreCourse.getSections(this.course.id, false, true)); if (!sections) { return; } // Get the title again now that we have sections. - this.title = CoreCourseFormatDelegate.getCourseTitle(this.course!, sections); + this.title = CoreCourseFormatDelegate.getCourseTitle(this.course, sections); } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { const path = CoreNavigator.getRouteFullPath(this.route.snapshot); @@ -208,6 +240,39 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { this.tabsComponent?.ionViewDidLeave(); } + /** + * Open the course summary + */ + openCourseSummary(): void { + if (!this.course) { + return; + } + + CoreNavigator.navigateToSitePath( + '/course/' + this.course.id + '/preview', + { params: { course: this.course, avoidOpenCourse: true } }, + ); + } + + /** + * Update course progress. + */ + protected updateProgress(): void { + if ( + !this.course || + !('progress' in this.course) || + typeof this.course.progress !== 'number' || + this.course.progress < 0 || + this.course.completionusertracked === false + ) { + this.progress = undefined; + + return; + } + + this.progress = this.course.progress; + } + } type CourseTab = CoreTabsOutletTab & { diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 170cbbef5..001452497 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -71,6 +71,11 @@ export enum CoreCourseModuleCompletionStatus { COMPLETION_COMPLETE_FAIL = 3, } +export enum CoreCourseCompletionType { + MANUAL = 0, + AUTO = 1, +} + /** * Completion tracking valid values. */ diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts index dc2efc50b..967903d58 100644 --- a/src/core/features/course/services/format-delegate.ts +++ b/src/core/features/course/services/format-delegate.ts @@ -267,8 +267,8 @@ export class CoreCourseFormatDelegateService extends CoreDelegate ion-row { display: block; } +.core-underheader { + position: absolute; + top: 0; + left: 0; + right: 0; +} + ion-header[collapsible] { @include core-transition(all, 500ms); @@ -1247,7 +1254,14 @@ ion-header[collapsible] { .collapsible-title { overflow: visible; - *, h1, h2, .subheading { + --inner-padding-top: 0px; + --padding-top: 0px; + + ion-label { + margin-top: 0px; + } + + *, h1 { @include core-transition(all, 200ms, linear); } @@ -1255,9 +1269,12 @@ ion-header[collapsible] { overflow: visible !important; } - h1, h2, .subheading { + h1 { --max-width: none; } + h1.cloned { + opacity: 0 !important; + } } ion-app.ios .collapsible-title h1 { @@ -1269,7 +1286,7 @@ ion-app.md .collapsible-title h1 { } .collapsible-title.collapsible-title-collapsed { - ion-label, h1, h2, ion-row, ion-col, .subheading { + ion-label, h1, ion-row, ion-col { opacity: 0; } } @@ -1279,13 +1296,13 @@ ion-app.md .collapsible-title h1 { opacity: var(--collapse-opacity, 0); } - ion-label, h1, h2, ion-row, ion-col, .subheading { + ion-label, h1, ion-row, ion-col, .subheading { opacity: 1; } } .collapsible-title.collapsible-title-collapse-nowrap { - h1, h2, .subheading { + h1:not(.cloned) { max-width: var(--max-width); white-space: nowrap; overflow: hidden; From 6b46f48b3ce1ea714b8896ef713278cdaab1fe8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Mon, 24 Jan 2022 17:46:36 +0100 Subject: [PATCH 4/7] MOBILE-3915 course: Improve course summary page --- scripts/langindex.json | 1 + src/core/classes/page-transition.ts | 3 + .../components/single-activity.scss | 3 + .../components/singleactivity.ts | 1 + .../course/pages/contents/contents.ts | 4 +- src/core/features/course/pages/index/index.ts | 2 +- .../course/pages/preview/preview.html | 92 ++++++------ .../course/pages/preview/preview.page.ts | 141 +++++++++++------- .../course/pages/preview/preview.scss | 64 ++++---- .../features/course/services/course-helper.ts | 23 +-- src/core/features/course/services/course.ts | 15 +- .../course/services/format-delegate.ts | 12 +- .../services/handlers/default-format.ts | 91 +++-------- .../course-list-item/course-list-item.ts | 2 +- .../courses/services/handlers/course-link.ts | 2 +- src/core/lang.json | 1 + upgrade.txt | 1 + 17 files changed, 233 insertions(+), 225 deletions(-) create mode 100644 src/core/features/course/format/singleactivity/components/single-activity.scss diff --git a/scripts/langindex.json b/scripts/langindex.json index 6d97093db..c2d7b6f89 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -2257,6 +2257,7 @@ "core.strftimetime24": "langconfig", "core.submit": "moodle", "core.success": "moodle", + "core.summary": "moodle", "core.tablet": "local_moodlemobileapp", "core.tag.defautltagcoll": "tag", "core.tag.errorareanotsupported": "local_moodlemobileapp", diff --git a/src/core/classes/page-transition.ts b/src/core/classes/page-transition.ts index 4ae2d83c9..4260515e0 100644 --- a/src/core/classes/page-transition.ts +++ b/src/core/classes/page-transition.ts @@ -93,6 +93,7 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp } rootAnimation.addAnimation(enteringContentAnimation); + enteringContentAnimation.beforeAddClass('animating').afterRemoveClass('animating'); if (backDirection) { enteringContentAnimation @@ -214,6 +215,8 @@ export const moodleTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp // setup leaving view if (leavingEl) { const leavingContent = createAnimation(); + leavingContent.beforeAddClass('animating').afterRemoveClass('animating'); + const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); diff --git a/src/core/features/course/format/singleactivity/components/single-activity.scss b/src/core/features/course/format/singleactivity/components/single-activity.scss new file mode 100644 index 000000000..1352a5254 --- /dev/null +++ b/src/core/features/course/format/singleactivity/components/single-activity.scss @@ -0,0 +1,3 @@ +:host ::ng-deep .collapsible-title { + display: none; +} diff --git a/src/core/features/course/format/singleactivity/components/singleactivity.ts b/src/core/features/course/format/singleactivity/components/singleactivity.ts index 01da69058..b3d11a9d7 100644 --- a/src/core/features/course/format/singleactivity/components/singleactivity.ts +++ b/src/core/features/course/format/singleactivity/components/singleactivity.ts @@ -31,6 +31,7 @@ import { CoreCourse } from '@features/course/services/course'; @Component({ selector: 'core-course-format-single-activity', templateUrl: 'core-course-format-single-activity.html', + styleUrls: ['single-activity.scss'], }) export class CoreCourseFormatSingleActivityComponent implements OnChanges { diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index a4de288f3..199599a4c 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -384,8 +384,8 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy { */ openCourseSummary(): void { CoreNavigator.navigateToSitePath( - '/course/' + this.course.id + '/preview', - { params: { course: this.course } }, + `/course/${this.course.id}/preview`, + { params: { course: this.course, avoidOpenCourse: true } }, ); } diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index 2ecf4abab..963df1009 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -249,7 +249,7 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { } CoreNavigator.navigateToSitePath( - '/course/' + this.course.id + '/preview', + `/course/${this.course.id}/preview`, { params: { course: this.course, avoidOpenCourse: true } }, ); } diff --git a/src/core/features/course/pages/preview/preview.html b/src/core/features/course/pages/preview/preview.html index 4969b265b..4cb9bad62 100644 --- a/src/core/features/course/pages/preview/preview.html +++ b/src/core/features/course/pages/preview/preview.html @@ -5,7 +5,7 @@

- + {{'core.course.coursesummary' | translate}}

@@ -16,44 +16,49 @@
-
+
- - + -

- - -

+

+ + +

{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }} - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}

+
+ + +
+

+ {{'core.summary' | translate}} +

- - + +

{{ 'core.teachers' | translate }}

- @@ -62,7 +67,7 @@ -
+ @@ -83,7 +88,8 @@ -
+ +

{{ instance.name }}

@@ -92,23 +98,24 @@
-
- - -

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

-

{{ 'core.paymentinstant' | translate }}

- - {{ 'core.courses.sendpaymentbutton' | translate }} - -
-
- - -

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

-
-
- + + +

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

+

{{ 'core.paymentinstant' | translate }}

+ + {{ 'core.courses.sendpaymentbutton' | translate }} + +
+
+ + +

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

+
+
+ + + @@ -116,23 +123,24 @@ [name]="prefetchCourseData.icon" color="success" aria-hidden="true" role="status"> - -

{{ 'core.course.downloadcourse' | translate }}

-

{{ 'core.course.refreshcourse' | translate }}

-
-
- + {{ 'core.course.downloadcourse' | translate }} + {{ 'core.course.refreshcourse' | translate }} + + + -

{{ 'core.course' | translate }}

+ {{ 'core.course' | translate }}
-
- + + + -

{{ 'core.openinbrowser' | translate }}

+ {{ 'core.openinbrowser' | translate }}
-
+ +
diff --git a/src/core/features/course/pages/preview/preview.page.ts b/src/core/features/course/pages/preview/preview.page.ts index 923eb7109..8c1f9b9ce 100644 --- a/src/core/features/course/pages/preview/preview.page.ts +++ b/src/core/features/course/pages/preview/preview.page.ts @@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreTextUtils } from '@services/utils/text'; import { + CoreCourseCustomField, CoreCourseEnrolmentMethod, - CoreCourseGetCoursesData, CoreCourses, CoreCourseSearchedData, CoreCoursesProvider, @@ -34,6 +34,8 @@ import { Translate } from '@singletons'; import { CoreConstants } from '@/core/constants'; import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password'; import { CoreNavigator } from '@services/navigator'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper'; /** * Page that allows "previewing" a course and enrolling in it if enabled and not enrolled. @@ -45,12 +47,13 @@ import { CoreNavigator } from '@services/navigator'; }) export class CoreCoursePreviewPage implements OnInit, OnDestroy { - course?: CoreCourseSearchedData; + course?: CoreCourseSummaryData; isEnrolled = false; canAccessCourse = true; selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; paypalEnabled = false; dataLoaded = false; + avoidOpenCourse = false; prefetchCourseData: CorePrefetchStatusInfo = { icon: '', statusTranslatable: 'core.loading', @@ -64,6 +67,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { courseUrl = ''; courseImageUrl?: string; isMobile: boolean; + progress?: number; protected isGuestEnabled = false; protected useGuestAccess = false; @@ -74,6 +78,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { protected paypalReturnUrl = ''; protected pageDestroyed = false; protected courseStatusObserver?: CoreEventObserver; + protected courseId!: number; constructor( protected zone: NgZone, @@ -84,7 +89,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { if (this.downloadCourseEnabled) { // Listen for status change in course. this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => { - if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { + if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) { this.updateCourseStatus(data.status); } }, CoreSites.getCurrentSiteId()); @@ -92,27 +97,25 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { } /** - * View loaded. + * @inheritdoc */ async ngOnInit(): Promise { - this.course = CoreNavigator.getRouteParam('course'); - - if (!this.course) { + try { + this.courseId = CoreNavigator.getRequiredRouteNumberParam('courseId'); + } catch (error) { + CoreDomUtils.showErrorModal(error); CoreNavigator.back(); return; } - const currentSite = CoreSites.getCurrentSite(); - const currentSiteUrl = currentSite && currentSite.getURL(); + this.avoidOpenCourse = !!CoreNavigator.getRouteBooleanParam('avoidOpenCourse'); + this.course = CoreNavigator.getRouteParam('course'); - this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1; - this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id); - this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id); - this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php'); - if (this.course.overviewfiles.length > 0) { - this.courseImageUrl = this.course.overviewfiles[0].fileurl; - } + const currentSiteUrl = CoreSites.getRequiredCurrentSite().getURL(); + this.enrolUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/index.php?id=' + this.courseId); + this.courseUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'course/view.php?id=' + this.courseId); + this.paypalReturnUrl = CoreTextUtils.concatenatePaths(currentSiteUrl, 'enrol/paypal/return.php'); try { await this.getCourse(); @@ -120,11 +123,11 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { if (this.downloadCourseEnabled) { // Determine course prefetch icon. - this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.course!.id); + this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId); if (this.prefetchCourseData.loading) { // Course is being downloaded. Get the download promise. - const promise = CoreCourseHelper.getCourseDownloadPromise(this.course!.id); + const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId); if (promise) { // There is a download promise. If it fails, show an error. promise.catch((error) => { @@ -134,7 +137,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { }); } else { // No download, this probably means that the app was closed while downloading. Set previous status. - CoreCourse.setCoursePreviousStatus(this.course!.id); + CoreCourse.setCoursePreviousStatus(this.courseId); } } } @@ -177,13 +180,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { this.selfEnrolInstances = []; try { - this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.course!.id); + this.enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId); this.enrolmentMethods.forEach((method) => { if (method.type === 'self') { this.selfEnrolInstances.push(method); } else if (method.type === 'guest') { this.isGuestEnabled = true; + } else if (method.type === 'paypal') { + this.paypalEnabled = true; } }); } catch (error) { @@ -191,22 +196,17 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { } try { - let course: CoreEnrolledCourseData | CoreCourseGetCoursesData; - // Check if user is enrolled in the course. try { - course = await CoreCourses.getUserCourse(this.course!.id); + this.course = await CoreCourses.getUserCourse(this.courseId); this.isEnrolled = true; } catch { // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course. this.isEnrolled = false; - - course = await CoreCourses.getCourse(this.course!.id); + this.course = await CoreCourses.getCourse(this.courseId); } // Success retrieving the course, we can assume the user has permissions to view it. - this.course!.fullname = course.fullname || this.course!.fullname; - this.course!.summary = course.summary || this.course!.summary; this.canAccessCourse = true; this.useGuestAccess = false; } catch { @@ -219,14 +219,37 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { } } - if (!CoreSites.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) { - try { - const course = await CoreCourses.getCourseByField('id', this.course!.id); + if (this.course && 'overviewfiles' in this.course && this.course.overviewfiles?.length) { + this.courseImageUrl = this.course.overviewfiles[0].fileurl; + } - this.course!.customfields = course.customfields; - } catch { - // Ignore errors. + try { + const courseByField = await CoreCourses.getCourseByField('id', this.courseId); + if (this.course) { + this.course.customfields = courseByField.customfields; + this.course.contacts = courseByField.contacts; + this.course.displayname = courseByField.displayname; + this.course.categoryname = courseByField.categoryname; + this.course.overviewfiles = courseByField.overviewfiles; + } else { + this.course = courseByField; } + + this.paypalEnabled = !this.isEnrolled && courseByField.enrollmentmethods?.indexOf('paypal') > -1; + + } catch { + // Ignore errors. + } + + if (!this.course || + !('progress' in this.course) || + typeof this.course.progress !== 'number' || + this.course.progress < 0 || + this.course.completionusertracked === false + ) { + this.progress = undefined; + } else { + this.progress = this.course.progress; } this.dataLoaded = true; @@ -234,13 +257,15 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { /** * Open the course. + * + * @param replaceCurrentPage If current place should be replaced in the navigation stack. */ - openCourse(): void { - if (!this.canAccessCourse) { + openCourse(replaceCurrentPage = false): void { + if (!this.canAccessCourse || !this.course || this.avoidOpenCourse) { return; } - CoreCourseHelper.openCourse(this.course!, { isGuest: this.useGuestAccess }); + CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage }); } /** @@ -279,7 +304,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { }; // Open the enrolment page in InAppBrowser. - const window = await CoreSites.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl); + const window = await CoreSites.getRequiredCurrentSite().openInAppWithAutoLogin(this.enrolUrl); // Observe loaded pages in the InAppBrowser to check if the enrol process has ended. const inAppLoadSubscription = window.on('loadstart').subscribe((event) => { @@ -319,7 +344,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { const modal = await CoreDomUtils.showModalLoading('core.loading', true); try { - await CoreCourses.selfEnrol(this.course!.id, password, instanceId); + await CoreCourses.selfEnrol(this.courseId, password, instanceId); // Close modal and refresh data. this.isEnrolled = true; @@ -331,13 +356,13 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { await this.refreshData().finally(() => { // My courses have been updated, trigger event. CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { - courseId: this.course!.id, + courseId: this.courseId, course: this.course, action: CoreCoursesProvider.ACTION_ENROL, }, CoreSites.getCurrentSiteId()); }); - this.openCourse(); + this.openCourse(true); modal?.dismiss(); } catch (error) { @@ -378,12 +403,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { const promises: Promise[] = []; promises.push(CoreCourses.invalidateUserCourses()); - promises.push(CoreCourses.invalidateCourse(this.course!.id)); - promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.course!.id)); - promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id)); - if (CoreSites.getCurrentSite() && !CoreSites.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) { - promises.push(CoreCourses.invalidateCoursesByField('id', this.course!.id)); - } + promises.push(CoreCourses.invalidateCourse(this.courseId)); + promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId)); + promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId)); + promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId)); if (this.guestInstanceId) { promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId)); } @@ -419,14 +442,10 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { } // Check if user is enrolled in the course. - try { - CoreCourses.invalidateUserCourses(); - } catch { - // Ignore errors. - } + await CoreUtils.ignoreErrors(CoreCourses.invalidateUserCourses()); try { - await CoreCourses.getUserCourse(this.course!.id); + await CoreCourses.getUserCourse(this.courseId); } catch { // Not enrolled, wait a bit and try again. if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) { @@ -451,7 +470,7 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { */ async prefetchCourse(): Promise { try { - await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course!, { + await CoreCourseHelper.confirmAndPrefetchCourse(this.prefetchCourseData, this.course as CoreEnrolledCourseData, { isGuest: this.useGuestAccess, }); } catch (error) { @@ -462,14 +481,20 @@ export class CoreCoursePreviewPage implements OnInit, OnDestroy { } /** - * Page destroyed. + * @inheritdoc */ ngOnDestroy(): void { this.pageDestroyed = true; - - if (this.courseStatusObserver) { - this.courseStatusObserver.off(); - } + this.courseStatusObserver?.off(); } } + +type CoreCourseSummaryData = CoreCourseWithImageAndColor & (CoreEnrolledCourseData | CoreCourseSearchedData) & { + contacts?: { // Contact users. + id: number; // Contact user id. + fullname: string; // Contact user fullname. + }[]; + customfields?: CoreCourseCustomField[]; // Custom fields and associated values. + categoryname?: string; // Category name. +}; diff --git a/src/core/features/course/pages/preview/preview.scss b/src/core/features/course/pages/preview/preview.scss index 161f3647a..8f159c333 100644 --- a/src/core/features/course/pages/preview/preview.scss +++ b/src/core/features/course/pages/preview/preview.scss @@ -1,41 +1,43 @@ :host { - --scroll-factor: 0.5; - --translate-z: calc(-2 * var(--scroll-factor))px; - --scale: calc(1 + var(--scroll-factor) * 2); + ion-content:not(.animating) { + &::part(scroll) { + perspective: 1px; + perspective-origin: center top; + transform-style: preserve-3d; + } - perspective: 1px; - perspective-origin: center top; - transform-style: preserve-3d; + .core-course-thumb { + transform-origin: center top; - // @todo This parallax effect caused the image to be scaled during page transitions, - // and in some devices it seems like the problem persisted even after the transition. - // We should decide whether we want to keep this parallax or not, and if we do fix - // the problem or find an alternative implementation. For now, it's disabled. + --scroll-factor: 0.5; + --translate-z: calc(-2 * var(--scroll-factor))px; + --scale: calc(1 + var(--scroll-factor) * 2); + + /** + * Calculated with scroll-factor: 0.5; + * translate-z: -2 * $scroll-factor px; + * scale: 1 + $scroll-factor * 2; + */ + transform: translateZ(-1px) scale(2); + } + } + + .core-course-thumb-parallax-content { + transform: translateZ(0); + -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); + filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); + } + + .core-course-thumb-parallax { + height: 40vw; + max-height: 35vh; + z-index: -1; + overflow: hidden; + } - // .core-course-thumb-parallax-content { - // transform: translateZ(0); - // -webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); - // filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow))); - // } - // .core-course-thumb-parallax { - // height: 40vw; - // max-height: 35vh; - // z-index: -1; - // overflow: hidden; - // } .core-course-thumb { overflow: hidden; text-align: center; - cursor: pointer; - pointer-events: auto; - transform-origin: center top; - - /** - * Calculated with scroll-factor: 0.5; - * translate-z: -2 * $scroll-factor px; - * scale: 1 + $scroll-factor * 2; - */ - // transform: translateZ(-1px) scale(2); } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 737a405ae..56dee5790 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -68,7 +68,7 @@ import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { CoreFilterHelper } from '@features/filter/services/filter-helper'; import { CoreNetworkError } from '@classes/errors/network-error'; import { CoreSiteHome } from '@features/sitehome/services/sitehome'; -import { CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreSiteHomeHomeHandlerService } from '@features/sitehome/services/handlers/sitehome-home'; import { CoreStatusWithWarningsWSResponse } from '@services/ws'; @@ -1178,7 +1178,7 @@ export class CoreCourseHelperProvider { modal?.dismiss(); - return this.openCourse(course, params, siteId); + return this.openCourse(course, { params , siteId }); } /** @@ -2020,20 +2020,25 @@ export class CoreCourseHelperProvider { * they will see the result immediately. * * @param course Course to open - * @param params Params to pass to the course page. - * @param siteId Site ID. If not defined, current site. + * @param navOptions Navigation options that includes params to pass to the page. * @return Promise resolved when done. */ - async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params, siteId?: string): Promise { + async openCourse( + course: CoreCourseAnyCourseData | { id: number }, + navOptions?: CoreNavigationOptions & { siteId?: string }, + ): Promise { + const siteId = navOptions?.siteId; if (!siteId || siteId == CoreSites.getCurrentSiteId()) { // Current site, we can open the course. - return CoreCourse.openCourse(course, params); + return CoreCourse.openCourse(course, navOptions); } else { // We need to load the site first. - params = params || {}; - Object.assign(params, { course: course }); + navOptions = navOptions || {}; - await CoreNavigator.navigateToSitePath(`course/${course.id}`, { siteId, params }); + navOptions.params = navOptions.params || {}; + Object.assign(navOptions.params, { course: course }); + + await CoreNavigator.navigateToSitePath(`course/${course.id}`, navOptions); } } diff --git a/src/core/features/course/services/course.ts b/src/core/features/course/services/course.ts index 001452497..fa30f2991 100644 --- a/src/core/features/course/services/course.ts +++ b/src/core/features/course/services/course.ts @@ -43,7 +43,7 @@ import { CoreCourseLogCronHandler } from './handlers/log-cron'; import { CoreSitePlugins } from '@features/siteplugins/services/siteplugins'; import { CoreCourseAutoSyncData, CoreCourseSyncProvider } from './sync'; import { CoreTagItem } from '@features/tag/services/tag'; -import { CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreCourseModuleDelegate } from './module-delegate'; const ROOT_CACHE_KEY = 'mmCourse:'; @@ -1177,10 +1177,13 @@ export class CoreCourseProvider { * This function must be in here instead of course helper to prevent circular dependencies. * * @param course Course to open - * @param params Other params to pass to the course page. + * @param navOptions Navigation options that includes params to pass to the page. * @return Promise resolved when done. */ - async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise { + async openCourse( + course: CoreCourseAnyCourseData | { id: number }, + navOptions?: CoreNavigationOptions, + ): Promise { const loading = await CoreDomUtils.showModalLoading(); // Wait for site plugins to be fetched. @@ -1197,7 +1200,7 @@ export class CoreCourseProvider { if (!format || !CoreSitePlugins.sitePluginPromiseExists(`format_${format}`)) { // No custom format plugin. We don't need to wait for anything. loading.dismiss(); - await CoreCourseFormatDelegate.openCourse( course, params); + await CoreCourseFormatDelegate.openCourse( course, navOptions); return; } @@ -1208,7 +1211,7 @@ export class CoreCourseProvider { // The format loaded successfully, but the handlers wont be registered until all site plugins have loaded. if (CoreSitePlugins.sitePluginsFinishedLoading) { - return CoreCourseFormatDelegate.openCourse( course, params); + return CoreCourseFormatDelegate.openCourse( course, navOptions); } // Wait for plugins to be loaded. @@ -1217,7 +1220,7 @@ export class CoreCourseProvider { const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => { observer?.off(); - CoreCourseFormatDelegate.openCourse( course, params) + CoreCourseFormatDelegate.openCourse( course, navOptions) .then(deferred.resolve).catch(deferred.reject); }); diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts index 967903d58..4bfb14bc7 100644 --- a/src/core/features/course/services/format-delegate.ts +++ b/src/core/features/course/services/format-delegate.ts @@ -13,10 +13,10 @@ // limitations under the License. import { Injectable, Type } from '@angular/core'; -import { Params } from '@angular/router'; import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreNavigationOptions } from '@services/navigator'; import { makeSingleton } from '@singletons'; import { CoreCourseWSSection } from './course'; import { CoreCourseSection } from './course-helper'; @@ -100,10 +100,10 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { * Your page should include the course handlers using CoreCoursesDelegate. * * @param course The course to open. It should contain a "format" attribute. - * @param params Params to pass to the course page. + * @param navOptions Navigation options that includes params to pass to the page. * @return Promise resolved when done. */ - openCourse?(course: CoreCourseAnyCourseData, params?: Params): Promise; + openCourse?(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise; /** * Return the Component to use to display the course format instead of using the default one. @@ -323,11 +323,11 @@ export class CoreCourseFormatDelegateService extends CoreDelegate { - await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, params]); + async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise { + await this.executeFunctionOnEnabled(course.format || '', 'openCourse', [course, navOptions]); } /** diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts index 4ef53ca4b..d7254f194 100644 --- a/src/core/features/course/services/handlers/default-format.ts +++ b/src/core/features/course/services/handlers/default-format.ts @@ -13,12 +13,9 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { Params } from '@angular/router'; - import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; -import { CoreNavigator } from '@services/navigator'; +import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; -import { CoreCourseWSSection } from '../course'; import { CoreCourseSection } from '../course-helper'; import { CoreCourseFormatHandler } from '../format-delegate'; @@ -32,19 +29,14 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { format = 'default'; /** - * Whether or not the handler is enabled on a site level. - * - * @return Promise resolved with true if enabled. + * @inheritdoc */ async isEnabled(): Promise { return true; } /** - * Get the title to use in course page. - * - * @param course The course. - * @return Title. + * @inheritdoc */ getCourseTitle(course: CoreCourseAnyCourseData): string { if (course.displayname) { @@ -57,57 +49,35 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { } /** - * Whether it allows seeing all sections at the same time. Defaults to true. - * - * @param course The course to check. - * @return Whether it can view all sections. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - canViewAllSections(course: CoreCourseAnyCourseData): boolean { + canViewAllSections(): boolean { return true; } /** - * Whether the option blocks should be displayed. Defaults to true. - * - * @param course The course to check. - * @return Whether it can display blocks. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - displayBlocks(course: CoreCourseAnyCourseData): boolean { + displayBlocks(): boolean { return true; } /** - * Whether the default section selector should be displayed. Defaults to true. - * - * @param course The course to check. - * @return Whether the default section selector should be displayed. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - displaySectionSelector(course: CoreCourseAnyCourseData): boolean { + displaySectionSelector(): boolean { return true; } /** - * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, - * and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true. - * - * @param course The course to check. - * @param sections List of course sections. - * @return Whether the refresher should be displayed. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean { + displayRefresher(): boolean { return true; } /** - * Given a list of sections, get the "current" section that should be displayed first. - * - * @param course The course to get the title. - * @param sections List of sections. - * @return Current section (or promise resolved with current section). + * @inheritdoc */ async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { let marker: number | undefined; @@ -137,48 +107,33 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { } /** - * Invalidate the data required to load the course format. - * - * @param course The course to get the title. - * @param sections List of sections. - * @return Promise resolved when the data is invalidated. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async invalidateData(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): Promise { + async invalidateData(course: CoreCourseAnyCourseData): Promise { await CoreCourses.invalidateCoursesByField('id', course.id); } /** - * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. - * Implement it only if you want to create your own page to display the course. In general it's better to use the method - * getCourseFormatComponent because it will display the course handlers at the top. - * Your page should include the course handlers using CoreCoursesDelegate. - * - * @param course The course to open. It should contain a "format" attribute. - * @param params Params to pass to the course page. - * @return Promise resolved when done. + * @inheritdoc */ - async openCourse(course: CoreCourseAnyCourseData, params?: Params): Promise { - params = params || {}; - Object.assign(params, { course: course }); + async openCourse(course: CoreCourseAnyCourseData, navOptions?: CoreNavigationOptions): Promise { + navOptions = navOptions || {}; + + navOptions.params = navOptions.params || {}; + Object.assign(navOptions.params, { course: course }); // Don't return the .push promise, we don't want to display a loading modal during the page transition. const currentTab = CoreNavigator.getCurrentMainMenuTab(); const routeDepth = CoreNavigator.getRouteDepth(`/main/${currentTab}/course/${course.id}`); const deepPath = '/deep'.repeat(routeDepth); - CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, { params }); + CoreNavigator.navigateToSitePath(`course${deepPath}/${course.id}`, navOptions); } /** - * Whether the view should be refreshed when completion changes. If your course format doesn't display - * activity completion then you should return false. - * - * @param course The course. - * @return Whether course view should be refreshed when an activity completion changes. + * @inheritdoc */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise { + async shouldRefreshWhenCompletionChanges(): Promise { return true; } 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 bf9d4419f..5a5f5fa24 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 @@ -171,7 +171,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On CoreCourseHelper.openCourse(this.course); } else { CoreNavigator.navigateToSitePath( - '/course/' + this.course.id + '/preview', + `/course/${this.course.id}/preview`, { params: { course: this.course } }, ); } diff --git a/src/core/features/courses/services/handlers/course-link.ts b/src/core/features/courses/services/handlers/course-link.ts index 5e1800ddb..5c7347844 100644 --- a/src/core/features/courses/services/handlers/course-link.ts +++ b/src/core/features/courses/services/handlers/course-link.ts @@ -156,7 +156,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler modal.dismiss(); // Now open the course. - CoreCourseHelper.openCourse(course, pageParams); + CoreCourseHelper.openCourse(course, { params: pageParams }); } /** diff --git a/src/core/lang.json b/src/core/lang.json index 6690dc4bb..624038e0b 100644 --- a/src/core/lang.json +++ b/src/core/lang.json @@ -301,6 +301,7 @@ "strftimetime24": "%H:%M", "submit": "Submit", "success": "Success", + "summary": "Summary", "tablet": "Tablet", "teachers": "Teachers", "thereisdatatosync": "There are offline {{$a}} to be synchronised.", diff --git a/upgrade.txt b/upgrade.txt index 79ccced1d..e55234e0d 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -15,6 +15,7 @@ information provided here is intended especially for developers. The function CoreUserDelegate.getProfileHandlersFor must now receive a context + contextId instead of a courseId. The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId. Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action. +- CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page. === 3.9.5 === From 0af808571cf745e778a9636fc5906e53283349e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 25 Jan 2022 17:33:20 +0100 Subject: [PATCH 5/7] MOBILE-3915 course: Improve Course index page --- scripts/langindex.json | 7 +- .../components/course-index/course-index.html | 79 ++++++++----- .../components/course-index/course-index.scss | 24 +++- .../components/course-index/course-index.ts | 111 +++++++++++++----- .../components/format/core-course-format.html | 3 +- .../course/components/format/format.scss | 6 + .../course/components/format/format.ts | 23 +++- .../handlers/singleactivity-format.ts | 2 +- .../weeks/services/handlers/weeks-format.ts | 21 ++-- src/core/features/course/lang.json | 13 +- .../course/services/format-delegate.ts | 48 +++++++- .../services/handlers/default-format.ts | 10 +- .../classes/handlers/course-format-handler.ts | 5 +- .../siteplugins/services/siteplugins.ts | 4 + src/core/services/utils/dom.ts | 2 +- upgrade.txt | 1 + 16 files changed, 264 insertions(+), 95 deletions(-) diff --git a/scripts/langindex.json b/scripts/langindex.json index c2d7b6f89..70e2ef078 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1536,19 +1536,23 @@ "core.course.confirmpartialdownloadsize": "local_moodlemobileapp", "core.course.couldnotloadsectioncontent": "local_moodlemobileapp", "core.course.couldnotloadsections": "local_moodlemobileapp", + "core.course.courseindex": "courseformat", "core.course.coursesummary": "moodle", + "core.course.done": "completion", "core.course.downloadcourse": "tool_mobile", "core.course.downloadcoursesprogressdescription": "local_moodlemobileapp", "core.course.downloadsectionprogressdescription": "local_moodlemobileapp", "core.course.errordownloadingcourse": "local_moodlemobileapp", "core.course.errordownloadingsection": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp", + "core.course.failed": "completion", "core.course.gotonextactivity": "local_moodlemobileapp", "core.course.gotonextactivitynotfound": "local_moodlemobileapp", "core.course.gotopreviousactivity": "local_moodlemobileapp", "core.course.gotopreviousactivitynotfound": "local_moodlemobileapp", "core.course.hiddenfromstudents": "moodle", "core.course.hiddenoncoursepage": "moodle", + "core.course.highlighted": "moodle", "core.course.insufficientavailablequota": "local_moodlemobileapp", "core.course.insufficientavailablespace": "local_moodlemobileapp", "core.course.manualcompletionnotsynced": "local_moodlemobileapp", @@ -1557,7 +1561,8 @@ "core.course.overriddennotice": "grades", "core.course.refreshcourse": "local_moodlemobileapp", "core.course.section": "moodle", - "core.course.sections": "moodle", + "core.course.thisweek": "format_weeks/currentsection", + "core.course.todo": "completion", "core.course.useactivityonbrowser": "local_moodlemobileapp", "core.course.warningmanualcompletionmodified": "local_moodlemobileapp", "core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp", diff --git a/src/core/features/course/components/course-index/course-index.html b/src/core/features/course/components/course-index/course-index.html index 242cf97c4..7a97a6b41 100644 --- a/src/core/features/course/components/course-index/course-index.html +++ b/src/core/features/course/components/course-index/course-index.html @@ -13,49 +13,66 @@ - - - +

- - - - - {{ 'core.course.hiddenfromstudents' | translate }} - - - {{ 'core.notavailable' | translate }} - - - - -
-
- - - - - - - - + + + + +

- +

+ {{highlighted}} +
+
+ + + + + + + + + + + +

+ + +

+
+ +
+
+
+
diff --git a/src/core/features/course/components/course-index/course-index.scss b/src/core/features/course/components/course-index/course-index.scss index 828f42727..8ef120471 100644 --- a/src/core/features/course/components/course-index/course-index.scss +++ b/src/core/features/course/components/course-index/course-index.scss @@ -2,6 +2,7 @@ core-progress-bar { --bar-margin: 8px 0 4px 0; --line-height: 20px; + --background: var(--contrast-background); } @if ($core-hide-progress-on-section-selector) { @@ -10,6 +11,25 @@ core-progress-bar { } } -ion-badge { - text-align: start; +ion-icon.completioninfo { + font-size: 10px; + width: 18px; +} + +ion-item.section::part(native) { + --padding-start: 0; +} + +ion-icon.expandable-status-icon { + margin: 0; + @include padding(12px, 32px, 12px, 16px); +} + +ion-item.item-current ion-icon.expandable-status-icon { + @include padding(null, null, null, 11px); + +} + +ion-icon.restricted { + font-size: 14px; } diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index 3b51cad30..87c653fb2 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, Input, OnInit } from '@angular/core'; +import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; -import { CoreCourseModuleData, CoreCourseSection, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; +import { CoreCourseModuleData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking, @@ -23,6 +23,9 @@ import { import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; +import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; +import { IonContent } from '@ionic/angular'; +import { CoreDomUtils } from '@services/utils/dom'; /** * Component to display course index modal. @@ -34,19 +37,30 @@ import { ModalController } from '@singletons'; }) export class CoreCourseCourseIndexComponent implements OnInit { - @Input() sections?: SectionWithProgress[]; - @Input() selected?: CoreCourseSection; + @ViewChild(IonContent) content?: IonContent; + + @Input() sections?: CourseIndexSection[]; + @Input() selectedId?: number; @Input() course?: CoreCourseAnyCourseData; stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; + allSectionId = CoreCourseProvider.ALL_SECTIONS_ID; + highlighted?: string; + + constructor( + protected elementRef: ElementRef, + ) { + } /** * @inheritdoc */ - ngOnInit(): void { + async ngOnInit(): Promise { if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) || !this.course.courseformatoptions) { + this.closeModal(); + return; } @@ -55,32 +69,48 @@ export class CoreCourseCourseIndexComponent implements OnInit { if (!formatOptions || formatOptions.completionusertracked === false) { return; } + const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections); + currentSection.highlighted = true; + if (this.selectedId === undefined) { + currentSection.expanded = true; + this.selectedId = currentSection.id; + } else { + const selectedSection = this.sections.find((section) => section.id == this.selectedId); + if (selectedSection) { + selectedSection.expanded = true; + } + } this.sections.forEach((section) => { - let complete = 0; - let total = 0; section.modules.forEach((module) => { - console.error(module); - if (!module.uservisible || module.completiondata === undefined || - module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE) { - module.completionStatus = undefined; - - return; - } - - module.completionStatus = module.completiondata.state; - - total++; - if (module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE || - module.completiondata.state == CoreCourseModuleCompletionStatus.COMPLETION_COMPLETE_PASS) { - complete++; - } + module.completionStatus = module.completiondata === undefined || + module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE + ? undefined + : module.completiondata.state; }); - - if (total > 0) { - section.progress = complete / total * 100; - } }); + + this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); + + setTimeout(() => { + CoreDomUtils.scrollToElementBySelector( + this.elementRef.nativeElement, + this.content, + '.item.item-current', + ); + }, 200); + } + + /** + * Toggle expand status. + * + * @param event Event object. + * @param section Section to expand / collapse. + */ + toggleExpand(event: Event, section: CourseIndexSection): void { + section.expanded = !section.expanded; + event.stopPropagation(); + event.preventDefault(); } /** @@ -93,19 +123,40 @@ export class CoreCourseCourseIndexComponent implements OnInit { /** * Select a section. * + * @param event Event. * @param section Selected section object. */ - selectSection(section: SectionWithProgress): void { + selectSection(event: Event, section: CoreCourseSectionWithStatus): void { if (section.uservisible !== false) { - ModalController.dismiss(section); + ModalController.dismiss({ event, section }); + } + } + + /** + * Select a section and open a module + * + * @param event Event. + * @param section Selected section object. + * @param module Selected module object. + */ + selectModule(event: Event,section: CoreCourseSectionWithStatus, module: CoreCourseModuleData): void { + if (module.uservisible !== false) { + ModalController.dismiss({ event, section, module }); } } } -type SectionWithProgress = Omit & { - progress?: number; +type CourseIndexSection = Omit & { + highlighted?: boolean; + expanded?: boolean; modules: (CoreCourseModuleData & { completionStatus?: CoreCourseModuleCompletionStatus; })[]; }; + +export type CoreCourseIndexSectionWithModule = { + event: Event; + section: CourseIndexSection; + module?: CoreCourseModuleData; +}; diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 3249dad88..1b34598a3 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -66,7 +66,8 @@
- +

diff --git a/src/core/features/course/components/format/format.scss b/src/core/features/course/components/format/format.scss index f6444fb72..7c33a30cc 100644 --- a/src/core/features/course/components/format/format.scss +++ b/src/core/features/course/components/format/format.scss @@ -9,3 +9,9 @@ text-transform: none; } } + +.course-section { + ion-badge { + text-align: start; + } +} diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index 1cdc25197..760637898 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -44,9 +44,11 @@ import { CoreCourseFormatDelegate } from '@features/course/services/format-deleg import { CoreEventObserver, CoreEvents } from '@singletons/events'; import { IonContent, IonRefresher } from '@ionic/angular'; import { CoreUtils } from '@services/utils/utils'; -import { CoreCourseCourseIndexComponent } from '../course-index/course-index'; +import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreNavigator } from '@services/navigator'; +import { database } from 'faker'; +import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; /** * Component to display course contents using a certain format. If the format isn't found, use default one. @@ -182,7 +184,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Course has changed, try to get the components. this.getComponents(); - this.displayCourseIndex = CoreCourseFormatDelegate.displaySectionSelector(this.course); + this.displayCourseIndex = CoreCourseFormatDelegate.displayCourseIndex(this.course); this.displayBlocks = CoreCourseFormatDelegate.displayBlocks(this.course); this.hasBlocks = await CoreBlockHelper.hasCourseBlocks(this.course.id); @@ -319,17 +321,28 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { * Display the course index modal. */ async openCourseIndex(): Promise { - const data = await CoreDomUtils.openModal({ + const data = await CoreDomUtils.openModal({ component: CoreCourseCourseIndexComponent, componentProps: { course: this.course, sections: this.sections, - selected: this.selectedSection, + selectedId: this.selectedSection?.id, }, }); if (data) { - this.sectionChanged(data); + this.sectionChanged(data.section); + if (data.module) { + if (!data.module.handlerData) { + data.module.handlerData = + await CoreCourseModuleDelegate.getModuleDataFor(data.module.modname, data.module, this.course.id); + } + + if (data.module.uservisible !== false && data.module.handlerData?.action) { + data.module.handlerData.action(data.event, data.module, data.module.course); + } + this.moduleId = data.module.id; + } } } diff --git a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts index 945b99e4c..8e4b1f6d3 100644 --- a/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts +++ b/src/core/features/course/format/singleactivity/services/handlers/singleactivity-format.ts @@ -71,7 +71,7 @@ export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseF /** * @inheritdoc */ - displaySectionSelector(): boolean { + displayCourseIndex(): boolean { return false; } diff --git a/src/core/features/course/format/weeks/services/handlers/weeks-format.ts b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts index 10de07200..3af676006 100644 --- a/src/core/features/course/format/weeks/services/handlers/weeks-format.ts +++ b/src/core/features/course/format/weeks/services/handlers/weeks-format.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreCourseFormatHandler } from '@features/course/services/format-delegate'; -import { makeSingleton } from '@singletons'; +import { makeSingleton, Translate } from '@singletons'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreCourseWSSection } from '@features/course/services/course'; import { CoreConstants } from '@/core/constants'; @@ -32,20 +32,14 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand format = 'weeks'; /** - * Whether or not the handler is enabled on a site level. - * - * @return True or promise resolved with true if enabled. + * @inheritdoc */ async isEnabled(): Promise { return true; } /** - * Given a list of sections, get the "current" section that should be displayed first. - * - * @param course The course to get the title. - * @param sections List of sections. - * @return Current section (or promise resolved with current section). + * @inheritdoc */ async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise { const now = CoreTimeUtils.timestamp(); @@ -71,6 +65,13 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand return sections[0]; } + /** + * @inheritdoc + */ + getSectionHightlightedName(): string { + return Translate.instant('core.course.thisweek'); + } + /** * Return the start and end date of a section. * @@ -83,7 +84,7 @@ export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHand startDate = startDate + 7200; const dates = { - start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)), + start: startDate + (CoreConstants.SECONDS_WEEK * ((section.section || 0) - 1)), end: 0, }; dates.end = dates.start + CoreConstants.SECONDS_WEEK; diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index d83be79fa..b3b0f24be 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -27,21 +27,24 @@ "confirmpartialdownloadsize": "You are about to download at least {{size}}.{{availableSpace}} Are you sure you want to continue?", "confirmlimiteddownload": "You are not currently connected to Wi-Fi. ", "courseindex": "Course index", - "gotonextactivity": "Continue to next activity", - "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.", - "gotopreviousactivity": "Continue to previous activity", - "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.", "couldnotloadsectioncontent": "Could not load the section content. Please try again later.", "couldnotloadsections": "Could not load the sections. Please try again later.", "coursesummary": "Course summary", + "done": "Done", "downloadcourse": "Download course", "downloadcoursesprogressdescription": "Downloading courses: downloaded {{count}} out of {{total}}.", "downloadsectionprogressdescription": "Downloading section: downloaded {{count}} out of {{total}}.", "errordownloadingcourse": "Error downloading course.", "errordownloadingsection": "Error downloading section.", "errorgetmodule": "Error getting activity data.", + "failed": "Failed", + "gotonextactivity": "Continue to next activity", + "gotonextactivitynotfound": "Next activity not found. It's possible that it has been hidden or deleted.", + "gotopreviousactivity": "Continue to previous activity", + "gotopreviousactivitynotfound": "Previous activity not found. It's possible that it has been hidden or deleted.", "hiddenfromstudents": "Hidden from students", "hiddenoncoursepage": "Available but not shown on course page", + "highlighted": "Highlighted", "insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.", "insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.", "manualcompletionnotsynced": "Manual completion not synchronised.", @@ -50,6 +53,8 @@ "overriddennotice": "Your final grade from this activity was manually adjusted.", "refreshcourse": "Refresh course", "section": "Section", + "thisweek": "This week", + "todo": "To do", "useactivityonbrowser": "You can still use it using your device's web browser.", "warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.", "warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}" diff --git a/src/core/features/course/services/format-delegate.ts b/src/core/features/course/services/format-delegate.ts index 4bfb14bc7..94a4bcc4c 100644 --- a/src/core/features/course/services/format-delegate.ts +++ b/src/core/features/course/services/format-delegate.ts @@ -66,13 +66,22 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { */ displayEnableDownload?(course: CoreCourseAnyCourseData): boolean; + /** + * Whether the default course index should be displayed. Defaults to true. + * + * @deprecated on 4.0. Please use displayCourseIndex instead. + * @param course The course to check. + * @return Whether the default course index should be displayed. + */ + displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; + /** * Whether the default section selector should be displayed. Defaults to true. * * @param course The course to check. * @return Whether the default section selector should be displayed. */ - displaySectionSelector?(course: CoreCourseAnyCourseData): boolean; + displayCourseIndex?(course: CoreCourseAnyCourseData): boolean; /** * Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format, @@ -93,6 +102,13 @@ export interface CoreCourseFormatHandler extends CoreDelegateHandler { */ getCurrentSection?(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise; + /** + * Returns the name for the highlighted section. + * + * @return The name for the highlighted section based on the given course format. + */ + getSectionHightlightedName?(): string; + /** * Open the page to display a course. If not defined, the page CoreCourseSectionPage will be opened. * Implement it only if you want to create your own page to display the course. In general it's better to use the method @@ -209,12 +225,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate(course.format || '', 'displayCourseIndex', [course]); + + if (display !== undefined) { + return display; + } + + // Use displaySectionSelector while is not completely deprecated. return !!this.executeFunctionOnEnabled(course.format || '', 'displaySectionSelector', [course]); } @@ -278,9 +301,9 @@ export class CoreCourseFormatDelegateService extends CoreDelegate { + async getCurrentSection(course: CoreCourseAnyCourseData, sections: T[]): Promise { try { - const section = await this.executeFunctionOnEnabled( + const section = await this.executeFunctionOnEnabled( course.format || '', 'getCurrentSection', [course, sections], @@ -293,6 +316,19 @@ export class CoreCourseFormatDelegateService extends CoreDelegate( + course.format || '', + 'getSectionHightlightedName', + ); + } + /** * Get the component to use to display a single section. This component will only be used if the user is viewing * a single section. If all the sections are displayed at once then it won't be used. diff --git a/src/core/features/course/services/handlers/default-format.ts b/src/core/features/course/services/handlers/default-format.ts index d7254f194..38c68d3df 100644 --- a/src/core/features/course/services/handlers/default-format.ts +++ b/src/core/features/course/services/handlers/default-format.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; +import { Translate } from '@singletons'; import { CoreCourseSection } from '../course-helper'; import { CoreCourseFormatHandler } from '../format-delegate'; @@ -65,7 +66,7 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { /** * @inheritdoc */ - displaySectionSelector(): boolean { + displayCourseIndex(): boolean { return true; } @@ -106,6 +107,13 @@ export class CoreCourseFormatDefaultHandler implements CoreCourseFormatHandler { return sections[0]; } + /** + * @inheritdoc + */ + getSectionHightlightedName(): string { + return Translate.instant('core.course.highlighted'); + } + /** * @inheritdoc */ diff --git a/src/core/features/siteplugins/classes/handlers/course-format-handler.ts b/src/core/features/siteplugins/classes/handlers/course-format-handler.ts index 89cfaff07..c6b7914d8 100644 --- a/src/core/features/siteplugins/classes/handlers/course-format-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/course-format-handler.ts @@ -38,8 +38,9 @@ export class CoreSitePluginsCourseFormatHandler extends CoreSitePluginsBaseHandl /** * @inheritdoc */ - displaySectionSelector(): boolean { - return this.handlerSchema.displaysectionselector ?? true; + displayCourseIndex(): boolean { + // Use displaysectionselector while is not completely deprecated. + return this.handlerSchema.displaycourseindex ?? this.handlerSchema.displaysectionselector ?? true; } /** diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 70e85ea13..eded0cd07 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -884,7 +884,11 @@ export type CoreSitePluginsCourseModuleHandlerData = CoreSitePluginsHandlerCommo export type CoreSitePluginsCourseFormatHandlerData = CoreSitePluginsHandlerCommonData & { canviewallsections?: boolean; displayenabledownload?: boolean; + /** + * @deprecated on 4.0, use displaycourseindex instead. + */ displaysectionselector?: boolean; + displaycourseindex?: boolean; }; /** diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 945871287..0dbd3e638 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -1118,7 +1118,7 @@ export class CoreDomUtilsProvider { content.scrollToPoint(position[0], position[1], duration || 0); return true; - } catch (error) { + } catch { return false; } } diff --git a/upgrade.txt b/upgrade.txt index e55234e0d..8b8313511 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -16,6 +16,7 @@ information provided here is intended especially for developers. The user handler function isEnabledForCourse is now called isEnabledForContext and receives a context + contextId instead of a courseId. Some user handler's functions have also changed to accept context + contextId instead of a courseId: isEnabledForUser, getDisplayData, action. - CoreCourseHelperProvider.openCourse parameters changed, now it admits CoreNavigationOptions + siteId on the same object that includes Params passed to page. +- displaySectionSelector has been deprecated on CoreCourseFormatHandler, use displayCourseIndex instead. === 3.9.5 === From 8d59e03b9db9efe6efb910e3c548048ea0259e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 26 Jan 2022 23:09:22 +0100 Subject: [PATCH 6/7] MOBILE-3915 course: Adapt course contents to new prototypes --- .../mod/resource/services/handlers/module.ts | 2 +- .../components/course-index/course-index.ts | 8 +- .../components/format/core-course-format.html | 12 ++- .../course/components/format/format.ts | 13 +++- .../core-course-module-completion.html | 5 +- .../module-info/core-course-module-info.html | 19 +++-- .../module-info/course-module-info.scss | 5 ++ .../core-course-module-manual-completion.html | 8 +- .../core-course-module-navigation.html | 6 +- .../components/module/core-course-module.html | 74 +++++++++++++------ .../course/components/module/module.scss | 7 ++ .../course/components/module/module.ts | 14 +++- .../components/singleactivity.ts | 4 +- .../pages/module-preview/module-preview.html | 42 ++++++----- .../components/course-format/course-format.ts | 4 +- src/theme/theme.base.scss | 5 +- 16 files changed, 150 insertions(+), 78 deletions(-) diff --git a/src/addons/mod/resource/services/handlers/module.ts b/src/addons/mod/resource/services/handlers/module.ts index a889ac9ed..0eaab6861 100644 --- a/src/addons/mod/resource/services/handlers/module.ts +++ b/src/addons/mod/resource/services/handlers/module.ts @@ -95,7 +95,7 @@ export class AddonModResourceModuleHandlerService extends CoreModuleHandlerBase this.getResourceData(module, courseId, handlerData).then((extra) => { handlerData.extraBadge = extra; - handlerData.extraBadgeColor = 'light'; + handlerData.extraBadgeColor = ''; return; }).catch(() => { diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index 87c653fb2..712e87e37 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -14,7 +14,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core'; -import { CoreCourseModuleData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; +import { CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking, @@ -126,7 +126,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { * @param event Event. * @param section Selected section object. */ - selectSection(event: Event, section: CoreCourseSectionWithStatus): void { + selectSection(event: Event, section: CoreCourseSection): void { if (section.uservisible !== false) { ModalController.dismiss({ event, section }); } @@ -139,7 +139,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { * @param section Selected section object. * @param module Selected module object. */ - selectModule(event: Event,section: CoreCourseSectionWithStatus, module: CoreCourseModuleData): void { + selectModule(event: Event,section: CoreCourseSection, module: CoreCourseModuleData): void { if (module.uservisible !== false) { ModalController.dismiss({ event, section, module }); } @@ -147,7 +147,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { } -type CourseIndexSection = Omit & { +type CourseIndexSection = Omit & { highlighted?: boolean; expanded?: boolean; modules: (CoreCourseModuleData & { diff --git a/src/core/features/course/components/format/core-course-format.html b/src/core/features/course/components/format/core-course-format.html index 1b34598a3..3c8036beb 100644 --- a/src/core/features/course/components/format/core-course-format.html +++ b/src/core/features/course/components/format/core-course-format.html @@ -74,19 +74,23 @@

-

- +

+ + {{ 'core.course.hiddenfromstudents' | translate }} - + + {{ 'core.notavailable' | translate }} - + +

+ {{highlighted}}
diff --git a/src/core/features/course/components/format/format.ts b/src/core/features/course/components/format/format.ts index 760637898..25a3cd218 100644 --- a/src/core/features/course/components/format/format.ts +++ b/src/core/features/course/components/format/format.ts @@ -38,7 +38,6 @@ import { CoreCourseModuleData, CoreCourseModuleCompletionData, CoreCourseSection, - CoreCourseSectionWithStatus, } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreEventObserver, CoreEvents } from '@singletons/events'; @@ -47,7 +46,6 @@ import { CoreUtils } from '@services/utils/utils'; import { CoreCourseCourseIndexComponent, CoreCourseIndexSectionWithModule } from '../course-index/course-index'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreNavigator } from '@services/navigator'; -import { database } from 'faker'; import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate'; /** @@ -70,7 +68,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called. @Input() course!: CoreCourseAnyCourseData; // The course to render. - @Input() sections: CoreCourseSectionWithStatus[] = []; // List of course sections. + @Input() sections: CoreCourseSectionToDisplay[] = []; // List of course sections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. @@ -98,6 +96,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID; loaded = false; progress?: number; + highlighted?: string; protected selectTabObserver?: CoreEventObserver; protected completionObserver?: CoreEventObserver; @@ -220,6 +219,10 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { // Format has changed or it's the first time, load all the components. this.lastCourseFormat = this.course.format; + this.highlighted = CoreCourseFormatDelegate.getSectionHightlightedName(this.course); + const currentSection = await CoreCourseFormatDelegate.getCurrentSection(this.course, this.sections); + currentSection.highlighted = true; + await Promise.all([ this.loadCourseFormatComponent(), this.loadCourseSummaryComponent(), @@ -559,3 +562,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { } } + +type CoreCourseSectionToDisplay = CoreCourseSection & { + highlighted?: boolean; +}; diff --git a/src/core/features/course/components/module-completion/core-course-module-completion.html b/src/core/features/course/components/module-completion/core-course-module-completion.html index 571e9f15a..dbdc18616 100644 --- a/src/core/features/course/components/module-completion/core-course-module-completion.html +++ b/src/core/features/course/components/module-completion/core-course-module-completion.html @@ -4,16 +4,19 @@ + {{ 'core.course.completion_automatic:done' | translate }} {{ rule.rulevalue.description }} + {{ 'core.course.completion_automatic:failed' | translate }} {{ rule.rulevalue.description }} - + + {{ 'core.course.completion_automatic:todo' | translate }} {{ rule.rulevalue.description }} diff --git a/src/core/features/course/components/module-info/core-course-module-info.html b/src/core/features/course/components/module-info/core-course-module-info.html index 88060b6ca..864dd98a5 100644 --- a/src/core/features/course/components/module-info/core-course-module-info.html +++ b/src/core/features/course/components/module-info/core-course-module-info.html @@ -19,12 +19,21 @@ - - + + -

- {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} -

+ +
+

+ {{ date.label }} {{ date.timestamp * 1000 | + coreFormatDate:'strftimedatetime' }} +

+
+ + +
diff --git a/src/core/features/course/components/module-info/course-module-info.scss b/src/core/features/course/components/module-info/course-module-info.scss index 514315b48..f540001d2 100644 --- a/src/core/features/course/components/module-info/course-module-info.scss +++ b/src/core/features/course/components/module-info/course-module-info.scss @@ -16,4 +16,9 @@ core-mod-icon { align-self: flex-start; } + + .core-module-dates ion-icon { + @include margin-horizontal(null, 8px); + } + } diff --git a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html index d205a5a65..8f8e5d6e3 100644 --- a/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html +++ b/src/core/features/course/components/module-manual-completion/core-course-module-manual-completion.html @@ -2,22 +2,20 @@ - + {{ 'core.course.completion_manual:done' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} - + {{ 'core.course.completion_manual:markdone' | translate }} diff --git a/src/core/features/course/components/module-navigation/core-course-module-navigation.html b/src/core/features/course/components/module-navigation/core-course-module-navigation.html index 7d0fde92d..497b0e42e 100644 --- a/src/core/features/course/components/module-navigation/core-course-module-navigation.html +++ b/src/core/features/course/components/module-navigation/core-course-module-navigation.html @@ -6,12 +6,10 @@ - + + [moduleId]="currentModule.id" [showManualCompletion]="true" (completionChanged)="completionChanged.emit($event)"> diff --git a/src/core/features/course/components/module/core-course-module.html b/src/core/features/course/components/module/core-course-module.html index 1f5af1171..adfa73f2a 100644 --- a/src/core/features/course/components/module/core-course-module.html +++ b/src/core/features/course/components/module/core-course-module.html @@ -18,25 +18,26 @@ [courseId]="module.course" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">

- - - - - {{ 'core.course.hiddenfromstudents' | translate }} - - - {{ 'core.course.hiddenoncoursepage' | translate }} - -
- {{ 'core.restricted' | translate }} - - -
- - {{ 'core.course.manualcompletionnotsynced' | translate }} - + + + + {{ 'core.course.todo' | translate }} + + + + + {{'core.course.done' | translate }} + + + + {{'core.course.failed' | translate }} + + + + +
+ + +

- {{ date.label }} {{ date.timestamp * 1000 | coreFormatDate:'strftimedatetime' }} + {{ date.label }} {{ date.timestamp * + 1000 | coreFormatDate:'strftimedatetime' }}

@@ -76,9 +82,31 @@ [showManualCompletion]="showManualCompletion" (completionChanged)="completionChanged.emit($event)"> - - + + + {{ 'core.course.manualcompletionnotsynced' | translate }} + + + + + + {{ 'core.course.hiddenfromstudents' | translate }} + + + + + {{ 'core.course.hiddenoncoursepage' | translate }} + + + + + + + + + +
diff --git a/src/core/features/course/components/module/module.scss b/src/core/features/course/components/module/module.scss index 2253562e5..faf96e525 100644 --- a/src/core/features/course/components/module/module.scss +++ b/src/core/features/course/components/module/module.scss @@ -1,3 +1,5 @@ +@import "~theme/globals"; + :host { .item.core-module-main-item { @@ -59,4 +61,9 @@ --inner-border-width: 0px; } + .core-module-availabilityinfo ion-icon, + .core-module-dates ion-icon { + @include margin-horizontal(null, 8px); + } + } diff --git a/src/core/features/course/components/module/module.ts b/src/core/features/course/components/module/module.ts index 936901f5f..287c2a484 100644 --- a/src/core/features/course/components/module/module.ts +++ b/src/core/features/course/components/module/module.ts @@ -20,7 +20,7 @@ import { CoreCourseModuleCompletionData, CoreCourseSection, } from '@features/course/services/course-helper'; -import { CoreCourse } from '@features/course/services/course'; +import { CoreCourse, CoreCourseModuleCompletionStatus, CoreCourseModuleCompletionTracking } from '@features/course/services/course'; import { CoreCourseModuleDelegate, CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate'; /** @@ -47,6 +47,7 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { hasInfo = false; showLegacyCompletion = false; // Whether to show module completion in the old format. showManualCompletion = false; // Whether to show manual completion when completion conditions are disabled. + completionStatus?: CoreCourseModuleCompletionStatus; /** * Component being initialized. @@ -61,13 +62,22 @@ export class CoreCourseModuleComponent implements OnInit, OnDestroy { } this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title; + this.completionStatus = this.module.completiondata === undefined || + this.module.completiondata.tracking == CoreCourseModuleCompletionTracking.COMPLETION_TRACKING_NONE + ? undefined + : this.module.completiondata.state; + this.hasInfo = !!( this.module.description || (this.showActivityDates && this.module.dates && this.module.dates.length) || (this.module.completiondata && ((this.showManualCompletion && !this.module.completiondata.isautomatic) || (this.showCompletionConditions && this.module.completiondata.isautomatic)) - ) + ) || + this.module.completiondata?.offline || + (this.module.visible === 0 && (!this.section || this.section.visible)) || + (this.module.visible !== 0 && this.module.isStealth) || + (this.module.availabilityinfo) ); } diff --git a/src/core/features/course/format/singleactivity/components/singleactivity.ts b/src/core/features/course/format/singleactivity/components/singleactivity.ts index b3d11a9d7..499d7f805 100644 --- a/src/core/features/course/format/singleactivity/components/singleactivity.ts +++ b/src/core/features/course/format/singleactivity/components/singleactivity.ts @@ -19,7 +19,7 @@ import { CoreCourseUnsupportedModuleComponent } from '@features/course/component import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { IonRefresher } from '@ionic/angular'; -import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; +import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreBlockHelper } from '@features/block/services/block-helper'; import { CoreCourse } from '@features/course/services/course'; @@ -36,7 +36,7 @@ import { CoreCourse } from '@features/course/services/course'; export class CoreCourseFormatSingleActivityComponent implements OnChanges { @Input() course?: CoreCourseAnyCourseData; // The course to render. - @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. + @Input() sections?: CoreCourseSection[]; // List of course sections. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. diff --git a/src/core/features/course/pages/module-preview/module-preview.html b/src/core/features/course/pages/module-preview/module-preview.html index 711f8c740..bd5b02506 100644 --- a/src/core/features/course/pages/module-preview/module-preview.html +++ b/src/core/features/course/pages/module-preview/module-preview.html @@ -27,33 +27,35 @@ -
- - - +
+ + +
-
- - {{ 'core.course.hiddenfromstudents' | translate }} - +
+ + + {{ 'core.course.hiddenfromstudents' | translate }} +
-
- - {{ 'core.course.hiddenoncoursepage' | translate }} - +
+ + + {{ 'core.course.hiddenoncoursepage' | translate }} +
-
- {{ 'core.restricted' | translate }} -
+
+ + -
+
-
- - {{ 'core.course.manualcompletionnotsynced' | translate }} - +
+ + {{ 'core.course.manualcompletionnotsynced' | translate }} +
diff --git a/src/core/features/siteplugins/components/course-format/course-format.ts b/src/core/features/siteplugins/components/course-format/course-format.ts index 025c1036e..9fd9e6459 100644 --- a/src/core/features/siteplugins/components/course-format/course-format.ts +++ b/src/core/features/siteplugins/components/course-format/course-format.ts @@ -16,7 +16,7 @@ import { Component, OnChanges, Input, ViewChild, Output, EventEmitter } from '@a import { IonRefresher } from '@ionic/angular'; import { CoreCourseFormatComponent } from '@features/course/components/format/format'; -import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper'; +import { CoreCourseModuleCompletionData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; @@ -33,7 +33,7 @@ import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin- export class CoreSitePluginsCourseFormatComponent implements OnChanges { @Input() course?: CoreCourseAnyCourseData; // The course to render. - @Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component. + @Input() sections?: CoreCourseSection[]; // List of course sections. The status will be calculated in this component. @Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled. @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index cd61d7f89..74a6f739d 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -844,6 +844,8 @@ ion-select-popover ion-item.core-select-option-title { ion-chip { line-height: 1.1; + border-radius: var(--big-radius); + @include padding-horizontal(16px); } ion-searchbar { @@ -1167,9 +1169,8 @@ ion-item.item-input ion-input.has-focus { } } -// Ionic set this value to 0 without px that provoked miscalculations. ion-item-divider { - --inner-padding-end: 0px; + --inner-padding-end: 8px; } // Change default outline. From 27729cbf26d39693e0dffc991d153884d705b718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Fri, 28 Jan 2022 11:34:03 +0100 Subject: [PATCH 7/7] MOBILE-3915 style: Improve radius vars --- src/theme/theme.base.scss | 24 ++++++++++++++---------- src/theme/theme.light.scss | 10 ++++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 74a6f739d..cfd6a58fa 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -382,17 +382,21 @@ ion-alert.core-nohead { } } -ion-alert .alert-wrapper { - overflow: auto; -} +ion-alert { + --border-radius: var(--huge-radius); + .alert-wrapper { + overflow: auto; + border-radius: var(--border-radius) !important; -ion-alert .alert-message { - user-select: text; - flex-shrink: 0; -} + button.alert-button { + color: var(--primary); + } + } -ion-alert .alert-wrapper button.alert-button { - color: var(--primary); + .alert-message { + user-select: text; + flex-shrink: 0; + } } // Ionic list. @@ -844,7 +848,7 @@ ion-select-popover ion-item.core-select-option-title { ion-chip { line-height: 1.1; - border-radius: var(--big-radius); + border-radius: var(--medium-radius); @include padding-horizontal(16px); } diff --git a/src/theme/theme.light.scss b/src/theme/theme.light.scss index ee6036013..6d0ff6d15 100644 --- a/src/theme/theme.light.scss +++ b/src/theme/theme.light.scss @@ -105,7 +105,9 @@ --subdued-text-color: #595959; --small-radius: 4px; - --big-radius: 8px; + --medium-radius: 8px; + --big-radius: 16px; + --huge-radius: 24px; --ion-card-color: var(--text-color); ion-card { @@ -113,7 +115,7 @@ --border-style: solid; --border-color: var(--stroke); --box-shadow: none; - --border-radius: var(--big-radius); + --border-radius: var(--medium-radius); } --text-hightlight-background-color: #{$core-text-hightlight-background-color}; @@ -181,7 +183,7 @@ height: var(--a11y-min-target-size); border: 1px solid var(--stroke); box-shadow: none; - border-radius: var(--big-radius); + border-radius: var(--medium-radius); } } @@ -282,7 +284,7 @@ --core-combobox-color: var(--gray-900); --core-combobox-border-color: var(--stroke); --core-combobox-border-width: 1px; - --core-combobox-radius: var(--big-radius); + --core-combobox-radius: var(--medium-radius); --core-combobox-box-shadow: none; --selected-item-color: var(--primary);