diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index e03732e71..fe7e431d5 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -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, ], diff --git a/src/addons/block/recentlyaccessedcourses/components/components.module.ts b/src/addons/block/recentlyaccessedcourses/components/components.module.ts new file mode 100644 index 000000000..560df1e3e --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/components.module.ts @@ -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 {} diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html new file mode 100644 index 000000000..b4fe5b2e1 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/addon-block-recentlyaccessedcourses.html @@ -0,0 +1,22 @@ + + +

{{ 'addon.block_recentlyaccessedcourses.pluginname' | translate }}

+
+
+ + + + + {{prefetchCoursesData.badge}} + +
+
+ + + +
+ + + +
+
diff --git a/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts new file mode 100644 index 000000000..ffb1f3f22 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts @@ -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 { + + // 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 { + const promises: Promise[] = []; + + 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 { + 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 { + 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 { + 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 { + 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(); + } + +} diff --git a/src/addons/block/recentlyaccessedcourses/lang.json b/src/addons/block/recentlyaccessedcourses/lang.json new file mode 100644 index 000000000..e87006df7 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/lang.json @@ -0,0 +1,4 @@ +{ + "nocourses": "No recent courses", + "pluginname": "Recently accessed courses" +} diff --git a/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts b/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts new file mode 100644 index 000000000..aeac5a5df --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/recentlyaccessedcourses.module.ts @@ -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 {} diff --git a/src/addons/block/recentlyaccessedcourses/services/block-handler.ts b/src/addons/block/recentlyaccessedcourses/services/block-handler.ts new file mode 100644 index 000000000..a104adf90 --- /dev/null +++ b/src/addons/block/recentlyaccessedcourses/services/block-handler.ts @@ -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) {} diff --git a/src/addons/block/starredcourses/components/components.module.ts b/src/addons/block/starredcourses/components/components.module.ts new file mode 100644 index 000000000..8dc0291d0 --- /dev/null +++ b/src/addons/block/starredcourses/components/components.module.ts @@ -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 {} diff --git a/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html new file mode 100644 index 000000000..09db419a6 --- /dev/null +++ b/src/addons/block/starredcourses/components/starredcourses/addon-block-starredcourses.html @@ -0,0 +1,22 @@ + + +

{{ 'addon.block_starredcourses.pluginname' | translate }}

+
+
+ + + + + {{prefetchCoursesData.badge}} + +
+
+ + + +
+ + + +
+
diff --git a/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts new file mode 100644 index 000000000..d7a848b96 --- /dev/null +++ b/src/addons/block/starredcourses/components/starredcourses/starredcourses.ts @@ -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 { + // 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 { + const promises: Promise[] = []; + + 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 { + 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 { + 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 { + 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 { + 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(); + } + +} diff --git a/src/addons/block/starredcourses/lang.json b/src/addons/block/starredcourses/lang.json new file mode 100644 index 000000000..cc3dd03c7 --- /dev/null +++ b/src/addons/block/starredcourses/lang.json @@ -0,0 +1,4 @@ +{ + "nocourses": "No starred courses", + "pluginname": "Starred courses" +} diff --git a/src/addons/block/starredcourses/services/block-handler.ts b/src/addons/block/starredcourses/services/block-handler.ts new file mode 100644 index 000000000..b220d9be3 --- /dev/null +++ b/src/addons/block/starredcourses/services/block-handler.ts @@ -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) {} diff --git a/src/addons/block/starredcourses/starredcourses.module.ts b/src/addons/block/starredcourses/starredcourses.module.ts new file mode 100644 index 000000000..20dfba809 --- /dev/null +++ b/src/addons/block/starredcourses/starredcourses.module.ts @@ -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 {} diff --git a/src/core/features/courses/components/course-progress/course-progress.scss b/src/core/features/courses/components/course-progress/course-progress.scss index 67b5b9470..50dc956eb 100644 --- a/src/core/features/courses/components/course-progress/course-progress.scss +++ b/src/core/features/courses/components/course-progress/course-progress.scss @@ -109,27 +109,34 @@ // @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 { - margin-right: 2px; - } + h2 ion-icon { + margin-right: 2px; } &.core-course-with-buttons { @@ -148,7 +155,6 @@ .item-button[icon-only] { min-width: 40px; width: 40px; - font-size: 1.5rem; padding: 8px; } diff --git a/src/theme/app.scss b/src/theme/app.scss index a3f55ee7a..e88fd2d8c 100644 --- a/src/theme/app.scss +++ b/src/theme/app.scss @@ -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; +}