MOBILE-3608 blocks: Starred and recent courses block
parent
b2ecdf5224
commit
d0af70be5a
|
@ -30,8 +30,10 @@ import { AddonBlockMyOverviewModule } from './block/myoverview/myoverview.module
|
||||||
import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module';
|
import { AddonBlockNewsItemsModule } from './block/newsitems/newsitems.module';
|
||||||
import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module';
|
import { AddonBlockOnlineUsersModule } from './block/onlineusers/onlineusers.module';
|
||||||
import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module';
|
import { AddonBlockPrivateFilesModule } from './block/privatefiles/privatefiles.module';
|
||||||
|
import { AddonBlockRecentlyAccessedCoursesModule } from './block/recentlyaccessedcourses/recentlyaccessedcourses.module';
|
||||||
import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module';
|
import { AddonBlockRssClientModule } from './block/rssclient/rssclient.module';
|
||||||
import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module';
|
import { AddonBlockSelfCompletionModule } from './block/selfcompletion/selfcompletion.module';
|
||||||
|
import { AddonBlockStarredCoursesModule } from './block/starredcourses/starredcourses.module';
|
||||||
import { AddonBlockTagsModule } from './block/tags/tags.module';
|
import { AddonBlockTagsModule } from './block/tags/tags.module';
|
||||||
import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
|
import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
|
||||||
import { AddonFilterModule } from './filter/filter.module';
|
import { AddonFilterModule } from './filter/filter.module';
|
||||||
|
@ -57,8 +59,10 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield
|
||||||
AddonBlockNewsItemsModule,
|
AddonBlockNewsItemsModule,
|
||||||
AddonBlockOnlineUsersModule,
|
AddonBlockOnlineUsersModule,
|
||||||
AddonBlockPrivateFilesModule,
|
AddonBlockPrivateFilesModule,
|
||||||
|
AddonBlockRecentlyAccessedCoursesModule,
|
||||||
AddonBlockRssClientModule,
|
AddonBlockRssClientModule,
|
||||||
AddonBlockSelfCompletionModule,
|
AddonBlockSelfCompletionModule,
|
||||||
|
AddonBlockStarredCoursesModule,
|
||||||
AddonBlockTagsModule,
|
AddonBlockTagsModule,
|
||||||
AddonUserProfileFieldModule,
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"nocourses": "No starred courses",
|
||||||
|
"pluginname": "Starred courses"
|
||||||
|
}
|
|
@ -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) {}
|
|
@ -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
|
// @todo
|
||||||
:host-context(.core-horizontal-scroll) {
|
: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 {
|
ion-card {
|
||||||
.core-course-thumb {
|
.core-course-thumb {
|
||||||
padding-top: 30%;
|
padding-top: 30%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.core-course-link {
|
.core-course-header {
|
||||||
/*@include padding(4px, 0px, 4px, 8px);*/
|
padding-top: 4px;
|
||||||
.core-course-additional-info {
|
padding-bottom: 4px;
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.core-course-title {
|
.core-course-title {
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
|
|
||||||
h2 {
|
h2 ion-icon {
|
||||||
font-size: 1.5rem;
|
|
||||||
ion-icon {
|
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.core-course-with-buttons {
|
&.core-course-with-buttons {
|
||||||
max-width: calc(100% - 40px);
|
max-width: calc(100% - 40px);
|
||||||
|
@ -148,7 +155,6 @@
|
||||||
.item-button[icon-only] {
|
.item-button[icon-only] {
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
font-size: 1.5rem;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -277,3 +277,11 @@ ion-select.core-button-select,
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Horizontal scrolling elements
|
||||||
|
.core-horizontal-scroll {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: nowrap;
|
||||||
|
overflow-x: scroll;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue