MOBILE-3608 blocks: Starred and recent courses block
This commit is contained in:
		
							parent
							
								
									b2ecdf5224
								
							
						
					
					
						commit
						d0af70be5a
					
				| @ -30,8 +30,10 @@ import { AddonBlockMyOverviewModule } from './block/myoverview/myoverview.module | ||||
| import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module'; | ||||
| import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module'; | ||||
| import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module'; | ||||
| import { AddonBlockRecentlyAccessedCoursesModule } from './block/recentlyaccessedcourses/recentlyaccessedcourses.module'; | ||||
| import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module'; | ||||
| import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module'; | ||||
| import { AddonBlockStarredCoursesModule } from './block/starredcourses/starredcourses.module'; | ||||
| import { AddonBlockTagsModule } from './block/tags/tags.module'; | ||||
| import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module'; | ||||
| import { AddonFilterModule } from './filter/filter.module'; | ||||
| @ -57,8 +59,10 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield | ||||
|         AddonBlockNewsItemsModule, | ||||
|         AddonBlockOnlineUsersModule, | ||||
|         AddonBlockPrivateFilesModule, | ||||
|         AddonBlockRecentlyAccessedCoursesModule, | ||||
|         AddonBlockRssClientModule, | ||||
|         AddonBlockSelfCompletionModule, | ||||
|         AddonBlockStarredCoursesModule, | ||||
|         AddonBlockTagsModule, | ||||
|         AddonUserProfileFieldModule, | ||||
|     ], | ||||
|  | ||||
| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; | ||||
| 
 | ||||
| import { AddonBlockRecentlyAccessedCoursesComponent } from './recentlyaccessedcourses/recentlyaccessedcourses'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlockRecentlyAccessedCoursesComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CoreCoursesComponentsModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonBlockRecentlyAccessedCoursesComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonBlockRecentlyAccessedCoursesComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonBlockRecentlyAccessedCoursesComponentsModule {} | ||||
| @ -0,0 +1,22 @@ | ||||
| <ion-item-divider> | ||||
|     <ion-label> | ||||
|         <h2>{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}</h2> | ||||
|         </ion-label> | ||||
|     <div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner" slot="end"> | ||||
|         <ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark" (click)="prefetchCourses()"> | ||||
|             <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only"> | ||||
|             </ion-icon> | ||||
|         </ion-button> | ||||
|         <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge">{{prefetchCoursesData.badge}}</ion-badge> | ||||
|         <ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"></ion-spinner> | ||||
|     </div> | ||||
| </ion-item-divider> | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> | ||||
|     <core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_recentlyaccessedcourses.nocourses' | translate"></core-empty-box> | ||||
|     <!-- List of courses. --> | ||||
|      <div class="core-horizontal-scroll"> | ||||
|         <ng-container *ngFor="let course of courses"> | ||||
|             <core-courses-course-progress [course]="course" class="core-recentlyaccessedcourses" [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> | ||||
|         </ng-container> | ||||
|     </div> | ||||
| </core-loading> | ||||
| @ -0,0 +1,237 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; | ||||
| import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; | ||||
| import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; | ||||
| import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; | ||||
| import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a recent courses block. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-block-recentlyaccessedcourses', | ||||
|     templateUrl: 'addon-block-recentlyaccessedcourses.html', | ||||
| }) | ||||
| export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|     @Input() downloadEnabled = false; | ||||
| 
 | ||||
|     courses: CoreEnrolledCourseDataWithOptions [] = []; | ||||
|     prefetchCoursesData: CorePrefetchStatusInfo = { | ||||
|         icon: '', | ||||
|         statusTranslatable: 'core.loading', | ||||
|         status: '', | ||||
|         loading: true, | ||||
|         badge: '', | ||||
|     }; | ||||
| 
 | ||||
|     downloadCourseEnabled = false; | ||||
|     downloadCoursesEnabled = false; | ||||
| 
 | ||||
|     protected prefetchIconsInitialized = false; | ||||
|     protected isDestroyed = false; | ||||
|     protected coursesObserver?: CoreEventObserver; | ||||
|     protected updateSiteObserver?: CoreEventObserver; | ||||
|     protected courseIds = []; | ||||
|     protected fetchContentDefaultError = 'Error getting recent courses data.'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonBlockRecentlyAccessedCoursesComponent'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
| 
 | ||||
|         // Refresh the enabled flags if enabled.
 | ||||
|         this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||
|         this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         // Refresh the enabled flags if site is updated.
 | ||||
|         this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { | ||||
|             this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||
|             this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         }, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|         this.coursesObserver = CoreEvents.on( | ||||
|             CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, | ||||
|             (data: CoreCoursesMyCoursesUpdatedEventData) => { | ||||
| 
 | ||||
|                 if (this.shouldRefreshOnUpdatedEvent(data)) { | ||||
|                     this.refreshCourseList(); | ||||
|                 } | ||||
|             }, | ||||
| 
 | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
| 
 | ||||
|         super.ngOnInit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect changes on input properties. | ||||
|      */ | ||||
|     ngOnChanges(changes: {[name: string]: SimpleChange}): void { | ||||
|         if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { | ||||
|             // Download all courses is enabled now, initialize it.
 | ||||
|             this.initPrefetchCoursesIcons(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async invalidateContent(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(CoreCourses.instance.invalidateUserCourses().finally(() => | ||||
|             // Invalidate course completion data.
 | ||||
|             CoreUtils.instance.allPromises(this.courseIds.map((courseId) => | ||||
|                 AddonCourseCompletion.instance.invalidateCourseCompletion(courseId))))); | ||||
| 
 | ||||
|         promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); | ||||
|         if (this.courseIds.length > 0) { | ||||
|             promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); | ||||
|         } | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises).finally(() => { | ||||
|             this.prefetchIconsInitialized = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the courses for recent courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && | ||||
|             this.block.configsRecord.displaycategories.value == '1'; | ||||
| 
 | ||||
|         this.courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions('lastaccess', 10, undefined, showCategories); | ||||
|         this.initPrefetchCoursesIcons(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the list of courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async refreshCourseList(): Promise<void> { | ||||
|         CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreCourses.instance.invalidateUserCourses(); | ||||
|         } catch (error) { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         await this.loadContent(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the prefetch icon for selected courses. | ||||
|      */ | ||||
|     protected async initPrefetchCoursesIcons(): Promise<void> { | ||||
|         if (this.prefetchIconsInitialized || !this.downloadEnabled) { | ||||
|             // Already initialized.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.prefetchIconsInitialized = true; | ||||
| 
 | ||||
|         this.prefetchCoursesData = await CoreCourseHelper.instance.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. | ||||
|      * | ||||
|      * @param data Event data. | ||||
|      * @return Whether to refresh. | ||||
|      */ | ||||
|     protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { | ||||
|         if (data.action == CoreCoursesProvider.ACTION_ENROL) { | ||||
|             // Always update if user enrolled in a course.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (data.action == CoreCoursesProvider.ACTION_VIEW && data.courseId != CoreSites.instance.getCurrentSiteHomeId() && | ||||
|                 this.courses[0] && data.courseId != this.courses[0].id) { | ||||
|             // Update list if user viewed a course that isn't the most recent one and isn't site home.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE && | ||||
|                 data.courseId && this.hasCourse(data.courseId)) { | ||||
|             // Update list if a visible course is now favourite or unfavourite.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a certain course is in the list of courses. | ||||
|      * | ||||
|      * @param courseId Course ID to search. | ||||
|      * @return Whether it's in the list. | ||||
|      */ | ||||
|     protected hasCourse(courseId: number): boolean { | ||||
|         if (!this.courses) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         return !!this.courses.find((course) => course.id == courseId); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch all the shown courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetchCourses(): Promise<void> { | ||||
|         const initialIcon = this.prefetchCoursesData.icon; | ||||
| 
 | ||||
|         try { | ||||
|             return CoreCourseHelper.instance.prefetchCourses(this.courses, this.prefetchCoursesData); | ||||
|         } catch (error) { | ||||
|             if (!this.isDestroyed) { | ||||
|                 CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); | ||||
|                 this.prefetchCoursesData.icon = initialIcon; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.coursesObserver?.off(); | ||||
|         this.updateSiteObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/addons/block/recentlyaccessedcourses/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/block/recentlyaccessedcourses/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|     "nocourses": "No recent courses", | ||||
|     "pluginname": "Recently accessed courses" | ||||
| } | ||||
| @ -0,0 +1,40 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreBlockDelegate } from '@features/block/services/block-delegate'; | ||||
| import { AddonBlockRecentlyAccessedCoursesComponentsModule } from './components/components.module'; | ||||
| import { AddonBlockRecentlyAccessedCoursesHandler } from './services/block-handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         IonicModule, | ||||
|         CoreComponentsModule, | ||||
|         AddonBlockRecentlyAccessedCoursesComponentsModule, | ||||
|         TranslateModule.forChild(), | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: () => { | ||||
|                 CoreBlockDelegate.instance.registerHandler(AddonBlockRecentlyAccessedCoursesHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class AddonBlockRecentlyAccessedCoursesModule {} | ||||
| @ -0,0 +1,46 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; | ||||
| import { AddonBlockRecentlyAccessedCoursesComponent } from '../components/recentlyaccessedcourses/recentlyaccessedcourses'; | ||||
| import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Block handler. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonBlockRecentlyAccessedCoursesHandlerService extends CoreBlockBaseHandler { | ||||
| 
 | ||||
|     name = 'AddonBlockRecentlyAccessedCourses'; | ||||
|     blockName = 'recentlyaccessedcourses'; | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the data needed to render the block. | ||||
|      * | ||||
|      * @return Data or promise resolved with the data. | ||||
|      */ | ||||
|     getDisplayData(): CoreBlockHandlerData { | ||||
| 
 | ||||
|         return { | ||||
|             title: 'addon.block_recentlyaccessedcourses.pluginname', | ||||
|             class: 'addon-block-recentlyaccessedcourses', | ||||
|             component: AddonBlockRecentlyAccessedCoursesComponent, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonBlockRecentlyAccessedCoursesHandler extends makeSingleton(AddonBlockRecentlyAccessedCoursesHandlerService) {} | ||||
| @ -0,0 +1,45 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { NgModule } from '@angular/core'; | ||||
| import { CommonModule } from '@angular/common'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| 
 | ||||
| import { CoreComponentsModule } from '@components/components.module'; | ||||
| import { CoreDirectivesModule } from '@directives/directives.module'; | ||||
| import { CoreCoursesComponentsModule } from '@features/courses/components/components.module'; | ||||
| 
 | ||||
| import { AddonBlockStarredCoursesComponent } from './starredcourses/starredcourses'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AddonBlockStarredCoursesComponent, | ||||
|     ], | ||||
|     imports: [ | ||||
|         CommonModule, | ||||
|         IonicModule, | ||||
|         TranslateModule.forChild(), | ||||
|         CoreComponentsModule, | ||||
|         CoreDirectivesModule, | ||||
|         CoreCoursesComponentsModule, | ||||
|     ], | ||||
|     exports: [ | ||||
|         AddonBlockStarredCoursesComponent, | ||||
|     ], | ||||
|     entryComponents: [ | ||||
|         AddonBlockStarredCoursesComponent, | ||||
|     ], | ||||
| }) | ||||
| export class AddonBlockStarredCoursesComponentsModule {} | ||||
| @ -0,0 +1,22 @@ | ||||
| <ion-item-divider sticky="true"> | ||||
|     <ion-label> | ||||
|         <h2>{{ 'addon.block_starredcourses.pluginname' | translate }}</h2> | ||||
|     </ion-label> | ||||
|     <div *ngIf="downloadCoursesEnabled && downloadEnabled && courses && courses.length > 1" class="core-button-spinner" slot="end"> | ||||
|         <ion-button *ngIf="prefetchCoursesData.icon && !prefetchCoursesData.loading" fill="clear" color="dark" (click)="prefetchCourses()"> | ||||
|             <ion-icon [name]="prefetchCoursesData.icon" slot="icon-only"> | ||||
|             </ion-icon> | ||||
|         </ion-button> | ||||
|         <ion-badge class="core-course-download-courses-progress" *ngIf="prefetchCoursesData.badge">{{prefetchCoursesData.badge}}</ion-badge> | ||||
|         <ion-spinner *ngIf="!prefetchCoursesData.icon || prefetchCoursesData.loading"></ion-spinner> | ||||
|     </div> | ||||
| </ion-item-divider> | ||||
| <core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page"> | ||||
|     <core-empty-box *ngIf="courses.length == 0" image="assets/img/icons/courses.svg" [message]="'addon.block_starredcourses.nocourses' | translate"></core-empty-box> | ||||
|     <!-- List of courses. --> | ||||
|     <div class="core-horizontal-scroll"> | ||||
|         <ng-container *ngFor="let course of courses"> | ||||
|             <core-courses-course-progress [course]="course" class="core-block_starredcourses" [showDownload]="downloadCourseEnabled && downloadEnabled"></core-courses-course-progress> | ||||
|         </ng-container> | ||||
|     </div> | ||||
| </core-loading> | ||||
| @ -0,0 +1,217 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Component, OnInit, OnDestroy, Input, OnChanges, SimpleChange } from '@angular/core'; | ||||
| import { CoreEventObserver, CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites } from '@services/sites'; | ||||
| import { CoreCoursesProvider, CoreCoursesMyCoursesUpdatedEventData, CoreCourses } from '@features/courses/services/courses'; | ||||
| import { CoreCoursesHelper, CoreEnrolledCourseDataWithOptions } from '@features/courses/services/courses-helper'; | ||||
| import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course-helper'; | ||||
| import { CoreCourseOptionsDelegate } from '@features/course/services/course-options-delegate'; | ||||
| import { AddonCourseCompletion } from '@/addons/coursecompletion/services/coursecompletion'; | ||||
| import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a starred courses block. | ||||
|  */ | ||||
| @Component({ | ||||
|     selector: 'addon-block-starredcourses', | ||||
|     templateUrl: 'addon-block-starredcourses.html', | ||||
| }) | ||||
| export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy { | ||||
| 
 | ||||
|     @Input() downloadEnabled = false; | ||||
| 
 | ||||
|     courses: CoreEnrolledCourseDataWithOptions [] = []; | ||||
|     prefetchCoursesData: CorePrefetchStatusInfo = { | ||||
|         icon: '', | ||||
|         statusTranslatable: 'core.loading', | ||||
|         status: '', | ||||
|         loading: true, | ||||
|         badge: '', | ||||
|     }; | ||||
| 
 | ||||
|     downloadCourseEnabled = false; | ||||
|     downloadCoursesEnabled = false; | ||||
| 
 | ||||
|     protected prefetchIconsInitialized = false; | ||||
|     protected isDestroyed = false; | ||||
|     protected coursesObserver?: CoreEventObserver; | ||||
|     protected updateSiteObserver?: CoreEventObserver; | ||||
|     protected courseIds: number[] = []; | ||||
|     protected fetchContentDefaultError = 'Error getting starred courses data.'; | ||||
| 
 | ||||
|     constructor() { | ||||
|         super('AddonBlockStarredCoursesComponent'); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being initialized. | ||||
|      */ | ||||
|     async ngOnInit(): Promise<void> { | ||||
|         // Refresh the enabled flags if enabled.
 | ||||
|         this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||
|         this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         // Refresh the enabled flags if site is updated.
 | ||||
|         this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => { | ||||
|             this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite(); | ||||
|             this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite(); | ||||
| 
 | ||||
|         }, CoreSites.instance.getCurrentSiteId()); | ||||
| 
 | ||||
|         this.coursesObserver = CoreEvents.on( | ||||
|             CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, | ||||
|             (data: CoreCoursesMyCoursesUpdatedEventData) => { | ||||
| 
 | ||||
|                 if (this.shouldRefreshOnUpdatedEvent(data)) { | ||||
|                     this.refreshCourseList(); | ||||
|                 } | ||||
|                 this.refreshContent(); | ||||
|             }, | ||||
| 
 | ||||
|             CoreSites.instance.getCurrentSiteId(), | ||||
|         ); | ||||
| 
 | ||||
|         super.ngOnInit(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Detect changes on input properties. | ||||
|      */ | ||||
|     ngOnChanges(changes: {[name: string]: SimpleChange}): void { | ||||
|         if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) { | ||||
|             // Download all courses is enabled now, initialize it.
 | ||||
|             this.initPrefetchCoursesIcons(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Perform the invalidate content function. | ||||
|      * | ||||
|      * @return Resolved when done. | ||||
|      */ | ||||
|     protected async invalidateContent(): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         promises.push(CoreCourses.instance.invalidateUserCourses().finally(() => | ||||
|             // Invalidate course completion data.
 | ||||
|             CoreUtils.instance.allPromises(this.courseIds.map((courseId) => | ||||
|                 AddonCourseCompletion.instance.invalidateCourseCompletion(courseId))))); | ||||
| 
 | ||||
|         promises.push(CoreCourseOptionsDelegate.instance.clearAndInvalidateCoursesOptions()); | ||||
|         if (this.courseIds.length > 0) { | ||||
|             promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds.join(','))); | ||||
|         } | ||||
| 
 | ||||
|         await CoreUtils.instance.allPromises(promises).finally(() => { | ||||
|             this.prefetchIconsInitialized = false; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch the courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async fetchContent(): Promise<void> { | ||||
|         const showCategories = this.block.configsRecord && this.block.configsRecord.displaycategories && | ||||
|             this.block.configsRecord.displaycategories.value == '1'; | ||||
| 
 | ||||
|         this.courses = await CoreCoursesHelper.instance.getUserCoursesWithOptions('timemodified', 0, 'isfavourite', showCategories); | ||||
|         this.initPrefetchCoursesIcons(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Refresh the list of courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     protected async refreshCourseList(): Promise<void> { | ||||
|         CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_REFRESHED); | ||||
| 
 | ||||
|         try { | ||||
|             await CoreCourses.instance.invalidateUserCourses(); | ||||
|         } catch (error) { | ||||
|             // Ignore errors.
 | ||||
|         } | ||||
| 
 | ||||
|         await this.loadContent(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Whether list should be refreshed based on a EVENT_MY_COURSES_UPDATED event. | ||||
|      * | ||||
|      * @param data Event data. | ||||
|      * @return Whether to refresh. | ||||
|      */ | ||||
|     protected shouldRefreshOnUpdatedEvent(data: CoreCoursesMyCoursesUpdatedEventData): boolean { | ||||
|         if (data.action == CoreCoursesProvider.ACTION_ENROL) { | ||||
|             // Always update if user enrolled in a course.
 | ||||
|             // New courses shouldn't be favourite by default, but just in case.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         if (data.action == CoreCoursesProvider.ACTION_STATE_CHANGED && data.state == CoreCoursesProvider.STATE_FAVOURITE) { | ||||
|             // Update list when making a course favourite or not.
 | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Initialize the prefetch icon for selected courses. | ||||
|      */ | ||||
|     protected async initPrefetchCoursesIcons(): Promise<void> { | ||||
|         if (this.prefetchIconsInitialized || !this.downloadEnabled) { | ||||
|             // Already initialized.
 | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.prefetchIconsInitialized = true; | ||||
| 
 | ||||
|         this.prefetchCoursesData = await CoreCourseHelper.instance.initPrefetchCoursesIcons(this.courses, this.prefetchCoursesData); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prefetch all the shown courses. | ||||
|      * | ||||
|      * @return Promise resolved when done. | ||||
|      */ | ||||
|     async prefetchCourses(): Promise<void> { | ||||
|         const initialIcon = this.prefetchCoursesData.icon; | ||||
| 
 | ||||
|         try { | ||||
|             return CoreCourseHelper.instance.prefetchCourses(this.courses, this.prefetchCoursesData); | ||||
|         } catch (error) { | ||||
|             if (!this.isDestroyed) { | ||||
|                 CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true); | ||||
|                 this.prefetchCoursesData.icon = initialIcon; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Component being destroyed. | ||||
|      */ | ||||
|     ngOnDestroy(): void { | ||||
|         this.isDestroyed = true; | ||||
|         this.coursesObserver?.off(); | ||||
|         this.updateSiteObserver?.off(); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										4
									
								
								src/addons/block/starredcourses/lang.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/addons/block/starredcourses/lang.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| { | ||||
|     "nocourses": "No starred courses", | ||||
|     "pluginname": "Starred courses" | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/addons/block/starredcourses/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/addons/block/starredcourses/services/block-handler.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreBlockHandlerData } from '@features/block/services/block-delegate'; | ||||
| import { AddonBlockStarredCoursesComponent } from '../components/starredcourses/starredcourses'; | ||||
| import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| 
 | ||||
| /** | ||||
|  * Block handler. | ||||
|  */ | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class AddonBlockStarredCoursesHandlerService extends CoreBlockBaseHandler { | ||||
| 
 | ||||
|     name = 'AddonBlockStarredCourses'; | ||||
|     blockName = 'starredcourses'; | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the data needed to render the block. | ||||
|      * | ||||
|      * @return Data or promise resolved with the data. | ||||
|      */ | ||||
|     getDisplayData(): CoreBlockHandlerData { | ||||
| 
 | ||||
|         return { | ||||
|             title: 'addon.starredcourses.pluginname', | ||||
|             class: 'addon-block-starredcourses', | ||||
|             component: AddonBlockStarredCoursesComponent, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export class AddonBlockStarredCoursesHandler extends makeSingleton(AddonBlockStarredCoursesHandlerService) {} | ||||
							
								
								
									
										38
									
								
								src/addons/block/starredcourses/starredcourses.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/addons/block/starredcourses/starredcourses.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { APP_INITIALIZER, NgModule } from '@angular/core'; | ||||
| import { IonicModule } from '@ionic/angular'; | ||||
| import { TranslateModule } from '@ngx-translate/core'; | ||||
| import { CoreBlockDelegate } from '@features/block/services/block-delegate'; | ||||
| import { AddonBlockStarredCoursesComponentsModule } from './components/components.module'; | ||||
| import { AddonBlockStarredCoursesHandler } from './services/block-handler'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         IonicModule, | ||||
|         AddonBlockStarredCoursesComponentsModule, | ||||
|         TranslateModule.forChild(), | ||||
|     ], | ||||
|     providers: [ | ||||
|         { | ||||
|             provide: APP_INITIALIZER, | ||||
|             multi: true, | ||||
|             useValue: () => { | ||||
|                 CoreBlockDelegate.instance.registerHandler(AddonBlockStarredCoursesHandler.instance); | ||||
|             }, | ||||
|         }, | ||||
|     ], | ||||
| }) | ||||
| export class AddonBlockStarredCoursesModule {} | ||||
| @ -109,28 +109,35 @@ | ||||
| 
 | ||||
| // @todo | ||||
| :host-context(.core-horizontal-scroll) { | ||||
|     /*@include horizontal_scroll_item(80%, 250px, 300px);*/ | ||||
|     flex: 0 0 80%; | ||||
|     min-width: 250px; | ||||
|     max-width: 300px; | ||||
|     align-self: stretch; | ||||
|     display: block; | ||||
| 
 | ||||
|     [text-wrap] .label { | ||||
|         h2, p { | ||||
|             white-space: nowrap; | ||||
|             overflow: hidden; | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     ion-card { | ||||
|         .core-course-thumb { | ||||
|             padding-top: 30%; | ||||
|         } | ||||
| 
 | ||||
|         .core-course-link { | ||||
|             /*@include padding(4px, 0px, 4px, 8px);*/ | ||||
|             .core-course-additional-info { | ||||
|                 font-size: 1.2rem; | ||||
|             } | ||||
|         .core-course-header { | ||||
|             padding-top: 4px; | ||||
|             padding-bottom: 4px; | ||||
| 
 | ||||
|             .core-course-title { | ||||
|                 margin: 3px 0; | ||||
| 
 | ||||
|                 h2 { | ||||
|                     font-size: 1.5rem; | ||||
|                     ion-icon { | ||||
|                 h2 ion-icon { | ||||
|                     margin-right: 2px; | ||||
|                 } | ||||
|                 } | ||||
| 
 | ||||
|                 &.core-course-with-buttons { | ||||
|                     max-width: calc(100% - 40px); | ||||
| @ -148,7 +155,6 @@ | ||||
|             .item-button[icon-only] { | ||||
|                 min-width: 40px; | ||||
|                 width: 40px; | ||||
|                 font-size: 1.5rem; | ||||
|                 padding: 8px; | ||||
|             } | ||||
| 
 | ||||
|  | ||||
| @ -277,3 +277,11 @@ ion-select.core-button-select, | ||||
|     cursor: pointer; | ||||
|     text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| // Horizontal scrolling elements | ||||
| .core-horizontal-scroll { | ||||
|     display: flex; | ||||
|     flex-flow: nowrap; | ||||
|     overflow-x: scroll; | ||||
|     flex-direction: row; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user