MOBILE-3594 sitehome: Course listing components of sitehome

main
Pau Ferrer Ocaña 2020-11-20 12:03:44 +01:00
parent 97d1c93399
commit 0d66e66fdd
14 changed files with 774 additions and 5 deletions

View File

@ -0,0 +1,44 @@
// (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 { CorePipesModule } from '@pipes/pipes.module';
import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
@NgModule({
declarations: [
CoreCoursesCourseListItemComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
],
providers: [
],
exports: [
CoreCoursesCourseListItemComponent,
],
})
export class CoreCoursesComponentsModule {}

View File

@ -0,0 +1,14 @@
<ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
[title]="course.displayname || course.fullname" detail>
<ion-icon name="fas-graduation-cap" slot="start"></ion-icon>
<ion-label>
<h2>
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
</core-format-text>
</h2>
</ion-label>
<ng-container *ngIf="!isEnrolled">
<ion-icon *ngFor="let icon of icons" color="dark" size="small"
[name]="icon.icon" [attr.aria-label]="icon.label | translate" slot="end"></ion-icon>
</ng-container>
</ion-item>

View File

@ -0,0 +1,105 @@
// (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, Input, OnInit } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreCourseHelper } from '@features/course/services/course.helper';
import { CoreCourses, CoreCourseSearchedData } from '@features/courses/services/courses';
/**
* This directive is meant to display an item for a list of courses.
*
* Example usage:
*
* <core-courses-course-list-item [course]="course"></core-courses-course-list-item>
*/
@Component({
selector: 'core-courses-course-list-item',
templateUrl: 'core-courses-course-list-item.html',
})
export class CoreCoursesCourseListItemComponent implements OnInit {
@Input() course!: CoreCourseSearchedData; // The course to render.
icons: CoreCoursesEnrolmentIcons[] = [];
isEnrolled = false;
constructor(
protected navCtrl: NavController,
) {
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
// Check if the user is enrolled in the course.
try {
await CoreCourses.instance.getUserCourse(this.course.id);
this.isEnrolled = true;
} catch {
this.isEnrolled = false;
this.icons = [];
this.course.enrollmentmethods.forEach((instance) => {
if (instance === 'self') {
this.icons.push({
label: 'core.courses.selfenrolment',
icon: 'fas-key',
});
} else if (instance === 'guest') {
this.icons.push({
label: 'core.courses.allowguests',
icon: 'fas-unlock',
});
} else if (instance === 'paypal') {
this.icons.push({
label: 'core.courses.paypalaccepted',
icon: 'fab-paypal',
});
}
});
if (this.icons.length == 0) {
this.icons.push({
label: 'core.courses.notenrollable',
icon: 'fas-lock',
});
}
}
}
/**
* Open a course.
*
* @param course The course to open.
*/
openCourse(): void {
if (this.isEnrolled) {
CoreCourseHelper.instance.openCourse(this.course);
} else {
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
}
}
}
/**
* Enrolment icons to show on the list with a label.
*/
export type CoreCoursesEnrolmentIcons = {
label: string;
icon: string;
};

View File

@ -13,12 +13,12 @@
// limitations under the License.
import { NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { RouterModule, Routes } from '@angular/router';
import { CoreHomeRoutingModule } from '../mainmenu/pages/home/home-routing.module';
import { CoreHomeDelegate } from '../mainmenu/services/home.delegate';
import { CoreDashboardHomeHandler } from './services/handlers/dashboard.home';
const routes: Routes = [
const homeRoutes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
@ -26,9 +26,50 @@ const routes: Routes = [
},
];
const routes: Routes = [
{
path: 'courses',
children: [
{
path: '',
redirectTo: 'all',
pathMatch: 'full',
},
{
path: 'categories',
redirectTo: 'categories/root', // Fake "id".
pathMatch: 'full',
},
{
path: 'categories/:id',
loadChildren: () =>
import('@features/courses/pages/categories/categories.page.module').then(m => m.CoreCoursesCategoriesPageModule),
},
{
path: 'all',
loadChildren: () =>
import('@features/courses/pages/available-courses/available-courses.page.module')
.then(m => m.CoreCoursesAvailableCoursesPageModule),
},
{
path: 'search',
loadChildren: () =>
import('@features/courses/pages/search/search.page.module')
.then(m => m.CoreCoursesSearchPageModule),
},
],
},
];
@NgModule({
imports: [CoreHomeRoutingModule.forChild(routes)],
exports: [CoreHomeRoutingModule],
imports: [
CoreHomeRoutingModule.forChild(homeRoutes),
RouterModule.forChild(routes),
],
exports: [
CoreHomeRoutingModule,
RouterModule,
],
providers: [
CoreDashboardHomeHandler,
],

View File

@ -0,0 +1,19 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.courses.availablecourses' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="coursesLoaded">
<ng-container *ngIf="courses.length > 0">
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
</ng-container>
<core-empty-box *ngIf="!courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,50 @@
// (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 { RouterModule, Routes } from '@angular/router';
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 '../../components/components.module';
import { CoreCoursesAvailableCoursesPage } from './available-courses.page';
const routes: Routes = [
{
path: '',
component: CoreCoursesAvailableCoursesPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule,
],
declarations: [
CoreCoursesAvailableCoursesPage,
],
exports: [RouterModule],
})
export class CoreCoursesAvailableCoursesPageModule { }

View File

@ -0,0 +1,78 @@
// (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 } from '@angular/core';
import { IonRefresher } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
/**
* Page that displays available courses in current site.
*/
@Component({
selector: 'page-core-courses-available-courses',
templateUrl: 'available-courses.html',
})
export class CoreCoursesAvailableCoursesPage implements OnInit {
courses: CoreCourseSearchedData[] = [];
coursesLoaded = false;
/**
* View loaded.
*/
ngOnInit(): void {
this.loadCourses().finally(() => {
this.coursesLoaded = true;
});
}
/**
* Load the courses.
*
* @return Promise resolved when done.
*/
protected async loadCourses(): Promise<void> {
const frontpageCourseId = CoreSites.instance.getCurrentSite()!.getSiteHomeId();
try {
const courses = await CoreCourses.instance.getCoursesByField();
this.courses = courses.filter((course) => course.id != frontpageCourseId);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
/**
* Refresh the courses.
*
* @param refresher Refresher.
*/
refreshCourses(refresher: CustomEvent<IonRefresher>): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.instance.invalidateUserCourses());
promises.push(CoreCourses.instance.invalidateCoursesByField());
Promise.all(promises).finally(() => {
this.loadCourses().finally(() => {
refresher?.detail.complete();
});
});
}
}

View File

@ -0,0 +1,67 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="coursecat" [contextInstanceId]="currentCategory && currentCategory!.id">
</core-format-text>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!categoriesLoaded" (ionRefresh)="refreshCategories($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="categoriesLoaded">
<ion-item *ngIf="currentCategory" class="ion-text-wrap">
<ion-icon name="fas-folder" slot="start"></ion-icon>
<ion-label>
<h2>
<core-format-text [text]="currentCategory!.name" contextLevel="coursecat"
[contextInstanceId]="currentCategory!.id"></core-format-text>
</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="currentCategory && currentCategory!.description">
<ion-label>
<h2>
<core-format-text [text]="currentCategory!.description" maxHeight="60" contextLevel="coursecat"
[contextInstanceId]="currentCategory!.id"></core-format-text>
</h2>
</ion-label>
</ion-item>
<div *ngIf="categories.length > 0">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.courses.categories' | translate }}</h2>
</ion-label>
</ion-item-divider>
<section *ngFor="let category of categories">
<ion-item class="ion-text-wrap" router-direction="forward" [routerLink]="['/courses/categories', category.id]"
[title]="category.name" detail>
<ion-icon name="fas-folder" slot="start"></ion-icon>
<ion-label>
<h2>
<core-format-text [text]="category.name" contextLevel="coursecat" [contextInstanceId]="category.id">
</core-format-text>
</h2>
</ion-label>
<ion-badge slot="end" *ngIf="category.coursecount > 0" color="light">{{category.coursecount}}</ion-badge>
</ion-item>
</section>
</div>
<div *ngIf="courses.length > 0">
<ion-item-divider>
<ion-label>
<h2>{{ 'core.courses.courses' | translate }}</h2>
</ion-label>
</ion-item-divider>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
</div>
<core-empty-box *ngIf="!categories.length && !courses.length" icon="ionic" [message]="'core.courses.nocoursesyet' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,50 @@
// (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 { RouterModule, Routes } from '@angular/router';
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 '../../components/components.module';
import { CoreCoursesCategoriesPage } from './categories.page';
const routes: Routes = [
{
path: '',
component: CoreCoursesCategoriesPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule,
],
declarations: [
CoreCoursesCategoriesPage,
],
exports: [RouterModule],
})
export class CoreCoursesCategoriesPageModule { }

View File

@ -0,0 +1,123 @@
// (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 } from '@angular/core';
import { IonRefresher, NavController } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreCategoryData, CoreCourses, CoreCourseSearchedData } from '../../services/courses';
import { Translate } from '@singletons/core.singletons';
import { ActivatedRoute } from '@angular/router';
/**
* Page that displays a list of categories and the courses in the current category if any.
*/
@Component({
selector: 'page-core-courses-categories',
templateUrl: 'categories.html',
})
export class CoreCoursesCategoriesPage implements OnInit {
title: string;
currentCategory?: CoreCategoryData;
categories: CoreCategoryData[] = [];
courses: CoreCourseSearchedData[] = [];
categoriesLoaded = false;
protected categoryId = 0;
constructor(
protected navCtrl: NavController,
protected route: ActivatedRoute,
) {
this.title = Translate.instance.instant('core.courses.categories');
}
/**
* View loaded.
*/
ngOnInit(): void {
this.categoryId = parseInt(this.route.snapshot.params['id'], 0) || 0;
this.fetchCategories().finally(() => {
this.categoriesLoaded = true;
});
}
/**
* Fetch the categories.
*
* @return Promise resolved when done.
*/
protected async fetchCategories(): Promise<void> {
try{
const categories: CoreCategoryData[] = await CoreCourses.instance.getCategories(this.categoryId, true);
this.currentCategory = undefined;
const index = categories.findIndex((category) => category.id == this.categoryId);
if (index >= 0) {
this.currentCategory = categories[index];
// Delete current Category to avoid problems with the formatTree.
delete categories[index];
}
// Sort by depth and sortorder to avoid problems formatting Tree.
categories.sort((a, b) => {
if (a.depth == b.depth) {
return (a.sortorder > b.sortorder) ? 1 : ((b.sortorder > a.sortorder) ? -1 : 0);
}
return a.depth > b.depth ? 1 : -1;
});
this.categories = CoreUtils.instance.formatTree(categories, 'parent', 'id', this.categoryId);
if (this.currentCategory) {
this.title = this.currentCategory.name;
try {
this.courses = await CoreCourses.instance.getCoursesByField('category', this.categoryId);
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
}
}
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcategories', true);
}
}
/**
* Refresh the categories.
*
* @param refresher Refresher.
*/
refreshCategories(refresher?: CustomEvent<IonRefresher>): void {
const promises: Promise<void>[] = [];
promises.push(CoreCourses.instance.invalidateUserCourses());
promises.push(CoreCourses.instance.invalidateCategories(this.categoryId, true));
promises.push(CoreCourses.instance.invalidateCoursesByField('category', this.categoryId));
promises.push(CoreSites.instance.getCurrentSite()!.invalidateConfig());
Promise.all(promises).finally(() => {
this.fetchCategories().finally(() => {
refresher?.detail.complete();
});
});
}
}

View File

@ -0,0 +1,24 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>{{ 'core.courses.searchcourses' | translate }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)"
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
searchArea="CoreCoursesSearch"></core-search-box>
<ng-container *ngIf="total > 0">
<ion-item-divider>
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}</h2></ion-label>
</ion-item-divider>
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreResults($event)" [error]="loadMoreError">
</core-infinite-loading>
</ng-container>
<core-empty-box *ngIf="total == 0" icon="search" [message]="'core.courses.nosearchresults' | translate"></core-empty-box>
</ion-content>

View File

@ -0,0 +1,52 @@
// (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 { RouterModule, Routes } from '@angular/router';
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 '../../components/components.module';
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
import { CoreCoursesSearchPage } from './search.page';
const routes: Routes = [
{
path: '',
component: CoreCoursesSearchPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreCoursesComponentsModule,
CoreSearchComponentsModule,
],
declarations: [
CoreCoursesSearchPage,
],
exports: [RouterModule],
})
export class CoreCoursesSearchPageModule { }

View File

@ -0,0 +1,100 @@
// (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 } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
/**
* Page that allows searching for courses.
*/
@Component({
selector: 'page-core-courses-search',
templateUrl: 'search.html',
})
export class CoreCoursesSearchPage {
total = 0;
courses: CoreCourseBasicSearchedData[] = [];
canLoadMore = false;
loadMoreError = false;
protected page = 0;
protected currentSearch = '';
/**
* Search a new text.
*
* @param text The text to search.
*/
async search(text: string): Promise<void> {
this.currentSearch = text;
this.courses = [];
this.page = 0;
this.total = 0;
const modal = await CoreDomUtils.instance.showModalLoading('core.searching', true);
this.searchCourses().finally(() => {
modal.dismiss();
});
}
/**
* Clear search box.
*/
clearSearch(): void {
this.currentSearch = '';
this.courses = [];
this.page = 0;
this.total = 0;
}
/**
* Load more results.
*
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
*/
loadMoreResults(infiniteComplete?: () => void ): void {
this.searchCourses().finally(() => {
infiniteComplete && infiniteComplete();
});
}
/**
* Search courses or load the next page of current search.
*
* @return Promise resolved when done.
*/
protected async searchCourses(): Promise<void> {
this.loadMoreError = false;
try {
const response = await CoreCourses.instance.search(this.currentSearch, this.page);
if (this.page === 0) {
this.courses = response.courses;
} else {
this.courses = this.courses.concat(response.courses);
}
this.total = response.total;
this.page++;
this.canLoadMore = this.courses.length < this.total;
} catch (error) {
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorsearching', true);
}
}
}

View File

@ -39,7 +39,9 @@
</ng-container>
</ng-container>
</ion-list>
<core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate"></core-empty-box>
<core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate">
</core-empty-box>
</core-loading>
<!-- @todo </core-block-course-blocks> -->
</ion-content>