|
@ -201,6 +201,7 @@ const appConfig = {
|
||||||
'no-duplicate-imports': 'error',
|
'no-duplicate-imports': 'error',
|
||||||
'no-empty': 'error',
|
'no-empty': 'error',
|
||||||
'no-eval': 'error',
|
'no-eval': 'error',
|
||||||
|
'no-fallthrough': 'off',
|
||||||
'no-invalid-this': 'error',
|
'no-invalid-this': 'error',
|
||||||
'no-irregular-whitespace': 'error',
|
'no-irregular-whitespace': 'error',
|
||||||
'no-multiple-empty-lines': 'error',
|
'no-multiple-empty-lines': 'error',
|
||||||
|
|
|
@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites';
|
||||||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||||
import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges';
|
import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of calendar events.
|
* Page that displays the list of calendar events.
|
||||||
|
@ -42,18 +42,13 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
||||||
badgeLoaded = false;
|
badgeLoaded = false;
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||||
this.userId = this.route.snapshot.queryParams['userId'] ||
|
this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSite()!.getUserId();
|
||||||
CoreSites.instance.getCurrentSite()?.getUserId();
|
this.badgeHash = CoreNavigator.instance.getRouteParam('badgeHash') || '';
|
||||||
this.badgeHash = this.route.snapshot.queryParams['badgeHash'];
|
|
||||||
|
|
||||||
this.fetchIssuedBadge().finally(() => {
|
this.fetchIssuedBadge().finally(() => {
|
||||||
this.badgeLoaded = true;
|
this.badgeLoaded = true;
|
||||||
|
|
|
@ -20,7 +20,6 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,18 +41,13 @@ export class AddonBadgesUserBadgesPage implements OnInit {
|
||||||
currentTime = 0;
|
currentTime = 0;
|
||||||
badgeHash!: string;
|
badgeHash!: string;
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||||
this.userId = this.route.snapshot.queryParams['userId'] ||
|
this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSite()!.getUserId();
|
||||||
CoreSites.instance.getCurrentSite()?.getUserId();
|
|
||||||
|
|
||||||
this.fetchBadges().finally(() => {
|
this.fetchBadges().finally(() => {
|
||||||
// @todo splitview
|
// @todo splitview
|
||||||
|
|
|
@ -14,10 +14,10 @@
|
||||||
|
|
||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
|
import { CoreSiteHome, FrontPageItemNames } from '@features/sitehome/services/sitehome';
|
||||||
// @todo import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||||
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
import { CoreBlockBaseComponent } from '@features/block/classes/base-block-component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +63,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
||||||
|
|
||||||
if (this.mainMenuBlock && this.mainMenuBlock.modules) {
|
if (this.mainMenuBlock && this.mainMenuBlock.modules) {
|
||||||
// Invalidate modules prefetch data.
|
// Invalidate modules prefetch data.
|
||||||
// @todo promises.push(this.prefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
|
promises.push(CoreCourseModulePrefetchDelegate.instance.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
@ -77,8 +77,8 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
||||||
protected async fetchContent(): Promise<void> {
|
protected async fetchContent(): Promise<void> {
|
||||||
const sections = await CoreCourse.instance.getSections(this.siteHomeId, false, true);
|
const sections = await CoreCourse.instance.getSections(this.siteHomeId, false, true);
|
||||||
|
|
||||||
this.mainMenuBlock = sections.find((section) => section.section == 0);
|
const mainMenuBlock = sections.find((section) => section.section == 0);
|
||||||
if (!this.mainMenuBlock) {
|
if (!mainMenuBlock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,10 +91,17 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
||||||
const items = config.frontpageloggedin.split(',');
|
const items = config.frontpageloggedin.split(',');
|
||||||
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
|
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
|
||||||
|
|
||||||
const hasContent = CoreCourseHelper.instance.sectionHasContent(this.mainMenuBlock);
|
const result = await CoreCourseHelper.instance.addHandlerDataForModules(
|
||||||
CoreCourseHelper.instance.addHandlerDataForModules([this.mainMenuBlock], this.siteHomeId, undefined, undefined, true);
|
[mainMenuBlock],
|
||||||
|
this.siteHomeId,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
if (!hasNewsItem || !hasContent) {
|
this.mainMenuBlock = result.sections[0];
|
||||||
|
|
||||||
|
if (!hasNewsItem || !this.mainMenuBlock.hasContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,9 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
.core-module-icon[slot="start"] {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host-context([dir=rtl]) {
|
:host-context([dir=rtl]) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { AddonCalendarFilterPopoverComponent } from '../../components/filter/fil
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Network, NgZone } from '@singletons';
|
import { Network, NgZone } from '@singletons';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
@ -101,7 +101,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
|
||||||
private popoverCtrl: PopoverController,
|
private popoverCtrl: PopoverController,
|
||||||
) {
|
) {
|
||||||
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
@ -235,19 +234,18 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
|
CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
|
||||||
const value = AddonCalendarEventType[name];
|
const value = AddonCalendarEventType[name];
|
||||||
const filter = this.route.snapshot.queryParams[name];
|
this.filter[name] = CoreNavigator.instance.getRouteBooleanParam(name) ?? true;
|
||||||
this.filter[name] = typeof filter == 'undefined' ? true : filter;
|
|
||||||
types.push(value);
|
types.push(value);
|
||||||
});
|
});
|
||||||
this.filter.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || -1;
|
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||||
this.filter.categoryId = parseInt(this.route.snapshot.queryParams['categoryId'], 10) || undefined;
|
this.filter.categoryId = CoreNavigator.instance.getRouteNumberParam('categoryId');
|
||||||
|
|
||||||
this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]);
|
this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
this.year = this.route.snapshot.queryParams['year'] || now.getFullYear();
|
this.year = CoreNavigator.instance.getRouteNumberParam('year') || now.getFullYear();
|
||||||
this.month = this.route.snapshot.queryParams['month'] || (now.getMonth() + 1);
|
this.month = CoreNavigator.instance.getRouteNumberParam('month') || (now.getMonth() + 1);
|
||||||
this.day = this.route.snapshot.queryParams['day'] || now.getDate();
|
this.day = CoreNavigator.instance.getRouteNumberParam('day') || now.getDate();
|
||||||
|
|
||||||
this.calculateCurrentMoment();
|
this.calculateCurrentMoment();
|
||||||
this.calculateIsCurrentDay();
|
this.calculateIsCurrentDay();
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
|
import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||||
import { IonRefresher, NavController } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreGroup, CoreGroups } from '@services/groups';
|
import { CoreGroup, CoreGroups } from '@services/groups';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -40,9 +40,9 @@ import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/cal
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calendar-offline';
|
import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calendar-offline';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a form to create/edit an event.
|
* Page that displays a form to create/edit an event.
|
||||||
|
@ -90,8 +90,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
|
||||||
protected gotEventData = false;
|
protected gotEventData = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected navCtrl: NavController,
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
protected fb: FormBuilder,
|
protected fb: FormBuilder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -128,11 +126,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.eventId = this.route.snapshot.queryParams['eventId'];
|
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || 0;
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || 0;
|
||||||
this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent';
|
this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent';
|
||||||
|
|
||||||
const timestamp = parseInt(this.route.snapshot.queryParams['timestamp'], 10);
|
const timestamp = CoreNavigator.instance.getRouteNumberParam('timestamp');
|
||||||
const currentDate = CoreTimeUtils.instance.toDatetimeFormat(timestamp);
|
const currentDate = CoreTimeUtils.instance.toDatetimeFormat(timestamp);
|
||||||
this.form.addControl('timestart', this.fb.control(currentDate, Validators.required));
|
this.form.addControl('timestart', this.fb.control(currentDate, Validators.required));
|
||||||
this.form.addControl('timedurationuntil', this.fb.control(currentDate));
|
this.form.addControl('timedurationuntil', this.fb.control(currentDate));
|
||||||
|
@ -578,7 +576,7 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
|
||||||
this.originalData = CoreUtils.instance.clone(this.form.value);
|
this.originalData = CoreUtils.instance.clone(this.form.value);
|
||||||
} else {*/
|
} else {*/
|
||||||
this.originalData = undefined; // Avoid asking for confirmation.
|
this.originalData = undefined; // Avoid asking for confirmation.
|
||||||
this.navCtrl.pop();
|
CoreNavigator.instance.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -43,7 +43,6 @@ import { Subscription } from 'rxjs';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { AddonCalendarReminderDBRecord } from '../../services/database/calendar';
|
import { AddonCalendarReminderDBRecord } from '../../services/database/calendar';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays a single calendar event.
|
* Page that displays a single calendar event.
|
||||||
|
@ -86,11 +85,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
syncIcon = 'spinner'; // Sync icon.
|
syncIcon = 'spinner'; // Sync icon.
|
||||||
isSplitViewOn = false;
|
isSplitViewOn = false;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
protected route: ActivatedRoute,
|
|
||||||
// @Optional() private svComponent: CoreSplitViewComponent,
|
|
||||||
) {
|
|
||||||
|
|
||||||
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
||||||
this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
|
this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
|
||||||
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
@ -150,8 +145,15 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.eventId = this.route.snapshot.queryParams['id'];
|
const eventId = CoreNavigator.instance.getRouteNumberParam('id');
|
||||||
|
if (!eventId) {
|
||||||
|
CoreDomUtils.instance.showErrorModal('Event ID not supplied.');
|
||||||
|
CoreNavigator.instance.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.eventId = eventId;
|
||||||
this.syncIcon = 'spinner';
|
this.syncIcon = 'spinner';
|
||||||
|
|
||||||
this.fetchEvent();
|
this.fetchEvent();
|
||||||
|
|
|
@ -168,12 +168,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
||||||
|
|
||||||
this.route.queryParams.subscribe(params => {
|
this.route.queryParams.subscribe(() => {
|
||||||
this.eventId = parseInt(params['eventId'], 10) || undefined;
|
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||||
this.filter.courseId = parseInt(params['courseId'], 10) || -1;
|
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||||
this.year = parseInt(params['year'], 10) || undefined;
|
this.year = CoreNavigator.instance.getRouteNumberParam('year');
|
||||||
this.month = parseInt(params['month'], 10) || undefined;
|
this.month = CoreNavigator.instance.getRouteNumberParam('month');
|
||||||
this.loadUpcoming = !!params['upcoming'];
|
this.loadUpcoming = !!CoreNavigator.instance.getRouteBooleanParam('upcoming');
|
||||||
this.showCalendar = !this.loadUpcoming;
|
this.showCalendar = !this.loadUpcoming;
|
||||||
this.filter.filtered = this.filter.courseId > 0;
|
this.filter.filtered = this.filter.courseId > 0;
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { CoreApp } from '@services/app';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
|
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
|
||||||
import { ActivatedRoute, Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { Network, NgZone } from '@singletons';
|
import { Network, NgZone } from '@singletons';
|
||||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
|
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
|
||||||
|
@ -102,7 +102,6 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
|
||||||
private popoverCtrl: PopoverController,
|
private popoverCtrl: PopoverController,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -248,8 +247,8 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
|
||||||
* View loaded.
|
* View loaded.
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
this.eventId = this.route.snapshot.queryParams['eventId'] || undefined;
|
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||||
this.filter.courseId = this.route.snapshot.queryParams['courseId'];
|
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||||
|
|
||||||
if (this.eventId) {
|
if (this.eventId) {
|
||||||
// There is an event to load, open the event in a new state.
|
// There is an event to load, open the event in a new state.
|
||||||
|
|
|
@ -61,7 +61,6 @@ export class AddonFilterMediaPluginHandlerService extends CoreFilterDefaultHandl
|
||||||
* Treat video filters. Currently only treating youtube video using video JS.
|
* Treat video filters. Currently only treating youtube video using video JS.
|
||||||
*
|
*
|
||||||
* @param el Video element.
|
* @param el Video element.
|
||||||
* @param navCtrl NavController to use.
|
|
||||||
*/
|
*/
|
||||||
protected treatVideoFilters(video: HTMLElement): void {
|
protected treatVideoFilters(video: HTMLElement): void {
|
||||||
// Treat Video JS Youtube video links and translate them to iframes.
|
// Treat Video JS Youtube video links and translate them to iframes.
|
||||||
|
|
|
@ -74,7 +74,6 @@ export class AddonNotificationsActionsComponent implements OnInit {
|
||||||
* Default action. Open in browser.
|
* Default action. Open in browser.
|
||||||
*
|
*
|
||||||
* @param siteId Site ID to use.
|
* @param siteId Site ID to use.
|
||||||
* @param navCtrl NavController.
|
|
||||||
*/
|
*/
|
||||||
protected async openInBrowser(siteId?: string): Promise<void> {
|
protected async openInBrowser(siteId?: string): Promise<void> {
|
||||||
const url = <string> this.data?.appurl || this.contextUrl;
|
const url = <string> this.data?.appurl || this.contextUrl;
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { IonRefresher, NavController } from '@ionic/angular';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreConfig } from '@services/config';
|
import { CoreConfig } from '@services/config';
|
||||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||||
|
@ -61,10 +61,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected updateTimeout?: number;
|
protected updateTimeout?: number;
|
||||||
|
|
||||||
constructor(
|
constructor() { // @todo @Optional() protected svComponent: CoreSplitViewComponent,
|
||||||
protected navCtrl: NavController,
|
|
||||||
// @Optional() protected svComponent: CoreSplitViewComponent,
|
|
||||||
) {
|
|
||||||
this.notifPrefsEnabled = AddonNotifications.instance.isNotificationPreferencesEnabled();
|
this.notifPrefsEnabled = AddonNotifications.instance.isNotificationPreferencesEnabled();
|
||||||
this.canChangeSound = CoreLocalNotifications.instance.canDisableSound();
|
this.canChangeSound = CoreLocalNotifications.instance.canDisableSound();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { IonRefresher } from '@ionic/angular';
|
||||||
import { IonRefresher, NavController } from '@ionic/angular';
|
|
||||||
import { Md5 } from 'ts-md5/dist/md5';
|
import { Md5 } from 'ts-md5/dist/md5';
|
||||||
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
|
@ -32,6 +31,7 @@ import {
|
||||||
} from '@/addons/privatefiles/services/privatefiles';
|
} from '@/addons/privatefiles/services/privatefiles';
|
||||||
import { AddonPrivateFilesHelper } from '@/addons/privatefiles/services/privatefiles-helper';
|
import { AddonPrivateFilesHelper } from '@/addons/privatefiles/services/privatefiles-helper';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page that displays the list of files.
|
* Page that displays the list of files.
|
||||||
|
@ -58,10 +58,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
protected updateSiteObserver: CoreEventObserver;
|
protected updateSiteObserver: CoreEventObserver;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
protected route: ActivatedRoute,
|
|
||||||
protected navCtrl: NavController,
|
|
||||||
) {
|
|
||||||
// Update visibility if current site info is updated.
|
// Update visibility if current site info is updated.
|
||||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||||
this.setVisibility();
|
this.setVisibility();
|
||||||
|
@ -72,17 +69,18 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.root = this.route.snapshot.queryParams['root'];
|
this.root = CoreNavigator.instance.getRouteParam('root');
|
||||||
|
const contextId = CoreNavigator.instance.getRouteNumberParam('contextid');
|
||||||
|
|
||||||
if (this.route.snapshot.queryParams['contextid']) {
|
if (contextId) {
|
||||||
// Loading a certain folder.
|
// Loading a certain folder.
|
||||||
this.path = {
|
this.path = {
|
||||||
contextid: this.route.snapshot.queryParams['contextid'],
|
contextid: contextId,
|
||||||
component: this.route.snapshot.queryParams['component'],
|
component: CoreNavigator.instance.getRouteParam<string>('component')!,
|
||||||
filearea: this.route.snapshot.queryParams['filearea'],
|
filearea: CoreNavigator.instance.getRouteParam<string>('filearea')!,
|
||||||
itemid: this.route.snapshot.queryParams['itemid'],
|
itemid: CoreNavigator.instance.getRouteNumberParam('itemid')!,
|
||||||
filepath: this.route.snapshot.queryParams['filepath'],
|
filepath: CoreNavigator.instance.getRouteParam<string>('filepath')!,
|
||||||
filename: this.route.snapshot.queryParams['filename'],
|
filename: CoreNavigator.instance.getRouteParam<string>('filename')!,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,10 +252,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(params));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(params));
|
||||||
|
|
||||||
this.navCtrl.navigateForward([`../${hash}`], {
|
CoreNavigator.instance.navigate(`../${hash}`, { params });
|
||||||
relativeTo: this.route,
|
|
||||||
queryParams: params,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { AppComponent } from '@/app/app.component';
|
import { AppComponent } from '@/app/app.component';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
|
@ -22,11 +21,12 @@ import { CoreLangProvider } from '@services/lang';
|
||||||
import { Network, Platform, NgZone } from '@singletons';
|
import { Network, Platform, NgZone } from '@singletons';
|
||||||
|
|
||||||
import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils';
|
import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils';
|
||||||
|
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
|
|
||||||
let langProvider: CoreLangProvider;
|
let langProvider: CoreLangProvider;
|
||||||
let navController: NavController;
|
let navigator: CoreNavigatorService;
|
||||||
let config: Partial<RenderConfig>;
|
let config: Partial<RenderConfig>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -35,12 +35,11 @@ describe('AppComponent', () => {
|
||||||
mockSingleton(Platform, { ready: () => Promise.resolve() });
|
mockSingleton(Platform, { ready: () => Promise.resolve() });
|
||||||
mockSingleton(NgZone, { run: jest.fn() });
|
mockSingleton(NgZone, { run: jest.fn() });
|
||||||
|
|
||||||
|
navigator = mockSingleton(CoreNavigator, ['navigate']);
|
||||||
langProvider = mock<CoreLangProvider>(['clearCustomStrings']);
|
langProvider = mock<CoreLangProvider>(['clearCustomStrings']);
|
||||||
navController = mock<NavController>(['navigateRoot']);
|
|
||||||
config = {
|
config = {
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: CoreLangProvider, useValue: langProvider },
|
{ provide: CoreLangProvider, useValue: langProvider },
|
||||||
{ provide: NavController, useValue: navController },
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -59,7 +58,7 @@ describe('AppComponent', () => {
|
||||||
CoreEvents.trigger(CoreEvents.LOGOUT);
|
CoreEvents.trigger(CoreEvents.LOGOUT);
|
||||||
|
|
||||||
expect(langProvider.clearCustomStrings).toHaveBeenCalled();
|
expect(langProvider.clearCustomStrings).toHaveBeenCalled();
|
||||||
expect(navController.navigateRoot).toHaveBeenCalledWith('/login/sites');
|
expect(navigator.navigate).toHaveBeenCalledWith('/login/sites', { reset: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it.todo('shows loading while app isn\'t ready');
|
it.todo('shows loading while app isn\'t ready');
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { CoreLangProvider } from '@services/lang';
|
import { CoreLangProvider } from '@services/lang';
|
||||||
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
||||||
|
@ -27,6 +26,7 @@ import {
|
||||||
import { Network, NgZone, Platform } from '@singletons';
|
import { Network, NgZone, Platform } from '@singletons';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
@ -37,7 +37,6 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected langProvider: CoreLangProvider,
|
protected langProvider: CoreLangProvider,
|
||||||
protected navCtrl: NavController,
|
|
||||||
protected loginHelper: CoreLoginHelperProvider,
|
protected loginHelper: CoreLoginHelperProvider,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -56,7 +55,7 @@ export class AppComponent implements OnInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||||
// Go to sites page when user is logged out.
|
// Go to sites page when user is logged out.
|
||||||
this.navCtrl.navigateRoot('/login/sites');
|
CoreNavigator.instance.navigate('/login/sites', { reset: true });
|
||||||
|
|
||||||
// Unload lang custom strings.
|
// Unload lang custom strings.
|
||||||
this.langProvider.clearCustomStrings();
|
this.langProvider.clearCustomStrings();
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c1.1,0,2,0.9,2,2v1h2V2c0-1.1-0.9-2-2-2h-3V2z M16,6h-2v4h2V6z M14,11v1
|
||||||
|
c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2
|
||||||
|
V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||||
|
<path style="fill:#FF403C;" d="M10.2,8l2.6-2.6c0.4-0.4,0.4-1,0-1.4L12,3.3c-0.4-0.4-1-0.4-1.4,0L8,5.9L5.4,3.3
|
||||||
|
c-0.4-0.4-1-0.4-1.4,0L3.3,4c-0.4,0.4-0.4,1,0,1.4L5.9,8l-2.6,2.6C3,11,3,11.6,3.3,12l0.7,0.7c0.4,0.4,1,0.4,1.4,0L8,10.2l2.5,2.5
|
||||||
|
c0.4,0.4,1,0.4,1.4,0l0.7-0.7c0.4-0.4,0.4-1,0-1.4L10.2,8z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c1.1 0 2 .9 2 2v1h2V2c0-1.1-.9-2-2-2h-3v2zm5 4h-2v4h2V6zm-2 5v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/></svg>
|
After Width: | Height: | Size: 579 B |
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c1.1,0,2,0.9,2,2v1h2V2c0-1.1-0.9-2-2-2h-3V2z M16,6h-2v4h2V6z M14,11v1
|
||||||
|
c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2
|
||||||
|
V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 955 B |
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c0.4,0,0.8,0.1,1.1,0.3c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8
|
||||||
|
V2c0-1.1-0.9-2-2-2h-3V2z M14,10h2V6.4l-2,2V10z M14,11v1c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4
|
||||||
|
c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||||
|
<path style="fill:#99CC33;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||||
|
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M10 0v2H6V0h4zm1 2h1c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2h-3v2zm3 8h2V6.4l-2 2V10zm0 1v1c0 1.1-.9 2-2 2h-1v2h3c1.1 0 2-.9 2-2v-3h-2zm-4 5v-2H6v2h4zm-5-2H4c-1.1 0-2-.9-2-2v-1H0v3c0 1.1.9 2 2 2h3v-2zm-5-4h2V6H0v4zm2-5V4c0-1.1.9-2 2-2h1V0H2C.9 0 0 .9 0 2v3h2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
|
After Width: | Height: | Size: 779 B |
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M10,0v2H6V0H10z M11,2h1c0.4,0,0.8,0.1,1.1,0.3c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8
|
||||||
|
V2c0-1.1-0.9-2-2-2h-3V2z M14,10h2V6.4l-2,2V10z M14,11v1c0,1.1-0.9,2-2,2h-1v2h3c1.1,0,2-0.9,2-2v-3H14z M10,16v-2H6v2H10z M5,14H4
|
||||||
|
c-1.1,0-2-0.9-2-2v-1H0v3c0,1.1,0.9,2,2,2h3V14z M0,10h2V6H0V10z M2,5V4c0-1.1,0.9-2,2-2h1V0H2C0.9,0,0,0.9,0,2v3H2z"/>
|
||||||
|
<path style="fill:#76A1F0;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||||
|
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 0H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V2c0-1.1-.9-2-2-2zm0 12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2v8z" fill="#FF2727"/></svg>
|
After Width: | Height: | Size: 476 B |
|
@ -0,0 +1,14 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M14,0H2C0.9,0,0,0.9,0,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V2C16,0.9,15.1,0,14,0z M14,12
|
||||||
|
c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h8c1.1,0,2,0.9,2,2V12z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 841 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M14 8.4V12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8c.4 0 .8.1 1.1.3.3-.3.8-.4 1.2-.4.5 0 1 .2 1.4.6l.3.3V2c0-1.1-.9-2-2-2H2C.9 0 0 .9 0 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V6.4l-2 2z" fill="#FF2727"/><path d="M15.7 3.9l-.7-.7c-.4-.4-1-.4-1.4 0l-6 6L5.4 7c-.4-.3-1-.3-1.4 0l-.7.7c-.4.4-.4 1 0 1.4l3.6 3.6c.4.4 1 .4 1.4 0l7.4-7.4c.4-.4.4-1 0-1.4z" fill="#76A1F0"/></svg>
|
After Width: | Height: | Size: 682 B |
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
|
||||||
|
<!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">
|
||||||
|
]>
|
||||||
|
<svg version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/"
|
||||||
|
x="0px" y="0px" width="16px" height="16px" viewBox="0 0 16 16" style="overflow:visible;enable-background:new 0 0 16 16;"
|
||||||
|
xml:space="preserve" preserveAspectRatio="xMinYMid meet">
|
||||||
|
<defs>
|
||||||
|
</defs>
|
||||||
|
<path style="fill:#999999;" d="M14,8.4V12c0,1.1-0.9,2-2,2H4c-1.1,0-2-0.9-2-2V4c0-1.1,0.9-2,2-2h8c0.4,0,0.8,0.1,1.1,0.3
|
||||||
|
c0.3-0.3,0.8-0.4,1.2-0.4c0.5,0,1,0.2,1.4,0.6L16,2.8V2c0-1.1-0.9-2-2-2H2C0.9,0,0,0.9,0,2v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2
|
||||||
|
V6.4L14,8.4z"/>
|
||||||
|
<path style="fill:#76A1F0;" d="M15.7,3.9L15,3.2c-0.4-0.4-1-0.4-1.4,0l-6,6L5.4,7C5,6.7,4.4,6.7,4,7L3.3,7.7c-0.4,0.4-0.4,1,0,1.4
|
||||||
|
l3.6,3.6c0.4,0.4,1,0.4,1.4,0l7.4-7.4C16.1,4.9,16.1,4.3,15.7,3.9z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
// (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 { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network error. It will automatically set the right error message if none is provided.
|
||||||
|
*/
|
||||||
|
export class CoreNetworkError extends Error {
|
||||||
|
|
||||||
|
constructor(message?: string) {
|
||||||
|
super(message || Translate.instance.instant('core.networkerrormsg'));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ import {
|
||||||
CoreWSAjaxPreSets,
|
CoreWSAjaxPreSets,
|
||||||
CoreWSExternalWarning,
|
CoreWSExternalWarning,
|
||||||
CoreWSUploadFileResult,
|
CoreWSUploadFileResult,
|
||||||
|
CoreWSPreSetsSplitRequest,
|
||||||
} from '@services/ws';
|
} from '@services/ws';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreTextUtils } from '@services/utils/text';
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
@ -516,6 +517,7 @@ export class CoreSite {
|
||||||
cleanUnicode: this.cleanUnicode,
|
cleanUnicode: this.cleanUnicode,
|
||||||
typeExpected: preSets.typeExpected,
|
typeExpected: preSets.typeExpected,
|
||||||
responseExpected: preSets.responseExpected,
|
responseExpected: preSets.responseExpected,
|
||||||
|
splitRequest: preSets.splitRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (wsPreSets.cleanUnicode && CoreTextUtils.instance.hasUnicodeData(data)) {
|
if (wsPreSets.cleanUnicode && CoreTextUtils.instance.hasUnicodeData(data)) {
|
||||||
|
@ -2052,6 +2054,12 @@ export type CoreSiteWSPreSets = {
|
||||||
* Component id. Optionally included when 'component' is set.
|
* Component id. Optionally included when 'component' is set.
|
||||||
*/
|
*/
|
||||||
componentId?: number;
|
componentId?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to split a request if it has too many parameters. Sending too many parameters to the site
|
||||||
|
* can cause the request to fail (see PHP's max_input_vars).
|
||||||
|
*/
|
||||||
|
splitRequest?: CoreWSPreSetsSplitRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -24,12 +24,10 @@ import {
|
||||||
KeyValueDiffers,
|
KeyValueDiffers,
|
||||||
SimpleChange,
|
SimpleChange,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Optional,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
KeyValueDiffer,
|
KeyValueDiffer,
|
||||||
Type,
|
Type,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreLogger } from '@singletons/logger';
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
@ -57,7 +55,7 @@ import { CoreLogger } from '@singletons/logger';
|
||||||
*
|
*
|
||||||
* Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't
|
* Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't
|
||||||
* be instantiated because it already is, it will be attached to the view and the right data will be passed to it.
|
* be instantiated because it already is, it will be attached to the view and the right data will be passed to it.
|
||||||
* Passing ComponentRef is meant for site plugins, so we'll inject a NavController instance to the component.
|
* Passing ComponentRef is meant for site plugins.
|
||||||
*
|
*
|
||||||
* The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above,
|
* The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above,
|
||||||
* if no component is supplied then the template will show the message "Cannot render the data.".
|
* if no component is supplied then the template will show the message "Cannot render the data.".
|
||||||
|
@ -90,7 +88,6 @@ export class CoreDynamicComponent implements OnChanges, DoCheck {
|
||||||
constructor(
|
constructor(
|
||||||
protected factoryResolver: ComponentFactoryResolver,
|
protected factoryResolver: ComponentFactoryResolver,
|
||||||
differs: KeyValueDiffers,
|
differs: KeyValueDiffers,
|
||||||
@Optional() protected navCtrl: NavController,
|
|
||||||
protected cdr: ChangeDetectorRef,
|
protected cdr: ChangeDetectorRef,
|
||||||
protected element: ElementRef,
|
protected element: ElementRef,
|
||||||
) {
|
) {
|
||||||
|
@ -167,7 +164,6 @@ export class CoreDynamicComponent implements OnChanges, DoCheck {
|
||||||
|
|
||||||
// This feature is usually meant for site plugins. Inject some properties.
|
// This feature is usually meant for site plugins. Inject some properties.
|
||||||
this.instance['ChangeDetectorRef'] = this.cdr;
|
this.instance['ChangeDetectorRef'] = this.cdr;
|
||||||
this.instance['NavController'] = this.navCtrl;
|
|
||||||
this.instance['componentContainer'] = this.element.nativeElement;
|
this.instance['componentContainer'] = this.element.nativeElement;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -16,7 +16,6 @@ import {
|
||||||
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
|
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { CoreFile } from '@services/file';
|
import { CoreFile } from '@services/file';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
@ -47,7 +46,6 @@ export class CoreIframeComponent implements OnChanges {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected sanitizer: DomSanitizer,
|
protected sanitizer: DomSanitizer,
|
||||||
protected navCtrl: NavController,
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
this.logger = CoreLogger.getInstance('CoreIframe');
|
this.logger = CoreLogger.getInstance('CoreIframe');
|
||||||
|
@ -77,7 +75,8 @@ export class CoreIframeComponent implements OnChanges {
|
||||||
this.loading = !this.src || !CoreUrlUtils.instance.isLocalFileUrl(this.src);
|
this.loading = !this.src || !CoreUrlUtils.instance.isLocalFileUrl(this.src);
|
||||||
|
|
||||||
// @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
// @todo const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
||||||
CoreIframeUtils.instance.treatFrame(iframe, false, this.navCtrl);
|
// CoreIframeUtils.instance.treatFrame(iframe, false, this.navCtrl);
|
||||||
|
CoreIframeUtils.instance.treatFrame(iframe, false);
|
||||||
|
|
||||||
iframe.addEventListener('load', () => {
|
iframe.addEventListener('load', () => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<ion-tabs class="hide-header">
|
<ion-tabs class="hide-header">
|
||||||
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="!tabs || numTabsShown < 1">
|
<ion-tab-bar slot="top" class="core-tabs-bar" [hidden]="!tabs || numTabsShown <= 1">
|
||||||
<ion-spinner *ngIf="!hideUntil"></ion-spinner>
|
<ion-spinner *ngIf="!hideUntil"></ion-spinner>
|
||||||
<ion-row *ngIf="hideUntil">
|
<ion-row *ngIf="hideUntil">
|
||||||
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1">
|
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1">
|
||||||
|
|
|
@ -24,18 +24,18 @@ import {
|
||||||
ViewChild,
|
ViewChild,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Platform, IonSlides, IonTabs, NavController } from '@ionic/angular';
|
import { Platform, IonSlides, IonTabs } from '@ionic/angular';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreConfig } from '@services/config';
|
import { CoreConfig } from '@services/config';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
|
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons';
|
import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { StackEvent } from '@ionic/angular/directives/navigation/stack-utils';
|
import { StackEvent } from '@ionic/angular/directives/navigation/stack-utils';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component displays some top scrollable tabs that will autohide on vertical scroll.
|
* This component displays some top scrollable tabs that will autohide on vertical scroll.
|
||||||
|
@ -106,7 +106,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
protected unregisterBackButtonAction: any;
|
protected unregisterBackButtonAction: any;
|
||||||
protected languageChangedSubscription: Subscription;
|
protected languageChangedSubscription: Subscription;
|
||||||
protected isInTransition = false; // Weather Slides is in transition.
|
protected isInTransition = false; // Weather Slides is in transition.
|
||||||
protected slidesSwiper: any;
|
protected slidesSwiper: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
protected slidesSwiperLoaded = false;
|
protected slidesSwiperLoaded = false;
|
||||||
protected stackEventsSubscription?: Subscription;
|
protected stackEventsSubscription?: Subscription;
|
||||||
|
|
||||||
|
@ -114,7 +114,6 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
protected element: ElementRef,
|
protected element: ElementRef,
|
||||||
platform: Platform,
|
platform: Platform,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
protected navCtrl: NavController,
|
|
||||||
) {
|
) {
|
||||||
this.direction = platform.isRTL ? 'rtl' : 'ltr';
|
this.direction = platform.isRTL ? 'rtl' : 'ltr';
|
||||||
|
|
||||||
|
@ -338,7 +337,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.firstSelectedTab = selectedTab.id;
|
this.firstSelectedTab = selectedTab.id!;
|
||||||
this.selectTab(this.firstSelectedTab);
|
this.selectTab(this.firstSelectedTab);
|
||||||
|
|
||||||
// Setup tab scrolling.
|
// Setup tab scrolling.
|
||||||
|
@ -548,18 +547,31 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tab selected.
|
* Select a tab by ID.
|
||||||
*
|
*
|
||||||
* @param tabId Selected tab index.
|
* @param tabId Tab ID.
|
||||||
* @param e Event.
|
* @param e Event.
|
||||||
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async selectTab(tabId: string, e?: Event): Promise<void> {
|
async selectTab(tabId: string, e?: Event): Promise<void> {
|
||||||
let index = this.tabs.findIndex((tab) => tabId == tab.id);
|
const index = this.tabs.findIndex((tab) => tabId == tab.id);
|
||||||
|
|
||||||
|
return this.selectByIndex(index, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a tab by index.
|
||||||
|
*
|
||||||
|
* @param index Index to select.
|
||||||
|
* @param e Event.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async selectByIndex(index: number, e?: Event): Promise<void> {
|
||||||
if (index < 0 || index >= this.tabs.length) {
|
if (index < 0 || index >= this.tabs.length) {
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
// Invalid index do not change tab.
|
// Invalid index do not change tab.
|
||||||
e && e.preventDefault();
|
e?.preventDefault();
|
||||||
e && e.stopPropagation();
|
e?.stopPropagation();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -568,12 +580,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTab = this.tabs[index];
|
const tabToSelect = this.tabs[index];
|
||||||
if (tabId == this.selected || !selectedTab || !selectedTab.enabled) {
|
if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
|
||||||
// Already selected or not enabled.
|
// Already selected or not enabled.
|
||||||
|
e?.preventDefault();
|
||||||
e && e.preventDefault();
|
e?.stopPropagation();
|
||||||
e && e.stopPropagation();
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -582,18 +593,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
await this.slides!.slideTo(index);
|
await this.slides!.slideTo(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageParams: NavigationOptions = {};
|
const ok = await CoreNavigator.instance.navigate(tabToSelect.page, {
|
||||||
if (selectedTab.pageParams) {
|
params: tabToSelect.pageParams,
|
||||||
pageParams.queryParams = selectedTab.pageParams;
|
});
|
||||||
}
|
|
||||||
const ok = await this.navCtrl.navigateForward(selectedTab.page, pageParams);
|
|
||||||
|
|
||||||
if (ok !== false) {
|
if (ok !== false) {
|
||||||
this.selectHistory.push(tabId);
|
this.selectHistory.push(tabToSelect.id!);
|
||||||
this.selected = tabId;
|
this.selected = tabToSelect.id;
|
||||||
this.selectedIndex = index;
|
this.selectedIndex = index;
|
||||||
|
|
||||||
this.ionChange.emit(selectedTab);
|
this.ionChange.emit(tabToSelect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -644,16 +653,14 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
||||||
/**
|
/**
|
||||||
* Core Tab class.
|
* Core Tab class.
|
||||||
*/
|
*/
|
||||||
class CoreTab {
|
export type CoreTab = {
|
||||||
|
page: string; // Page to navigate to.
|
||||||
id = ''; // Unique tab id.
|
title: string; // The translatable tab title.
|
||||||
class = ''; // Class, if needed.
|
id?: string; // Unique tab id.
|
||||||
title = ''; // The translatable tab title.
|
class?: string; // Class, if needed.
|
||||||
icon?: string; // The tab icon.
|
icon?: string; // The tab icon.
|
||||||
badge?: string; // A badge to add in the tab.
|
badge?: string; // A badge to add in the tab.
|
||||||
badgeStyle?: string; // The badge color.
|
badgeStyle?: string; // The badge color.
|
||||||
enabled = true; // Whether the tab is enabled.
|
enabled?: boolean; // Whether the tab is enabled.
|
||||||
page = ''; // Page to navigate to.
|
|
||||||
pageParams?: Params; // Page params.
|
pageParams?: Params; // Page params.
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
|
@ -13,15 +13,13 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChange } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
|
|
||||||
import { CoreApp } from '@services/app';
|
import { CoreApp } from '@services/app';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreObject } from '@singletons/object';
|
|
||||||
import { CoreUserProvider, CoreUserBasicData, CoreUserProfilePictureUpdatedData } from '@features/user/services/user';
|
import { CoreUserProvider, CoreUserBasicData, CoreUserProfilePictureUpdatedData } from '@features/user/services/user';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to display a "user avatar".
|
* Component to display a "user avatar".
|
||||||
|
@ -53,10 +51,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
protected currentUserId: number;
|
protected currentUserId: number;
|
||||||
protected pictureObserver: CoreEventObserver;
|
protected pictureObserver: CoreEventObserver;
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
protected navCtrl: NavController,
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) {
|
|
||||||
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
||||||
|
|
||||||
this.pictureObserver = CoreEvents.on<CoreUserProfilePictureUpdatedData>(
|
this.pictureObserver = CoreEvents.on<CoreUserProfilePictureUpdatedData>(
|
||||||
|
@ -143,12 +138,11 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
// @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav.
|
// @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav.
|
||||||
this.navCtrl.navigateForward(['user'], {
|
CoreNavigator.instance.navigateToSitePath('user', {
|
||||||
relativeTo: this.route,
|
params: {
|
||||||
queryParams: CoreObject.withoutEmpty({
|
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
courseId: this.courseId,
|
courseId: this.courseId,
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {
|
||||||
Optional,
|
Optional,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { NavController, IonContent } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
|
|
||||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
|
@ -84,7 +84,6 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
element: ElementRef,
|
element: ElementRef,
|
||||||
@Optional() protected navCtrl: NavController,
|
|
||||||
@Optional() protected content: IonContent,
|
@Optional() protected content: IonContent,
|
||||||
protected viewContainerRef: ViewContainerRef,
|
protected viewContainerRef: ViewContainerRef,
|
||||||
) {
|
) {
|
||||||
|
@ -471,7 +470,8 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
*/
|
*/
|
||||||
protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
|
protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
|
||||||
const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false;
|
const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false;
|
||||||
const navCtrl = this.navCtrl; // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
// @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
|
||||||
|
// @todo: Pass navCtrl to all treateFrame calls?
|
||||||
|
|
||||||
const images = Array.from(div.querySelectorAll('img'));
|
const images = Array.from(div.querySelectorAll('img'));
|
||||||
const anchors = Array.from(div.querySelectorAll('a'));
|
const anchors = Array.from(div.querySelectorAll('a'));
|
||||||
|
@ -521,7 +521,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
});
|
});
|
||||||
|
|
||||||
iframes.forEach((iframe) => {
|
iframes.forEach((iframe) => {
|
||||||
this.treatIframe(iframe, site, canTreatVimeo, navCtrl);
|
this.treatIframe(iframe, site, canTreatVimeo);
|
||||||
});
|
});
|
||||||
|
|
||||||
svgImages.forEach((image) => {
|
svgImages.forEach((image) => {
|
||||||
|
@ -554,7 +554,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
|
|
||||||
// Handle all kind of frames.
|
// Handle all kind of frames.
|
||||||
frames.forEach((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => {
|
frames.forEach((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => {
|
||||||
CoreIframeUtils.instance.treatFrame(frame, false, navCtrl);
|
CoreIframeUtils.instance.treatFrame(frame, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
CoreDomUtils.instance.handleBootstrapTooltips(div);
|
CoreDomUtils.instance.handleBootstrapTooltips(div);
|
||||||
|
@ -671,13 +671,11 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
* @param iframe Iframe to treat.
|
* @param iframe Iframe to treat.
|
||||||
* @param site Site instance.
|
* @param site Site instance.
|
||||||
* @param canTreatVimeo Whether Vimeo videos can be treated in the site.
|
* @param canTreatVimeo Whether Vimeo videos can be treated in the site.
|
||||||
* @param navCtrl NavController to use.
|
|
||||||
*/
|
*/
|
||||||
protected async treatIframe(
|
protected async treatIframe(
|
||||||
iframe: HTMLIFrameElement,
|
iframe: HTMLIFrameElement,
|
||||||
site: CoreSite | undefined,
|
site: CoreSite | undefined,
|
||||||
canTreatVimeo: boolean,
|
canTreatVimeo: boolean,
|
||||||
navCtrl: NavController,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const src = iframe.src;
|
const src = iframe.src;
|
||||||
const currentSite = CoreSites.instance.getCurrentSite();
|
const currentSite = CoreSites.instance.getCurrentSite();
|
||||||
|
@ -689,8 +687,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
const finalUrl = await currentSite.getAutoLoginUrl(src, false);
|
const finalUrl = await currentSite.getAutoLoginUrl(src, false);
|
||||||
|
|
||||||
iframe.src = finalUrl;
|
iframe.src = finalUrl;
|
||||||
|
CoreIframeUtils.instance.treatFrame(iframe, false);
|
||||||
CoreIframeUtils.instance.treatFrame(iframe, false, navCtrl);
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -751,7 +748,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreIframeUtils.instance.treatFrame(iframe, false, navCtrl);
|
CoreIframeUtils.instance.treatFrame(iframe, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { DomSanitizer } from '@angular/platform-browser';
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
import { IonContent, NavController } from '@ionic/angular';
|
import { IonContent } from '@ionic/angular';
|
||||||
import { NgZone } from '@angular/core';
|
import { NgZone } from '@angular/core';
|
||||||
import Faker from 'faker';
|
import Faker from 'faker';
|
||||||
|
|
||||||
|
@ -44,7 +44,6 @@ describe('CoreFormatTextDirective', () => {
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: NavController, useValue: null },
|
|
||||||
{ provide: IonContent, useValue: null },
|
{ provide: IonContent, useValue: null },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,8 +13,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
import { CoreObject } from '@singletons/object';
|
import { CoreObject } from '@singletons/object';
|
||||||
|
@ -34,8 +32,6 @@ export class CoreUserLinkDirective implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
element: ElementRef,
|
element: ElementRef,
|
||||||
protected navCtrl: NavController,
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
) {
|
) {
|
||||||
this.element = element.nativeElement;
|
this.element = element.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
<ion-list>
|
<ion-list>
|
||||||
<!-- Course expand="block"s. -->
|
<!-- Course expand="block"s. -->
|
||||||
<ng-container *ngFor="let block of blocks">
|
<ng-container *ngFor="let block of blocks">
|
||||||
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId" [extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
|
<core-block *ngIf="block.visible" [block]="block" contextLevel="course" [instanceId]="courseId"
|
||||||
|
[extraData]="{'downloadEnabled': downloadEnabled}"></core-block>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
</core-loading>
|
</core-loading>
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
max-width: var(--side-blocks-max-width);
|
max-width: var(--side-blocks-max-width);
|
||||||
min-width: var(--side-blocks-min-width);
|
min-width: var(--side-blocks-min-width);
|
||||||
box-shadow: -4px 0px 16px rgba(0, 0, 0, 0.18);
|
box-shadow: -4px 0px 16px rgba(0, 0, 0, 0.18);
|
||||||
|
z-index: 2;
|
||||||
// @todo @include core-split-area-end();
|
// @todo @include core-split-area-end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
import { CoreCourse, CoreCourseBlock } from '@features/course/services/course';
|
||||||
import { CoreBlockHelper } from '../../services/block-helper';
|
import { CoreBlockHelper } from '../../services/block-helper';
|
||||||
import { CoreBlockComponent } from '../block/block';
|
import { CoreBlockComponent } from '../block/block';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that displays the list of course blocks.
|
* Component that displays the list of course blocks.
|
||||||
|
@ -108,4 +109,15 @@ export class CoreBlockCourseBlocksComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(): Promise<void> {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateBlocks());
|
||||||
|
|
||||||
|
await this.loadContent();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { CoreContentLinksHandlerBase } from './base-handler';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { CoreContentLinksAction } from '../services/contentlinks-delegate';
|
import { CoreContentLinksAction } from '../services/contentlinks-delegate';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler to handle URLs pointing to a list of a certain type of modules.
|
* Handler to handle URLs pointing to a list of a certain type of modules.
|
||||||
|
@ -55,16 +56,15 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa
|
||||||
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
action: (siteId): void => {
|
action: (siteId): void => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
CoreNavigator.instance.navigateToSitePath('course/list-mod-type', {
|
||||||
const stateParams = {
|
params: {
|
||||||
courseId: params.id,
|
courseId: params.id,
|
||||||
modName: this.modName,
|
modName: this.modName,
|
||||||
title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'),
|
title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'),
|
||||||
};
|
},
|
||||||
|
siteId,
|
||||||
// @todo CoreNavigator.instance.goInSite('CoreCourseListModTypePage', stateParams, siteId);
|
});
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,13 +13,11 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { NavController } from '@ionic/angular';
|
|
||||||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreContentLinksAction } from '../../services/contentlinks-delegate';
|
import { CoreContentLinksAction } from '../../services/contentlinks-delegate';
|
||||||
import { CoreContentLinksHelper } from '../../services/contentlinks-helper';
|
import { CoreContentLinksHelper } from '../../services/contentlinks-helper';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
|
@ -34,27 +32,22 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
})
|
})
|
||||||
export class CoreContentLinksChooseSitePage implements OnInit {
|
export class CoreContentLinksChooseSitePage implements OnInit {
|
||||||
|
|
||||||
url: string;
|
url!: string;
|
||||||
sites: CoreSiteBasicInfo[] = [];
|
sites: CoreSiteBasicInfo[] = [];
|
||||||
loaded = false;
|
loaded = false;
|
||||||
protected action?: CoreContentLinksAction;
|
protected action?: CoreContentLinksAction;
|
||||||
protected isRootURL = false;
|
protected isRootURL = false;
|
||||||
|
|
||||||
constructor(
|
|
||||||
route: ActivatedRoute,
|
|
||||||
protected navCtrl: NavController,
|
|
||||||
) {
|
|
||||||
this.url = route.snapshot.queryParamMap.get('url')!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component being initialized.
|
* Component being initialized.
|
||||||
*/
|
*/
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
if (!this.url) {
|
const url = CoreNavigator.instance.getRouteParam<string>('url');
|
||||||
|
if (!url) {
|
||||||
return this.leaveView();
|
return this.leaveView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.url = url;
|
||||||
let siteIds: string[] | undefined = [];
|
let siteIds: string[] | undefined = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -115,7 +108,7 @@ export class CoreContentLinksChooseSitePage implements OnInit {
|
||||||
try {
|
try {
|
||||||
await CoreSites.instance.logout();
|
await CoreSites.instance.logout();
|
||||||
} finally {
|
} finally {
|
||||||
await this.navCtrl.navigateRoot('/login/sites');
|
await CoreNavigator.instance.navigate('/login/sites', { reset: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { CoreContentLinksDelegate, CoreContentLinksAction } from './contentlinks
|
||||||
import { CoreSite } from '@classes/site';
|
import { CoreSite } from '@classes/site';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
import { CoreNavigator } from '@services/navigator';
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service that provides some features regarding content links.
|
* Service that provides some features regarding content links.
|
||||||
|
@ -27,10 +28,6 @@ import { CoreNavigator } from '@services/navigator';
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class CoreContentLinksHelperProvider {
|
export class CoreContentLinksHelperProvider {
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected navCtrl: NavController,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a link can be handled by the app.
|
* Check whether a link can be handled by the app.
|
||||||
*
|
*
|
||||||
|
@ -93,8 +90,7 @@ export class CoreContentLinksHelperProvider {
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
* @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead.
|
* @deprecated since 3.9.5. Use CoreNavigator.navigateToSitePath instead.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
async goInSite(navCtrl: NavController, pageName: string, pageParams: Params, siteId?: string): Promise<void> {
|
||||||
async goInSite(navCtrl: NavController, pageName: string, pageParams: any, siteId?: string): Promise<void> {
|
|
||||||
await CoreNavigator.instance.navigateToSitePath(pageName, { params: pageParams, siteId });
|
await CoreNavigator.instance.navigateToSitePath(pageName, { params: pageParams, siteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +101,7 @@ export class CoreContentLinksHelperProvider {
|
||||||
* @todo set correct root.
|
* @todo set correct root.
|
||||||
*/
|
*/
|
||||||
async goToChooseSite(url: string): Promise<void> {
|
async goToChooseSite(url: string): Promise<void> {
|
||||||
await this.navCtrl.navigateRoot('CoreContentLinksChooseSitePage @todo', { queryParams: { url } });
|
await CoreNavigator.instance.navigate('CoreContentLinksChooseSitePage @todo', { params: { url }, reset: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,193 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreCourse, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. It is useful to minimize the amount of
|
||||||
|
* functions that handlers need to implement. It also provides some helper features like preventing a module to be
|
||||||
|
* downloaded twice at the same time.
|
||||||
|
*
|
||||||
|
* If your handler inherits from this service, you just need to override the functions that you want to change.
|
||||||
|
*
|
||||||
|
* This class should be used for ACTIVITIES. You must override the prefetch function, and it's recommended to call
|
||||||
|
* prefetchPackage in there since it handles the package status.
|
||||||
|
*/
|
||||||
|
export class CoreCourseActivityPrefetchHandlerBase extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
// Same implementation for download and prefetch.
|
||||||
|
return this.prefetch(module, courseId, false, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden. It should call prefetchPackage
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the module, setting package status at start and finish.
|
||||||
|
*
|
||||||
|
* Example usage from a child instance:
|
||||||
|
* return this.prefetchPackage(module, courseId, single, this.prefetchModule.bind(this, otherParam), siteId);
|
||||||
|
*
|
||||||
|
* Then the function "prefetchModule" will receive params:
|
||||||
|
* prefetchModule(module, courseId, single, siteId, someParam, anotherParam)
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param downloadFn Function to perform the prefetch. Please check the documentation of prefetchFunction.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the module has been downloaded. Data returned is not reliable.
|
||||||
|
*/
|
||||||
|
async prefetchPackage(
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
downloadFunction: () => Promise<string>,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (!CoreApp.instance.isOnline()) {
|
||||||
|
// Cannot prefetch in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDownloading(module.id, siteId)) {
|
||||||
|
// There's already a download ongoing for this module, return the promise.
|
||||||
|
return this.getOngoingDownload(module.id, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefetchPromise = this.changeStatusAndPrefetch(module, courseId, downloadFunction, siteId);
|
||||||
|
|
||||||
|
return this.addOngoingDownload(module.id, prefetchPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async changeStatusAndPrefetch(
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
downloadFunction: () => Promise<string>,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.setDownloading(module.id, siteId);
|
||||||
|
|
||||||
|
// Package marked as downloading, get module info to be able to handle links. Get module filters too.
|
||||||
|
await Promise.all([
|
||||||
|
CoreCourse.instance.getModuleBasicInfo(module.id, siteId),
|
||||||
|
CoreCourse.instance.getModule(module.id, courseId, undefined, false, true, siteId),
|
||||||
|
CoreFilterHelper.instance.getFilters('module', module.id, { courseId }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Call the download function.
|
||||||
|
let extra = await downloadFunction();
|
||||||
|
|
||||||
|
// Only accept string types.
|
||||||
|
if (typeof extra != 'string') {
|
||||||
|
extra = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefetch finished, mark as downloaded.
|
||||||
|
await this.setDownloaded(module.id, siteId, extra);
|
||||||
|
} catch (error) {
|
||||||
|
// Error prefetching, go back to previous status and reject the promise.
|
||||||
|
return this.setPreviousStatus(module.id, siteId);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the module as downloaded.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param extra Extra data to store.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
setDownloaded(id: number, siteId?: string, extra?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.storePackageStatus(siteId, CoreConstants.DOWNLOADED, this.component, id, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the module as downloading.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
setDownloading(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.storePackageStatus(siteId, CoreConstants.DOWNLOADING, this.component, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set previous status and return a rejected promise.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Rejected promise.
|
||||||
|
*/
|
||||||
|
async setPreviousStatus(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await CoreFilepool.instance.setPackagePreviousStatus(siteId, this.component, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set previous status and return a rejected promise.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param error Error to throw.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Rejected promise.
|
||||||
|
* @deprecated since 3.9.5. Use setPreviousStatus instead.
|
||||||
|
*/
|
||||||
|
async setPreviousStatusAndReject(id: number, error?: Error, siteId?: string): Promise<never> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await CoreFilepool.instance.setPackagePreviousStatus(siteId, this.component, id);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// (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 { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||||
|
import { CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class to create activity sync providers. It provides some common functions.
|
||||||
|
*/
|
||||||
|
export class CoreCourseActivitySyncBaseProvider extends CoreSyncBaseProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conveniece function to prefetch data after an update.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param preventDownloadRegex If regex matches, don't download the data. Defaults to check files.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async prefetchAfterUpdate(
|
||||||
|
prefetchHandler: CoreCourseModulePrefetchHandlerBase,
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
preventDownloadRegex?: RegExp,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Get the module updates to check if the data was updated or not.
|
||||||
|
const result = await CoreCourseModulePrefetchDelegate.instance.getModuleUpdates(module, courseId, true, siteId);
|
||||||
|
|
||||||
|
if (!result?.updates.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only prefetch if files haven't changed, to prevent downloading too much data automatically.
|
||||||
|
const regex = preventDownloadRegex || /^.*files$/;
|
||||||
|
const shouldDownload = !result.updates.find((entry) => entry.name.match(regex));
|
||||||
|
|
||||||
|
if (shouldDownload) {
|
||||||
|
return prefetchHandler.download(module, courseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
// (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, Inject, Input, OnDestroy, OnInit, Optional } from '@angular/core';
|
||||||
|
import { IonContent } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreCourseModuleMainResourceComponent } from './main-resource-component';
|
||||||
|
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||||
|
import { Network, NgZone } from '@singletons';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreCourse } from '../services/course';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { CoreCourseContentsPage } from '../pages/contents/contents';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template class to easily create CoreCourseModuleMainComponent of activities.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleMainActivityComponent extends CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() group?: number; // Group ID the component belongs to.
|
||||||
|
|
||||||
|
moduleName?: string; // Raw module name to be translated. It will be translated on init.
|
||||||
|
|
||||||
|
// Data for context menu.
|
||||||
|
syncIcon?: string; // Sync icon.
|
||||||
|
hasOffline?: boolean; // If it has offline data to be synced.
|
||||||
|
isOnline?: boolean; // If the app is online or not.
|
||||||
|
|
||||||
|
protected syncObserver?: CoreEventObserver; // It will observe the sync auto event.
|
||||||
|
protected onlineSubscription: Subscription; // It will observe the status of the network connection.
|
||||||
|
protected syncEventName?: string; // Auto sync event name.
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||||
|
protected content?: IonContent,
|
||||||
|
courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
super(loggerName, courseContentsPage);
|
||||||
|
|
||||||
|
// Refresh online status when changes.
|
||||||
|
this.onlineSubscription = Network.instance.onChange().subscribe(() => {
|
||||||
|
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||||
|
NgZone.instance.run(() => {
|
||||||
|
this.isOnline = CoreApp.instance.isOnline();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.hasOffline = false;
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.moduleName = CoreCourse.instance.translateModuleName(this.moduleName || '');
|
||||||
|
|
||||||
|
if (this.syncEventName) {
|
||||||
|
// Refresh data if this discussion is synchronized automatically.
|
||||||
|
this.syncObserver = CoreEvents.on(this.syncEventName, (data) => {
|
||||||
|
this.autoSyncEventReceived(data);
|
||||||
|
}, this.siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares sync event data with current data to check if refresh content is needed.
|
||||||
|
*
|
||||||
|
* @param syncEventData Data received on sync observer.
|
||||||
|
* @return True if refresh is needed, false otherwise.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: unknown): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An autosync event has been received, check if refresh is needed and update the view.
|
||||||
|
*
|
||||||
|
* @param syncEventData Data receiven on sync observer.
|
||||||
|
*/
|
||||||
|
protected autoSyncEventReceived(syncEventData: unknown): void {
|
||||||
|
if (this.isRefreshSyncNeeded(syncEventData)) {
|
||||||
|
// Refresh the data.
|
||||||
|
this.showLoadingAndRefresh(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateContent());
|
||||||
|
|
||||||
|
await this.loadContent(true, sync, showErrors);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show loading and perform the load content function.
|
||||||
|
*
|
||||||
|
* @param sync If the fetch needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async showLoadingAndFetch(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.loaded = false;
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loadContent(false, sync, showErrors);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show loading and perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected showLoadingAndRefresh(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
this.syncIcon = 'spinner';
|
||||||
|
this.loaded = false;
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
return this.refreshContent(sync, showErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the component contents.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the component contents and shows the corresponding error.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadContent(refresh?: boolean, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.isOnline = CoreApp.instance.isOnline();
|
||||||
|
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchContent(refresh, sync, showErrors);
|
||||||
|
} catch (error) {
|
||||||
|
if (!refresh) {
|
||||||
|
// Some call failed, retry without using cache since it might be a new activity.
|
||||||
|
return await this.refreshContent(sync);
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
this.syncIcon = 'fas-sync';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async sync(): Promise<unknown> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param result Data returned on the sync function.
|
||||||
|
* @return If suceed or not.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected hasSyncSucceed(result: unknown): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to synchronize the activity.
|
||||||
|
*
|
||||||
|
* @param showErrors If show errors to the user of hide them.
|
||||||
|
* @return Promise resolved with true if sync succeed, or false if failed.
|
||||||
|
*/
|
||||||
|
protected async syncActivity(showErrors: boolean = false): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = <{warnings?: CoreWSExternalWarning[]}> await this.sync();
|
||||||
|
|
||||||
|
if (result?.warnings?.length) {
|
||||||
|
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.hasSyncSucceed(result);
|
||||||
|
} catch (error) {
|
||||||
|
if (showErrors) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorsync', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
this.onlineSubscription?.unsubscribe();
|
||||||
|
this.syncObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,412 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { OnInit, OnDestroy, Input, Output, EventEmitter, Component, Optional, Inject } from '@angular/core';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
|
||||||
|
import { CoreTextErrorObject, CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
||||||
|
import { CoreLogger } from '@singletons/logger';
|
||||||
|
import { CoreCourseContentsPage } from '../pages/contents/contents';
|
||||||
|
import { CoreCourse } from '../services/course';
|
||||||
|
import { CoreCourseHelper, CoreCourseModule } from '../services/course-helper';
|
||||||
|
import { CoreCourseModuleDelegate, CoreCourseModuleMainComponent } from '../services/module-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '../services/module-prefetch-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a resource download.
|
||||||
|
*/
|
||||||
|
export type CoreCourseResourceDownloadResult = {
|
||||||
|
failed?: boolean; // Whether the download has failed.
|
||||||
|
error?: string | CoreTextErrorObject; // The error in case it failed.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Template class to easily create CoreCourseModuleMainComponent of resources (or activities without syncing).
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy, CoreCourseModuleMainComponent {
|
||||||
|
|
||||||
|
@Input() module?: CoreCourseModule; // The module of the component.
|
||||||
|
@Input() courseId?: number; // Course ID the component belongs to.
|
||||||
|
@Output() dataRetrieved = new EventEmitter<unknown>(); // Called to notify changes the index page from the main component.
|
||||||
|
|
||||||
|
loaded = false; // If the component has been loaded.
|
||||||
|
component?: string; // Component name.
|
||||||
|
componentId?: number; // Component ID.
|
||||||
|
blog?: boolean; // If blog is avalaible.
|
||||||
|
|
||||||
|
// Data for context menu.
|
||||||
|
externalUrl?: string; // External URL to open in browser.
|
||||||
|
description?: string; // Module description.
|
||||||
|
refreshIcon = 'spinner'; // Refresh icon, normally spinner or refresh.
|
||||||
|
prefetchStatusIcon?: string; // Used when calling fillContextMenu.
|
||||||
|
prefetchStatus?: string; // Used when calling fillContextMenu.
|
||||||
|
prefetchText?: string; // Used when calling fillContextMenu.
|
||||||
|
size?: string; // Used when calling fillContextMenu.
|
||||||
|
isDestroyed?: boolean; // Whether the component is destroyed, used when calling fillContextMenu.
|
||||||
|
contextMenuStatusObserver?: CoreEventObserver; // Observer of package status, used when calling fillContextMenu.
|
||||||
|
contextFileStatusObserver?: CoreEventObserver; // Observer of file status, used when calling fillContextMenu.
|
||||||
|
|
||||||
|
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
|
||||||
|
protected isCurrentView?: boolean; // Whether the component is in the current view.
|
||||||
|
protected siteId?: string; // Current Site ID.
|
||||||
|
protected statusObserver?: CoreEventObserver; // Observer of package status. Only if setStatusListener is called.
|
||||||
|
protected currentStatus?: string; // The current status of the module. Only if setStatusListener is called.
|
||||||
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
|
||||||
|
protected courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
this.logger = CoreLogger.getInstance(loggerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
this.description = this.module?.description;
|
||||||
|
this.componentId = this.module?.id;
|
||||||
|
this.externalUrl = this.module?.url;
|
||||||
|
this.courseId = this.courseId || this.module?.course;
|
||||||
|
// @todo this.blog = await this.blogProvider.isPluginEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param done Function to call when done.
|
||||||
|
* @param showErrors If show errors to the user of hide them.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.loaded || !this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a single activity course and the refresher is displayed within the component,
|
||||||
|
// call doRefresh on the section page to refresh the course data.
|
||||||
|
if (this.courseContentsPage && !CoreCourseModuleDelegate.instance.displayRefresherInSingleActivity(this.module.modname)) {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.courseContentsPage.doRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.refreshContent(true, showErrors));
|
||||||
|
|
||||||
|
refresher?.detail.complete();
|
||||||
|
done && done();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the refresh content function.
|
||||||
|
*
|
||||||
|
* @param sync If the refresh needs syncing.
|
||||||
|
* @param showErrors Wether to show errors to the user or hide them.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async refreshContent(sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshIcon = 'spinner';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateContent());
|
||||||
|
|
||||||
|
await this.loadContent(true);
|
||||||
|
} finally {
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the invalidate content function.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async invalidateContent(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the component contents.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected async fetchContent(refresh?: boolean): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the component contents and shows the corresponding error.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadContent(refresh?: boolean): Promise<void> {
|
||||||
|
if (!this.module) {
|
||||||
|
// This can happen if course format changes from single activity to weekly/topics.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchContent(refresh);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, this.fetchContentDefaultError, true);
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
this.refreshIcon = 'fas-redo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill the context menu options
|
||||||
|
*/
|
||||||
|
protected fillContextMenu(refresh: boolean = false): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All data obtained, now fill the context menu.
|
||||||
|
CoreCourseHelper.instance.fillContextMenu(this, this.module, this.courseId!, refresh, this.component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the module is prefetched or being prefetched. To make it faster, just use the data calculated by fillContextMenu.
|
||||||
|
* This means that you need to call fillContextMenu to make this work.
|
||||||
|
*/
|
||||||
|
protected isPrefetched(): boolean {
|
||||||
|
return this.prefetchStatus != CoreConstants.NOT_DOWNLOADABLE && this.prefetchStatus != CoreConstants.NOT_DOWNLOADED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the description.
|
||||||
|
*/
|
||||||
|
expandDescription(): void {
|
||||||
|
CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.description!, {
|
||||||
|
component: this.component,
|
||||||
|
componentId: this.module?.id,
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module?.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go to blog posts.
|
||||||
|
*/
|
||||||
|
async gotoBlog(): Promise<void> {
|
||||||
|
// @todo return this.linkHelper.goInSite(this.navCtrl, 'AddonBlogEntriesPage', { cmId: this.module.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the module.
|
||||||
|
*
|
||||||
|
* @param done Function to call when done.
|
||||||
|
*/
|
||||||
|
prefetch(done?: () => void): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.instance.contextMenuPrefetch(this, this.module, this.courseId!, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm and remove downloaded files.
|
||||||
|
*
|
||||||
|
* @param done Function to call when done.
|
||||||
|
*/
|
||||||
|
removeFiles(done?: () => void): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.prefetchStatus == CoreConstants.DOWNLOADING) {
|
||||||
|
CoreDomUtils.instance.showAlertTranslated(undefined, 'core.course.cannotdeletewhiledownloading');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.instance.confirmAndRemoveFiles(this.module, this.courseId!, done);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get message about an error occurred while downloading files.
|
||||||
|
*
|
||||||
|
* @param error The specific error.
|
||||||
|
* @param multiLine Whether to put each message in a different paragraph or in a single line.
|
||||||
|
*/
|
||||||
|
protected getErrorDownloadingSomeFilesMessage(error: string | CoreTextErrorObject, multiLine?: boolean): string {
|
||||||
|
if (multiLine) {
|
||||||
|
return CoreTextUtils.instance.buildSeveralParagraphsMessage([
|
||||||
|
Translate.instance.instant('core.errordownloadingsomefiles'),
|
||||||
|
error,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
error = CoreTextUtils.instance.getErrorMessageFromError(error) || error;
|
||||||
|
|
||||||
|
return Translate.instance.instant('core.errordownloadingsomefiles') + (error ? ' ' + error : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an error occurred while downloading files.
|
||||||
|
*
|
||||||
|
* @param error The specific error.
|
||||||
|
*/
|
||||||
|
protected showErrorDownloadingSomeFiles(error: string | CoreTextErrorObject): void {
|
||||||
|
CoreDomUtils.instance.showErrorModal(this.getErrorDownloadingSomeFilesMessage(error, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays some data based on the current status.
|
||||||
|
*
|
||||||
|
* @param status The current status.
|
||||||
|
* @param previousStatus The previous status. If not defined, there is no previous status.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected showStatus(status: string, previousStatus?: string): void {
|
||||||
|
// To be overridden.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watch for changes on the status.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async setStatusListener(): Promise<void> {
|
||||||
|
if (typeof this.statusObserver != 'undefined' || !this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for changes on this module status.
|
||||||
|
this.statusObserver = CoreEvents.on<CoreEventPackageStatusChanged>(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||||
|
if (!this.module || data.componentId != this.module.id || data.component != this.component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The status has changed, update it.
|
||||||
|
const previousStatus = this.currentStatus;
|
||||||
|
this.currentStatus = data.status;
|
||||||
|
|
||||||
|
this.showStatus(this.currentStatus, previousStatus);
|
||||||
|
}, this.siteId);
|
||||||
|
|
||||||
|
// Also, get the current status.
|
||||||
|
const status = await CoreCourseModulePrefetchDelegate.instance.getModuleStatus(this.module, this.courseId!);
|
||||||
|
|
||||||
|
this.currentStatus = status;
|
||||||
|
this.showStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a resource if needed.
|
||||||
|
* If the download call fails the promise won't be rejected, but the error will be included in the returned object.
|
||||||
|
* If module.contents cannot be loaded then the Promise will be rejected.
|
||||||
|
*
|
||||||
|
* @param refresh Whether we're refreshing data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async downloadResourceIfNeeded(
|
||||||
|
refresh?: boolean,
|
||||||
|
contentsAlreadyLoaded?: boolean,
|
||||||
|
): Promise<CoreCourseResourceDownloadResult> {
|
||||||
|
|
||||||
|
const result: CoreCourseResourceDownloadResult = {
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.module) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get module status to determine if it needs to be downloaded.
|
||||||
|
await this.setStatusListener();
|
||||||
|
|
||||||
|
if (this.currentStatus != CoreConstants.DOWNLOADED) {
|
||||||
|
// Download content. This function also loads module contents if needed.
|
||||||
|
try {
|
||||||
|
await CoreCourseModulePrefetchDelegate.instance.downloadModule(this.module, this.courseId!);
|
||||||
|
|
||||||
|
// If we reach here it means the download process already loaded the contents, no need to do it again.
|
||||||
|
contentsAlreadyLoaded = true;
|
||||||
|
} catch (error) {
|
||||||
|
// Mark download as failed but go on since the main files could have been downloaded.
|
||||||
|
result.failed = true;
|
||||||
|
result.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.module.contents.length || (refresh && !contentsAlreadyLoaded)) {
|
||||||
|
// Try to load the contents.
|
||||||
|
const ignoreCache = refresh && CoreApp.instance.isOnline();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourse.instance.loadModuleContents(this.module, this.courseId, undefined, false, ignoreCache);
|
||||||
|
} catch (error) {
|
||||||
|
// Error loading contents. If we ignored cache, try to get the cached value.
|
||||||
|
if (ignoreCache && !this.module.contents) {
|
||||||
|
await CoreCourse.instance.loadModuleContents(this.module, this.courseId);
|
||||||
|
} else if (!this.module.contents) {
|
||||||
|
// Not able to load contents, throw the error.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.contextMenuStatusObserver?.off();
|
||||||
|
this.contextFileStatusObserver?.off();
|
||||||
|
this.statusObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page that contains the component. This function should be called by the page that contains this component.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.isCurrentView = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page that contains the component. This function should be called by the page that contains this component.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.isCurrentView = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,344 @@
|
||||||
|
// (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 { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreFileSizeSum, CorePluginFileDelegate } from '@services/plugin-file-delegate';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandler } from '../services/module-prefetch-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. Prefetch handlers should inherit either
|
||||||
|
* from CoreCourseModuleActivityPrefetchHandlerBase or CoreCourseModuleResourcePrefetchHandlerBase, depending on whether
|
||||||
|
* they are an activity or a resource. It's not recommended to inherit from this class directly.
|
||||||
|
*/
|
||||||
|
export class CoreCourseModulePrefetchHandlerBase implements CoreCourseModulePrefetchHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the handler.
|
||||||
|
*/
|
||||||
|
name = 'CoreCourseModulePrefetchHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the module. It should match the "modname" of the module returned in core_course_get_contents.
|
||||||
|
*/
|
||||||
|
modName = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler's component.
|
||||||
|
*/
|
||||||
|
component = 'core_module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The RegExp to check updates. If a module has an update whose name matches this RegExp, the module will be marked
|
||||||
|
* as outdated. This RegExp is ignored if hasUpdates function is defined.
|
||||||
|
*/
|
||||||
|
updatesNames = /^.*files$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, this module will be ignored when determining the status of a list of modules. The module will
|
||||||
|
* still be downloaded when downloading the section/course, it only affects whether the button should be displayed.
|
||||||
|
*/
|
||||||
|
skipListStatus = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of download promises to prevent downloading the module twice at the same time.
|
||||||
|
*/
|
||||||
|
protected downloadPromises: { [s: string]: { [s: string]: Promise<void> } } = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an ongoing download to the downloadPromises list. When the promise finishes it will be removed.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param promise Promise to add.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise of the current download.
|
||||||
|
*/
|
||||||
|
async addOngoingDownload(id: number, promise: Promise<void>, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
const uniqueId = this.getUniqueId(id);
|
||||||
|
|
||||||
|
if (!this.downloadPromises[siteId]) {
|
||||||
|
this.downloadPromises[siteId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadPromises[siteId][uniqueId] = promise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.downloadPromises[siteId][uniqueId];
|
||||||
|
} finally {
|
||||||
|
delete this.downloadPromises[siteId][uniqueId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of content files that can be downloaded.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @return List of files.
|
||||||
|
*/
|
||||||
|
getContentDownloadableFiles(module: CoreCourseWSModule): CoreCourseModuleContentFile[] {
|
||||||
|
if (!module.contents?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return module.contents.filter((content) => this.isFileDownloadable(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the download size of a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the size.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getDownloadSize(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreFileSizeSum> {
|
||||||
|
try {
|
||||||
|
const files = await this.getFiles(module, courseId);
|
||||||
|
|
||||||
|
return await CorePluginFileDelegate.instance.getFilesDownloadSize(files);
|
||||||
|
} catch {
|
||||||
|
return { size: -1, total: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the downloaded size of a module. If not defined, we'll use getFiles to calculate it (it can be slow).
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Size, or promise resolved with the size.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getDownloadedSize(module: CoreCourseWSModule, courseId: number): Promise<number> {
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return CoreFilepool.instance.getFilesSizeByComponent(siteId, this.component, module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
// To be overridden.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns module intro files.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved with list of intro files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getIntroFiles(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
return this.getIntroFilesFromInstance(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns module intro files from instance.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param instance The instance to get the intro files (book, assign, ...). If not defined, module will be used.
|
||||||
|
* @return List of intro files.
|
||||||
|
*/
|
||||||
|
getIntroFilesFromInstance(module: CoreCourseWSModule, instance?: ModuleInstance): CoreWSExternalFile[] {
|
||||||
|
if (instance) {
|
||||||
|
if (typeof instance.introfiles != 'undefined') {
|
||||||
|
return instance.introfiles;
|
||||||
|
} else if (instance.intro) {
|
||||||
|
return CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(instance.intro);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (module.description) {
|
||||||
|
return CoreFilepool.instance.extractDownloadableFilesFromHtmlAsFakeFileObjects(module.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If there's an ongoing download for a certain identifier return it.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise of the current download.
|
||||||
|
*/
|
||||||
|
async getOngoingDownload(id: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isDownloading(id, siteId)) {
|
||||||
|
// There's already a download ongoing, return the promise.
|
||||||
|
return this.downloadPromises[siteId][this.getUniqueId(id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create unique identifier using component and id.
|
||||||
|
*
|
||||||
|
* @param id Unique ID inside component.
|
||||||
|
* @return Unique ID.
|
||||||
|
*/
|
||||||
|
getUniqueId(id: number): string {
|
||||||
|
return this.component + '#' + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID.
|
||||||
|
* @param courseId The course ID the module belongs to.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate WS calls needed to determine module status (usually, to check if module is downloadable).
|
||||||
|
* It doesn't need to invalidate check updates. It should NOT invalidate files nor all the prefetched data.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Promise resolved when invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
invalidateModule(module: CoreCourseWSModule, courseId: number): Promise<void> {
|
||||||
|
return CoreCourse.instance.invalidateModule(module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a module can be downloaded. If the function is not defined, we assume that all modules are downloadable.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Whether the module can be downloaded. The promise should never be rejected.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async isDownloadable(module: CoreCourseWSModule, courseId: number): Promise<boolean> {
|
||||||
|
// By default, mark all instances as downloadable.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a there's an ongoing download for the given identifier.
|
||||||
|
*
|
||||||
|
* @param id Unique identifier per component.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return True if downloading, false otherwise.
|
||||||
|
*/
|
||||||
|
isDownloading(id: number, siteId?: string): boolean {
|
||||||
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
return !!(this.downloadPromises[siteId] && this.downloadPromises[siteId][this.getUniqueId(id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is downloadable.
|
||||||
|
*
|
||||||
|
* @param file File to check.
|
||||||
|
* @return Whether the file is downloadable.
|
||||||
|
*/
|
||||||
|
isFileDownloadable(file: CoreCourseModuleContentFile): boolean {
|
||||||
|
return file.type === 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module contents into module.contents if they aren't loaded already.
|
||||||
|
*
|
||||||
|
* @param module Module to load the contents.
|
||||||
|
* @param courseId The course ID. Recommended to speed up the process and minimize data usage.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async loadContents(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
// To be overridden.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove module downloaded files. If not defined, we'll use getFiles to remove them (slow).
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
removeFiles(module: CoreCourseWSModule, courseId: number): Promise<void> {
|
||||||
|
return CoreFilepool.instance.removeFilesByComponent(CoreSites.instance.getCurrentSiteId(), this.component, module.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties a module instance should have to be able to retrieve its intro files.
|
||||||
|
*/
|
||||||
|
type ModuleInstance = {
|
||||||
|
introfiles?: CoreWSExternalFile[];
|
||||||
|
intro?: string;
|
||||||
|
};
|
|
@ -0,0 +1,203 @@
|
||||||
|
// (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 { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { CoreCourse, CoreCourseWSModule } from '../services/course';
|
||||||
|
import { CoreCourseModulePrefetchHandlerBase } from './module-prefetch-handler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base prefetch handler to be registered in CoreCourseModulePrefetchDelegate. It is useful to minimize the amount of
|
||||||
|
* functions that handlers need to implement. It also provides some helper features like preventing a module to be
|
||||||
|
* downloaded twice at the same time.
|
||||||
|
*
|
||||||
|
* If your handler inherits from this service, you just need to override the functions that you want to change.
|
||||||
|
*
|
||||||
|
* This class should be used for RESOURCES whose main purpose is downloading files present in module.contents.
|
||||||
|
*/
|
||||||
|
export class CoreCourseResourcePrefetchHandlerBase extends CoreCourseModulePrefetchHandlerBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
download(module: CoreCourseWSModule, courseId: number, dirPath?: string): Promise<void> {
|
||||||
|
return this.downloadOrPrefetch(module, courseId, false, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param prefetch True to prefetch, false to download right away.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files. This is to keep the files
|
||||||
|
* relative paths and make the package work in an iframe. Undefined to download the files
|
||||||
|
* in the filepool root folder.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
if (!CoreApp.instance.isOnline()) {
|
||||||
|
// Cannot download in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
if (this.isDownloading(module.id, siteId)) {
|
||||||
|
// There's already a download ongoing for this module, return the promise.
|
||||||
|
return this.getOngoingDownload(module.id, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get module info to be able to handle links.
|
||||||
|
const prefetchPromise = this.performDownloadOrPrefetch(siteId, module, courseId, !!prefetch, dirPath);
|
||||||
|
|
||||||
|
return this.addOngoingDownload(module.id, prefetchPromise, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download or prefetch the content.
|
||||||
|
*
|
||||||
|
* @param module The module object returned by WS.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param prefetch True to prefetch, false to download right away.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when all content is downloaded.
|
||||||
|
*/
|
||||||
|
protected async performDownloadOrPrefetch(
|
||||||
|
siteId: string,
|
||||||
|
module: CoreCourseWSModule,
|
||||||
|
courseId: number,
|
||||||
|
prefetch: boolean,
|
||||||
|
dirPath?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Get module info to be able to handle links.
|
||||||
|
await CoreCourse.instance.getModuleBasicInfo(module.id, siteId);
|
||||||
|
|
||||||
|
// Load module contents (ignore cache so we always have the latest data).
|
||||||
|
await this.loadContents(module, courseId, true);
|
||||||
|
|
||||||
|
// Get the intro files.
|
||||||
|
const introFiles = await this.getIntroFiles(module, courseId, true);
|
||||||
|
|
||||||
|
const contentFiles = this.getContentDownloadableFiles(module);
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
if (dirPath) {
|
||||||
|
// Download intro files in filepool root folder.
|
||||||
|
promises.push(
|
||||||
|
CoreFilepool.instance.downloadOrPrefetchFiles(siteId, introFiles, prefetch, false, this.component, module.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download content files inside dirPath.
|
||||||
|
promises.push(CoreFilepool.instance.downloadOrPrefetchPackage(
|
||||||
|
siteId,
|
||||||
|
contentFiles,
|
||||||
|
prefetch,
|
||||||
|
this.component,
|
||||||
|
module.id,
|
||||||
|
undefined,
|
||||||
|
dirPath,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// No dirPath, download everything in filepool root folder.
|
||||||
|
promises.push(CoreFilepool.instance.downloadOrPrefetchPackage(
|
||||||
|
siteId,
|
||||||
|
introFiles.concat(contentFiles),
|
||||||
|
prefetch,
|
||||||
|
this.component,
|
||||||
|
module.id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(CoreFilterHelper.instance.getFilters('module', module.id, { courseId }));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of files. If not defined, we'll assume they're in module.contents.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @return Promise resolved with the list of files.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getFiles(module: CoreCourseWSModule, courseId: number, single?: boolean): Promise<CoreWSExternalFile[]> {
|
||||||
|
// Load module contents if needed.
|
||||||
|
await this.loadContents(module, courseId);
|
||||||
|
|
||||||
|
const files = await this.getIntroFiles(module, courseId);
|
||||||
|
|
||||||
|
return files.concat(this.getContentDownloadableFiles(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID.
|
||||||
|
* @param courseId The course ID the module belongs to.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
|
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
CoreCourse.instance.invalidateModule(moduleId),
|
||||||
|
CoreFilepool.instance.invalidateFilesByComponent(siteId, this.component, moduleId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load module contents into module.contents if they aren't loaded already.
|
||||||
|
*
|
||||||
|
* @param module Module to load the contents.
|
||||||
|
* @param courseId The course ID. Recommended to speed up the process and minimize data usage.
|
||||||
|
* @param ignoreCache True if it should ignore cached data (it will always fail in offline or server down).
|
||||||
|
* @return Promise resolved when loaded.
|
||||||
|
*/
|
||||||
|
loadContents(module: CoreCourseWSModule, courseId: number, ignoreCache?: boolean): Promise<void> {
|
||||||
|
return CoreCourse.instance.loadModuleContents(module, courseId, undefined, false, ignoreCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param dirPath Path of the directory where to store all the content files.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
prefetch(module: CoreCourseWSModule, courseId?: number, single?: boolean, dirPath?: string): Promise<void> {
|
||||||
|
courseId = courseId || module.course;
|
||||||
|
if (!courseId) {
|
||||||
|
throw new CoreError('Course ID not supplied.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.downloadOrPrefetch(module, courseId, true, dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreBlockComponentsModule } from '@features/block/components/components.module';
|
||||||
|
import { CoreCourseFormatComponent } from './format/format';
|
||||||
|
import { CoreCourseModuleComponent } from './module/module';
|
||||||
|
import { CoreCourseModuleCompletionComponent } from './module-completion/module-completion';
|
||||||
|
import { CoreCourseModuleDescriptionComponent } from './module-description/module-description';
|
||||||
|
import { CoreCourseSectionSelectorComponent } from './section-selector/section-selector';
|
||||||
|
import { CoreCourseTagAreaComponent } from './tag-area/tag-area';
|
||||||
|
import { CoreCourseUnsupportedModuleComponent } from './unsupported-module/unsupported-module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreCourseFormatComponent,
|
||||||
|
CoreCourseModuleComponent,
|
||||||
|
CoreCourseModuleCompletionComponent,
|
||||||
|
CoreCourseModuleDescriptionComponent,
|
||||||
|
CoreCourseSectionSelectorComponent,
|
||||||
|
CoreCourseTagAreaComponent,
|
||||||
|
CoreCourseUnsupportedModuleComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreBlockComponentsModule,
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreCourseFormatComponent,
|
||||||
|
CoreCourseModuleComponent,
|
||||||
|
CoreCourseModuleCompletionComponent,
|
||||||
|
CoreCourseModuleDescriptionComponent,
|
||||||
|
CoreCourseSectionSelectorComponent,
|
||||||
|
CoreCourseTagAreaComponent,
|
||||||
|
CoreCourseUnsupportedModuleComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseComponentsModule {}
|
|
@ -0,0 +1,168 @@
|
||||||
|
<!-- Buttons to add to the header. *ngIf is needed, otherwise the component is executed too soon and doesn't find the header. -->
|
||||||
|
<core-navbar-buttons slot="end" *ngIf="loaded">
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item [hidden]="!displaySectionSelector || !sections || !sections.length" [priority]="500"
|
||||||
|
[content]="'core.course.sections' | translate" (action)="showSectionSelector()" iconAction="menu">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
|
||||||
|
<core-block-course-blocks *ngIf="loaded" [courseId]="course!.id" [hideBlocks]="!displayBlocks" [downloadEnabled]="downloadEnabled"
|
||||||
|
[hideBottomBlocks]="selectedSection && selectedSection.id == allSectionsId && canLoadMore">
|
||||||
|
|
||||||
|
<core-dynamic-component [component]="courseFormatComponent" [data]="data">
|
||||||
|
<!-- Default course format. -->
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<!-- Section selector. -->
|
||||||
|
<core-dynamic-component [component]="sectionSelectorComponent" [data]="data">
|
||||||
|
|
||||||
|
<div *ngIf="displaySectionSelector && sections && hasSeveralSections"
|
||||||
|
class="ion-text-wrap clearfix ion-justify-content-between core-button-selector-row"
|
||||||
|
[class.core-section-download]="downloadEnabled">
|
||||||
|
<ion-button class="core-button-select button-no-uppercase" (click)="showSectionSelector()"
|
||||||
|
aria-haspopup="true" [attr.aria-expanded]="sectionSelectorExpanded"
|
||||||
|
id="core-course-section-button" expand="block"> <!-- @todo: attr.aria-label? -->
|
||||||
|
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||||
|
<span class="core-button-select-text">
|
||||||
|
<core-format-text *ngIf="selectedSection" [text]="selectedSection.name" contextLevel="course"
|
||||||
|
[contextInstanceId]="course?.id" [clean]="true" [singleLine]="true">
|
||||||
|
</core-format-text>
|
||||||
|
<span *ngIf="!selectedSection">{{ 'core.course.sections' | translate }}</span>
|
||||||
|
</span>
|
||||||
|
<div class="select-icon" slot="end"><div class="select-icon-inner"></div></div>
|
||||||
|
</ion-button>
|
||||||
|
<!-- Section download. -->
|
||||||
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
|
</div>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
|
<!-- Course summary. By default we only display the course progress. -->
|
||||||
|
<core-dynamic-component [component]="courseSummaryComponent" [data]="data">
|
||||||
|
<ion-list lines="none" class="core-format-progress-list"
|
||||||
|
*ngIf="imageThumb || (selectedSection?.id == allSectionsId && progress !== undefined) ||
|
||||||
|
(selectedSection && selectedSection.id != allSectionsId &&
|
||||||
|
(selectedSection.availabilityinfo || selectedSection.visible === 0))">
|
||||||
|
<div *ngIf="imageThumb" class="core-course-thumb">
|
||||||
|
<img [src]="imageThumb" core-external-content alt=""/>
|
||||||
|
</div>
|
||||||
|
<ng-container *ngIf="selectedSection">
|
||||||
|
<ion-item class="core-course-progress"
|
||||||
|
*ngIf="selectedSection?.id == allSectionsId && progress !== undefined">
|
||||||
|
<core-progress-bar [progress]="progress"></core-progress-bar>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item *ngIf="selectedSection && selectedSection.id != allSectionsId &&
|
||||||
|
(selectedSection.availabilityinfo || selectedSection.visible === 0)">
|
||||||
|
<ion-badge color="secondary" class="ion-text-wrap"
|
||||||
|
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible !== false">
|
||||||
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" class="ion-text-wrap"
|
||||||
|
*ngIf="selectedSection.visible === 0 && selectedSection.uservisible === false">
|
||||||
|
{{ 'core.notavailable' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" class="ion-text-wrap" *ngIf="selectedSection.availabilityinfo">
|
||||||
|
<core-format-text [text]="selectedSection.availabilityinfo" contextLevel="course"
|
||||||
|
[contextInstanceId]="course?.id">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
|
<!-- Single section. -->
|
||||||
|
<div *ngIf="selectedSection && selectedSection.id != allSectionsId">
|
||||||
|
<core-dynamic-component [component]="singleSectionComponent" [data]="data">
|
||||||
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: selectedSection}"></ng-container>
|
||||||
|
<core-empty-box *ngIf="!selectedSection.hasContent" icon="fas-th-large"
|
||||||
|
[message]="'core.course.nocontentavailable' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
</core-dynamic-component>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multiple sections. -->
|
||||||
|
<div *ngIf="selectedSection && selectedSection.id == allSectionsId">
|
||||||
|
<core-dynamic-component [component]="allSectionsComponent" [data]="data">
|
||||||
|
<ng-container *ngFor="let section of sections; index as i">
|
||||||
|
<ng-container *ngIf="i <= showSectionId">
|
||||||
|
<ng-container *ngTemplateOutlet="sectionTemplate; context: {section: section}"></ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</core-dynamic-component>
|
||||||
|
|
||||||
|
<core-infinite-loading [enabled]="canLoadMore" (action)="showMoreActivities($event)"></core-infinite-loading>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-buttons class="ion-padding core-course-section-nav-buttons safe-padding-horizontal"
|
||||||
|
*ngIf="displaySectionSelector && sections?.length">
|
||||||
|
<ion-button *ngIf="previousSection" color="medium" (click)="sectionChanged(previousSection)"
|
||||||
|
title="{{ 'core.previous' | translate }}">
|
||||||
|
<ion-icon name="fas-chevron-left" slot="icon-only"></ion-icon>
|
||||||
|
<core-format-text class="accesshide" [text]="previousSection.name" contextLevel="course"
|
||||||
|
[contextInstanceId]="course?.id">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="nextSection" (click)="sectionChanged(nextSection)" title="{{ 'core.next' | translate }}">
|
||||||
|
<ion-icon name="fas-chevron-right" slot="icon-only"></ion-icon>
|
||||||
|
<core-format-text class="accesshide" [text]="nextSection.name" contextLevel="course"
|
||||||
|
[contextInstanceId]="course?.id">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</core-loading>
|
||||||
|
</core-dynamic-component>
|
||||||
|
</core-block-course-blocks>
|
||||||
|
|
||||||
|
<!-- Template to render a section. -->
|
||||||
|
<ng-template #sectionTemplate let-section="section">
|
||||||
|
<section ion-list *ngIf="!section.hiddenbynumsections && section.id != allSectionsId && section.id != stealthModulesSectionId">
|
||||||
|
<!-- Title is only displayed when viewing all sections. -->
|
||||||
|
<ion-item-divider *ngIf="selectedSection?.id == allSectionsId && section.name" class="ion-text-wrap" color="light"
|
||||||
|
[class.core-section-download]="downloadEnabled"
|
||||||
|
[class.item-dimmed]="section.visible === 0 || section.uservisible === false">
|
||||||
|
<ion-label>
|
||||||
|
<core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||||
|
<!-- Section download. -->
|
||||||
|
<ng-container *ngTemplateOutlet="sectionDownloadTemplate; context: {section: section}"></ng-container>
|
||||||
|
<p *ngIf="section.visible === 0 || section.availabilityinfo">
|
||||||
|
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
|
||||||
|
{{ 'core.notavailable' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" *ngIf="section.availabilityinfo" class="ion-text-wrap">
|
||||||
|
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="section.summary">
|
||||||
|
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<ng-container *ngFor="let module of section.modules">
|
||||||
|
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [courseId]="course?.id"
|
||||||
|
[downloadEnabled]="downloadEnabled" [section]="section" (completionChanged)="onCompletionChange($event)"
|
||||||
|
(statusChanged)="onModuleStatusChange()">
|
||||||
|
</core-course-module>
|
||||||
|
</ng-container>
|
||||||
|
</section>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<!-- Template to render a section download button/progress. -->
|
||||||
|
<ng-template #sectionDownloadTemplate let-section="section">
|
||||||
|
<div *ngIf="section && downloadEnabled" class="core-button-spinner ion-float-end">
|
||||||
|
<!-- Download progress. -->
|
||||||
|
<ion-badge class="core-course-download-section-progress"
|
||||||
|
*ngIf="section.isDownloading && section.total > 0 && section.count < section.total">
|
||||||
|
{{section.count}} / {{section.total}}
|
||||||
|
</ion-badge>
|
||||||
|
|
||||||
|
<core-download-refresh [status]="section.downloadStatus" [enabled]="downloadEnabled" (action)="prefetch(section)"
|
||||||
|
[loading]="section.isDownloading || section.isCalculating" [canTrustDownload]="section.canCheckUpdates">
|
||||||
|
</core-download-refresh>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,77 @@
|
||||||
|
// ion-app.app-root ion-badge.core-course-download-section-progress {
|
||||||
|
// display: block;
|
||||||
|
// @include float(start);
|
||||||
|
// @include margin(12px, 12px, null, 12px);
|
||||||
|
// }
|
||||||
|
|
||||||
|
:host {
|
||||||
|
|
||||||
|
.core-format-progress-list {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
.progress-bar-fallback,
|
||||||
|
&[value]::-webkit-progress-bar {
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-course-thumb {
|
||||||
|
display: none;
|
||||||
|
height: 150px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo
|
||||||
|
// .item-divider {
|
||||||
|
// .label {
|
||||||
|
// margin-top: 0;
|
||||||
|
// margin-bottom: 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// core-format-text {
|
||||||
|
// line-height: 44px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ion-badge core-format-text {
|
||||||
|
// line-height: normal;
|
||||||
|
// margin-bottom: 9px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &.core-section-download .label{
|
||||||
|
// @include margin(null, 0, null, null);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// div.core-section-download {
|
||||||
|
// @include padding(null, 0, null, null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .core-button-selector-row {
|
||||||
|
// @include safe-area-padding-start($content-padding !important, $content-padding);
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,633 @@
|
||||||
|
// (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,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
SimpleChange,
|
||||||
|
Output,
|
||||||
|
EventEmitter,
|
||||||
|
ViewChildren,
|
||||||
|
QueryList,
|
||||||
|
Type,
|
||||||
|
ViewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import {
|
||||||
|
CoreCourse,
|
||||||
|
CoreCourseProvider,
|
||||||
|
} from '@features/course/services/course';
|
||||||
|
import {
|
||||||
|
CoreCourseHelper,
|
||||||
|
CoreCourseModule,
|
||||||
|
CoreCourseModuleCompletionData,
|
||||||
|
CoreCourseSection,
|
||||||
|
CoreCourseSectionWithStatus,
|
||||||
|
} from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreEventObserver, CoreEvents, CoreEventSectionStatusChangedData, CoreEventSelectCourseTabData } from '@singletons/events';
|
||||||
|
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||||
|
import { CoreBlockCourseBlocksComponent } from '@features/block/components/course-blocks/course-blocks';
|
||||||
|
import { ModalController } from '@singletons';
|
||||||
|
import { CoreCourseSectionSelectorComponent } from '../section-selector/section-selector';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display course contents using a certain format. If the format isn't found, use default one.
|
||||||
|
*
|
||||||
|
* The inputs of this component will be shared with the course format components. Please use CoreCourseFormatDelegate
|
||||||
|
* to register your handler for course formats.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <core-course-format [course]="course" [sections]="sections" (completionChanged)="onCompletionChange()"></core-course-format>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-format',
|
||||||
|
templateUrl: 'core-course-format.html',
|
||||||
|
styleUrls: ['format.scss'],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
|
static readonly LOAD_MORE_ACTIVITIES = 20; // How many activities should load each time showMoreActivities is called.
|
||||||
|
|
||||||
|
@Input() course?: CoreCourseAnyCourseData; // The course to render.
|
||||||
|
@Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections. The status will be calculated in this component.
|
||||||
|
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
|
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||||
|
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||||
|
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||||
|
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
|
||||||
|
|
||||||
|
@ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList<CoreDynamicComponent>;
|
||||||
|
@ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent?: CoreBlockCourseBlocksComponent;
|
||||||
|
|
||||||
|
// All the possible component classes.
|
||||||
|
courseFormatComponent?: Type<unknown>;
|
||||||
|
courseSummaryComponent?: Type<unknown>;
|
||||||
|
sectionSelectorComponent?: Type<unknown>;
|
||||||
|
singleSectionComponent?: Type<unknown>;
|
||||||
|
allSectionsComponent?: Type<unknown>;
|
||||||
|
|
||||||
|
canLoadMore = false;
|
||||||
|
showSectionId = 0;
|
||||||
|
sectionSelectorExpanded = false;
|
||||||
|
data: Record<string, unknown> = {}; // Data to pass to the components.
|
||||||
|
|
||||||
|
displaySectionSelector?: boolean;
|
||||||
|
displayBlocks?: boolean;
|
||||||
|
selectedSection?: CoreCourseSection;
|
||||||
|
previousSection?: CoreCourseSection;
|
||||||
|
nextSection?: CoreCourseSection;
|
||||||
|
allSectionsId: number = CoreCourseProvider.ALL_SECTIONS_ID;
|
||||||
|
stealthModulesSectionId: number = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
|
loaded = false;
|
||||||
|
hasSeveralSections?: boolean;
|
||||||
|
imageThumb?: string;
|
||||||
|
progress?: number;
|
||||||
|
|
||||||
|
protected sectionStatusObserver?: CoreEventObserver;
|
||||||
|
protected selectTabObserver?: CoreEventObserver;
|
||||||
|
protected lastCourseFormat?: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected content: IonContent,
|
||||||
|
) {
|
||||||
|
// Pass this instance to all components so they can use its methods and properties.
|
||||||
|
this.data.coreCourseFormatComponent = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Listen for section status changes.
|
||||||
|
this.sectionStatusObserver = CoreEvents.on<CoreEventSectionStatusChangedData>(
|
||||||
|
CoreEvents.SECTION_STATUS_CHANGED,
|
||||||
|
async (data) => {
|
||||||
|
if (!this.downloadEnabled || !this.sections?.length || !data.sectionId || data.courseId != this.course?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the affected section is being downloaded.
|
||||||
|
// If so, we don't update section status because it'll already be updated when the download finishes.
|
||||||
|
const downloadId = CoreCourseHelper.instance.getSectionDownloadId({ id: data.sectionId });
|
||||||
|
if (CoreCourseModulePrefetchDelegate.instance.isBeingDownloaded(downloadId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the affected section.
|
||||||
|
const section = this.sections.find(section => section.id == data.sectionId);
|
||||||
|
if (!section) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate the status.
|
||||||
|
await CoreCourseHelper.instance.calculateSectionStatus(section, this.course.id, false);
|
||||||
|
|
||||||
|
if (section.isDownloading && !CoreCourseModulePrefetchDelegate.instance.isBeingDownloaded(downloadId)) {
|
||||||
|
// All the modules are now downloading, set a download all promise.
|
||||||
|
this.prefetch(section);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CoreSites.instance.getCurrentSiteId(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for select course tab events to select the right section if needed.
|
||||||
|
this.selectTabObserver = CoreEvents.on<CoreEventSelectCourseTabData>(CoreEvents.SELECT_COURSE_TAB, (data) => {
|
||||||
|
if (data.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let section: CoreCourseSection | undefined;
|
||||||
|
|
||||||
|
if (typeof data.sectionId != 'undefined' && data.sectionId != null && this.sections) {
|
||||||
|
section = this.sections.find((section) => section.id == data.sectionId);
|
||||||
|
} else if (typeof data.sectionNumber != 'undefined' && data.sectionNumber != null && this.sections) {
|
||||||
|
section = this.sections.find((section) => section.section == data.sectionNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section) {
|
||||||
|
this.sectionChanged(section);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
this.setInputData();
|
||||||
|
|
||||||
|
if (changes.course && this.course) {
|
||||||
|
// Course has changed, try to get the components.
|
||||||
|
this.getComponents();
|
||||||
|
|
||||||
|
this.displaySectionSelector = CoreCourseFormatDelegate.instance.displaySectionSelector(this.course);
|
||||||
|
this.displayBlocks = CoreCourseFormatDelegate.instance.displayBlocks(this.course);
|
||||||
|
this.progress = 'progress' in this.course && this.course.progress !== undefined && this.course.progress >= 0 &&
|
||||||
|
this.course.completionusertracked !== false ? this.course.progress : undefined;
|
||||||
|
if ('overviewfiles' in this.course) {
|
||||||
|
this.imageThumb = this.course.overviewfiles?.[0]?.fileurl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.sections && this.sections) {
|
||||||
|
this.treatSections(this.sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.downloadEnabled && (changes.downloadEnabled || changes.sections)) {
|
||||||
|
this.calculateSectionsStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the input data for components.
|
||||||
|
*/
|
||||||
|
protected setInputData(): void {
|
||||||
|
this.data.course = this.course;
|
||||||
|
this.data.sections = this.sections;
|
||||||
|
this.data.initialSectionId = this.initialSectionId;
|
||||||
|
this.data.initialSectionNumber = this.initialSectionNumber;
|
||||||
|
this.data.downloadEnabled = this.downloadEnabled;
|
||||||
|
this.data.moduleId = this.moduleId;
|
||||||
|
this.data.completionChanged = this.completionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the components classes.
|
||||||
|
*/
|
||||||
|
protected async getComponents(): Promise<void> {
|
||||||
|
if (!this.course || this.course.format == this.lastCourseFormat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format has changed or it's the first time, load all the components.
|
||||||
|
this.lastCourseFormat = this.course.format;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCourseFormatComponent(),
|
||||||
|
this.loadCourseSummaryComponent(),
|
||||||
|
this.loadSectionSelectorComponent(),
|
||||||
|
this.loadSingleSectionComponent(),
|
||||||
|
this.loadAllSectionsComponent(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load course format component.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadCourseFormatComponent(): Promise<void> {
|
||||||
|
this.courseFormatComponent = await CoreCourseFormatDelegate.instance.getCourseFormatComponent(this.course!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load course summary component.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadCourseSummaryComponent(): Promise<void> {
|
||||||
|
this.courseSummaryComponent = await CoreCourseFormatDelegate.instance.getCourseSummaryComponent(this.course!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load section selector component.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadSectionSelectorComponent(): Promise<void> {
|
||||||
|
this.sectionSelectorComponent = await CoreCourseFormatDelegate.instance.getSectionSelectorComponent(this.course!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load single section component.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadSingleSectionComponent(): Promise<void> {
|
||||||
|
this.singleSectionComponent = await CoreCourseFormatDelegate.instance.getSingleSectionComponent(this.course!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all sections component.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadAllSectionsComponent(): Promise<void> {
|
||||||
|
this.allSectionsComponent = await CoreCourseFormatDelegate.instance.getAllSectionsComponent(this.course!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Treat received sections.
|
||||||
|
*
|
||||||
|
* @param sections Sections to treat.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async treatSections(sections: CoreCourseSection[]): Promise<void> {
|
||||||
|
const hasAllSections = sections[0].id == CoreCourseProvider.ALL_SECTIONS_ID;
|
||||||
|
this.hasSeveralSections = sections.length > 2 || (sections.length == 2 && !hasAllSections);
|
||||||
|
|
||||||
|
if (this.selectedSection) {
|
||||||
|
// We have a selected section, but the list has changed. Search the section in the list.
|
||||||
|
let newSection = sections.find(section => this.compareSections(section, this.selectedSection!));
|
||||||
|
|
||||||
|
if (!newSection) {
|
||||||
|
// Section not found, calculate which one to use.
|
||||||
|
newSection = await CoreCourseFormatDelegate.instance.getCurrentSection(this.course!, sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sectionChanged(newSection);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is no selected section yet, calculate which one to load.
|
||||||
|
if (!this.hasSeveralSections) {
|
||||||
|
// Always load "All sections" to display the section title. If it isn't there just load the section.
|
||||||
|
this.loaded = true;
|
||||||
|
this.sectionChanged(sections[0]);
|
||||||
|
} else if (this.initialSectionId || this.initialSectionNumber) {
|
||||||
|
// We have an input indicating the section ID to load. Search the section.
|
||||||
|
const section = sections.find((section) => {
|
||||||
|
if (section.id != this.initialSectionId && (!section.section || section.section != this.initialSectionNumber)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't load the section if it cannot be viewed by the user.
|
||||||
|
if (section && this.canViewSection(section)) {
|
||||||
|
this.loaded = true;
|
||||||
|
this.sectionChanged(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.loaded) {
|
||||||
|
// No section specified, not found or not visible, get current section.
|
||||||
|
const section = await CoreCourseFormatDelegate.instance.getCurrentSection(this.course!, sections);
|
||||||
|
|
||||||
|
this.loaded = true;
|
||||||
|
this.sectionChanged(section);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the section selector modal.
|
||||||
|
*/
|
||||||
|
async showSectionSelector(): Promise<void> {
|
||||||
|
if (this.sectionSelectorExpanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sectionSelectorExpanded = true;
|
||||||
|
|
||||||
|
const modal = await ModalController.instance.create({
|
||||||
|
component: CoreCourseSectionSelectorComponent,
|
||||||
|
componentProps: {
|
||||||
|
course: this.course,
|
||||||
|
sections: this.sections,
|
||||||
|
selected: this.selectedSection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await modal.present();
|
||||||
|
|
||||||
|
const result = await modal.onWillDismiss();
|
||||||
|
|
||||||
|
this.sectionSelectorExpanded = false;
|
||||||
|
if (result?.data) {
|
||||||
|
this.sectionChanged(result?.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when selected section changes.
|
||||||
|
*
|
||||||
|
* @param newSection The new selected section.
|
||||||
|
*/
|
||||||
|
sectionChanged(newSection: CoreCourseSection): void {
|
||||||
|
const previousValue = this.selectedSection;
|
||||||
|
this.selectedSection = newSection;
|
||||||
|
this.data.section = this.selectedSection;
|
||||||
|
|
||||||
|
if (newSection.id != this.allSectionsId) {
|
||||||
|
// Select next and previous sections to show the arrows.
|
||||||
|
const i = this.sections!.findIndex((value) => this.compareSections(value, this.selectedSection!));
|
||||||
|
|
||||||
|
let j: number;
|
||||||
|
for (j = i - 1; j >= 1; j--) {
|
||||||
|
if (this.canViewSection(this.sections![j])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.previousSection = j >= 1 ? this.sections![j] : undefined;
|
||||||
|
|
||||||
|
for (j = i + 1; j < this.sections!.length; j++) {
|
||||||
|
if (this.canViewSection(this.sections![j])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.nextSection = j < this.sections!.length ? this.sections![j] : undefined;
|
||||||
|
} else {
|
||||||
|
this.previousSection = undefined;
|
||||||
|
this.nextSection = undefined;
|
||||||
|
this.canLoadMore = false;
|
||||||
|
this.showSectionId = 0;
|
||||||
|
this.showMoreActivities();
|
||||||
|
if (this.downloadEnabled) {
|
||||||
|
this.calculateSectionsStatus(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moduleId && typeof previousValue == 'undefined') {
|
||||||
|
setTimeout(() => {
|
||||||
|
CoreDomUtils.instance.scrollToElementBySelector(this.content, '#core-course-module-' + this.moduleId);
|
||||||
|
}, 200);
|
||||||
|
} else {
|
||||||
|
this.content.scrollToTop(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousValue || previousValue.id != newSection.id) {
|
||||||
|
// First load or section changed, add log in Moodle.
|
||||||
|
CoreUtils.instance.ignoreErrors(
|
||||||
|
CoreCourse.instance.logView(this.course!.id, newSection.section, undefined, this.course!.fullname),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare if two sections are equal.
|
||||||
|
*
|
||||||
|
* @param section1 First section.
|
||||||
|
* @param section2 Second section.
|
||||||
|
* @return Whether they're equal.
|
||||||
|
*/
|
||||||
|
compareSections(section1: CoreCourseSection, section2: CoreCourseSection): boolean {
|
||||||
|
return section1 && section2 ? section1.id === section2.id : section1 === section2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the status of sections.
|
||||||
|
*
|
||||||
|
* @param refresh If refresh or not.
|
||||||
|
*/
|
||||||
|
protected calculateSectionsStatus(refresh?: boolean): void {
|
||||||
|
if (!this.sections || !this.course) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, refresh));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
|
||||||
|
*
|
||||||
|
* @param section Section to download.
|
||||||
|
* @param refresh Refresh clicked (not used).
|
||||||
|
*/
|
||||||
|
async prefetch(section: CoreCourseSectionWithStatus): Promise<void> {
|
||||||
|
section.isCalculating = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await CoreCourseHelper.instance.confirmDownloadSizeSection(this.course!.id, section, this.sections);
|
||||||
|
|
||||||
|
await this.prefetchSection(section, true);
|
||||||
|
} catch (error) {
|
||||||
|
// User cancelled or there was an error calculating the size.
|
||||||
|
if (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModal(error);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
section.isCalculating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a section.
|
||||||
|
*
|
||||||
|
* @param section The section to download.
|
||||||
|
* @param manual Whether the prefetch was started manually or it was automatically started because all modules
|
||||||
|
* are being downloaded.
|
||||||
|
*/
|
||||||
|
protected async prefetchSection(section: CoreCourseSectionWithStatus, manual?: boolean): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreCourseHelper.instance.prefetchSection(section, this.course!.id, this.sections);
|
||||||
|
} catch (error) {
|
||||||
|
// Don't show error message if it's an automatic download.
|
||||||
|
if (!manual) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param done Function to call when done.
|
||||||
|
* @param afterCompletionChange Whether the refresh is due to a completion change.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
|
||||||
|
const promises = this.dynamicComponents?.map(async (component) => {
|
||||||
|
await component.callComponentFunction('doRefresh', [refresher, done, afterCompletionChange]);
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
if (this.courseBlocksComponent) {
|
||||||
|
promises.push(this.courseBlocksComponent.doRefresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show more activities (only used when showing all the sections at the same time).
|
||||||
|
*
|
||||||
|
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||||
|
*/
|
||||||
|
showMoreActivities(infiniteComplete?: () => void): void {
|
||||||
|
this.canLoadMore = false;
|
||||||
|
|
||||||
|
const sections = this.sections || [];
|
||||||
|
let modulesLoaded = 0;
|
||||||
|
let i: number;
|
||||||
|
for (i = this.showSectionId + 1; i < sections.length; i++) {
|
||||||
|
if (!sections[i].hasContent || !sections[i].modules) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
modulesLoaded += sections[i].modules.reduce((total, module) => module.visibleoncoursepage !== 0 ? total + 1 : total, 0);
|
||||||
|
|
||||||
|
if (modulesLoaded >= CoreCourseFormatComponent.LOAD_MORE_ACTIVITIES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showSectionId = i;
|
||||||
|
this.canLoadMore = i < sections.length;
|
||||||
|
|
||||||
|
if (this.canLoadMore) {
|
||||||
|
// Check if any of the following sections have any content.
|
||||||
|
let thereAreMore = false;
|
||||||
|
for (i++; i < sections.length; i++) {
|
||||||
|
if (sections[i].hasContent && sections[i].modules && sections[i].modules?.length > 0) {
|
||||||
|
thereAreMore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.canLoadMore = thereAreMore;
|
||||||
|
}
|
||||||
|
|
||||||
|
infiniteComplete && infiniteComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.sectionStatusObserver && this.sectionStatusObserver.off();
|
||||||
|
this.selectTabObserver && this.selectTabObserver.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.dynamicComponents?.forEach((component) => {
|
||||||
|
component.callComponentFunction('ionViewDidEnter');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.downloadEnabled || !this.course || !this.sections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The download status of a section might have been changed from within a module page.
|
||||||
|
if (this.selectedSection && this.selectedSection.id !== CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||||
|
CoreCourseHelper.instance.calculateSectionStatus(this.selectedSection, this.course.id, false, false);
|
||||||
|
} else {
|
||||||
|
CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.dynamicComponents?.forEach((component) => {
|
||||||
|
component.callComponentFunction('ionViewDidLeave');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a section can be viewed.
|
||||||
|
*
|
||||||
|
* @param section The section to check.
|
||||||
|
* @return Whether the section can be viewed.
|
||||||
|
*/
|
||||||
|
canViewSection(section: CoreCourseSection): boolean {
|
||||||
|
return section.uservisible !== false && !section.hiddenbynumsections &&
|
||||||
|
section.id != CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The completion of any of the modules have changed.
|
||||||
|
*/
|
||||||
|
onCompletionChange(completionData: CoreCourseModuleCompletionData): void {
|
||||||
|
// Emit a new event for other components.
|
||||||
|
this.completionChanged.emit(completionData);
|
||||||
|
|
||||||
|
if (completionData.valueused !== false || !this.course || !('progress' in this.course) ||
|
||||||
|
typeof this.course.progress == 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the completion value is not used, the page won't be reloaded, so update the progress bar.
|
||||||
|
const completionModules = (<CoreCourseModule[]> [])
|
||||||
|
.concat(...this.sections!.map((section) => section.modules))
|
||||||
|
.map((module) => module.completion && module.completion > 0 ? 1 : module.completion)
|
||||||
|
.reduce((accumulator, currentValue) => (accumulator || 0) + (currentValue || 0));
|
||||||
|
|
||||||
|
const moduleProgressPercent = 100 / (completionModules || 1);
|
||||||
|
// Use min/max here to avoid floating point rounding errors over/under-flowing the progress bar.
|
||||||
|
if (completionData.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||||
|
this.course.progress = Math.min(100, this.course.progress + moduleProgressPercent);
|
||||||
|
} else {
|
||||||
|
this.course.progress = Math.max(0, this.course.progress - moduleProgressPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recalculate the download status of each section, in response to a module being downloaded.
|
||||||
|
*/
|
||||||
|
onModuleStatusChange(): void {
|
||||||
|
if (!this.downloadEnabled || !this.sections || !this.course) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreCourseHelper.instance.calculateSectionsStatus(this.sections, this.course.id, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<button *ngIf="completion" (click)="completionClicked($event)">
|
||||||
|
<img [src]="completionImage" [alt]="completionDescription">
|
||||||
|
</button>
|
|
@ -0,0 +1,13 @@
|
||||||
|
:host {
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
img {
|
||||||
|
padding: 5px;
|
||||||
|
width: 30px;
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,176 @@
|
||||||
|
// (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, Output, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreCourseModuleCompletionData } from '@features/course/services/course-helper';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to handle activity completion. It shows a checkbox with the current status, and allows manually changing
|
||||||
|
* the completion if it's allowed.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <core-course-module-completion [completion]="module.completiondata" [moduleName]="module.name"
|
||||||
|
* (completionChanged)="completionChanged()"></core-course-module-completion>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-module-completion',
|
||||||
|
templateUrl: 'core-course-module-completion.html',
|
||||||
|
styleUrls: ['module-completion.scss'],
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleCompletionComponent implements OnChanges {
|
||||||
|
|
||||||
|
@Input() completion?: CoreCourseModuleCompletionData; // The completion status.
|
||||||
|
@Input() moduleId?: number; // The name of the module this completion affects.
|
||||||
|
@Input() moduleName?: string; // The name of the module this completion affects.
|
||||||
|
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when completion changes.
|
||||||
|
|
||||||
|
completionImage?: string;
|
||||||
|
completionDescription?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||||
|
if (changes.completion && this.completion) {
|
||||||
|
this.showStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Completion clicked.
|
||||||
|
*
|
||||||
|
* @param e The click event.
|
||||||
|
*/
|
||||||
|
async completionClicked(e: Event): Promise<void> {
|
||||||
|
if (!this.completion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.completion.cmid == 'undefined' || this.completion.tracking !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||||
|
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await CoreCourse.instance.markCompletedManually(
|
||||||
|
this.completion.cmid,
|
||||||
|
this.completion.state === 1,
|
||||||
|
this.completion.courseId!,
|
||||||
|
this.completion.courseName,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.completion.valueused === false) {
|
||||||
|
this.showStatus();
|
||||||
|
if (response.offline) {
|
||||||
|
this.completion.offline = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.completionChanged.emit(this.completion);
|
||||||
|
} catch (error) {
|
||||||
|
this.completion.state = this.completion.state === 1 ? 0 : 1;
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errorchangecompletion', true);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set image and description to show as completion icon.
|
||||||
|
*/
|
||||||
|
protected async showStatus(): Promise<void> {
|
||||||
|
if (!this.completion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleName = this.moduleName || '';
|
||||||
|
let langKey: string | undefined;
|
||||||
|
let image: string | undefined;
|
||||||
|
|
||||||
|
if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||||
|
image = 'completion-manual-n';
|
||||||
|
langKey = 'core.completion-alt-manual-n';
|
||||||
|
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_MANUAL &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||||
|
image = 'completion-manual-y';
|
||||||
|
langKey = 'core.completion-alt-manual-y';
|
||||||
|
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_INCOMPLETE) {
|
||||||
|
image = 'completion-auto-n';
|
||||||
|
langKey = 'core.completion-alt-auto-n';
|
||||||
|
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE) {
|
||||||
|
image = 'completion-auto-y';
|
||||||
|
langKey = 'core.completion-alt-auto-y';
|
||||||
|
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_PASS) {
|
||||||
|
image = 'completion-auto-pass';
|
||||||
|
langKey = 'core.completion-alt-auto-pass';
|
||||||
|
} else if (this.completion.tracking === CoreCourseProvider.COMPLETION_TRACKING_AUTOMATIC &&
|
||||||
|
this.completion.state === CoreCourseProvider.COMPLETION_COMPLETE_FAIL) {
|
||||||
|
image = 'completion-auto-fail';
|
||||||
|
langKey = 'core.completion-alt-auto-fail';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image) {
|
||||||
|
if (this.completion.overrideby > 0) {
|
||||||
|
image += '-override';
|
||||||
|
}
|
||||||
|
this.completionImage = 'assets/img/completion/' + image + '.svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!moduleName || !this.moduleId || !langKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await CoreFilterHelper.instance.getFiltersAndFormatText(
|
||||||
|
moduleName,
|
||||||
|
'module',
|
||||||
|
this.moduleId,
|
||||||
|
{ clean: true, singleLine: true, shortenLength: 50, courseId: this.completion.courseId },
|
||||||
|
);
|
||||||
|
|
||||||
|
let translateParams: Record<string, unknown> = {
|
||||||
|
$a: result.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.completion.overrideby > 0) {
|
||||||
|
langKey += '-override';
|
||||||
|
|
||||||
|
const profile = await CoreUser.instance.getProfile(this.completion.overrideby, this.completion.courseId, true);
|
||||||
|
|
||||||
|
translateParams = {
|
||||||
|
$a: {
|
||||||
|
overrideuser: profile.fullname,
|
||||||
|
modname: result.text,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completionDescription = Translate.instance.instant(langKey, translateParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<ion-card *ngIf="description">
|
||||||
|
<ion-item class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<core-format-text [text]="description" [component]="component" [componentId]="componentId"
|
||||||
|
[maxHeight]="showFull && showFull !== 'false' ? 0 : 120" fullOnClick="true" [contextLevel]="contextLevel"
|
||||||
|
[contextInstanceId]="contextInstanceId" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item class="ion-text-wrap" *ngIf="note">
|
||||||
|
<ion-label>
|
||||||
|
<ion-note slot="end">{{ note }}</ion-note>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
|
@ -0,0 +1,17 @@
|
||||||
|
// @todo Review commented styles.
|
||||||
|
// ion-app.app-root {
|
||||||
|
// .safe-area-page,
|
||||||
|
// .safe-padding-horizontal {
|
||||||
|
// core-course-module-description {
|
||||||
|
// padding-left: 0 !important;
|
||||||
|
// padding-right: 0 !important;
|
||||||
|
// .item-ios.item-block {
|
||||||
|
// @include safe-area-padding-horizontal($item-ios-padding-end / 2, null);
|
||||||
|
|
||||||
|
// .item-inner {
|
||||||
|
// @include safe-area-padding-horizontal(null, $item-ios-padding-end / 2);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
|
@ -0,0 +1,48 @@
|
||||||
|
// (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 } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display the description of a module.
|
||||||
|
*
|
||||||
|
* This directive is meant to display a module description in a similar way throughout all the app.
|
||||||
|
*
|
||||||
|
* You can add a note at the right side of the description by using the 'note' attribute.
|
||||||
|
*
|
||||||
|
* You can also pass a component and componentId to be used in format-text.
|
||||||
|
*
|
||||||
|
* Module descriptions are shortened by default, allowing the user to see the full description by clicking in it.
|
||||||
|
* If you want the whole description to be shown you can use the 'showFull' attribute.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <core-course-module-description [description]="myDescription"></core-course-module-description
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-module-description',
|
||||||
|
templateUrl: 'core-course-module-description.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleDescriptionComponent {
|
||||||
|
|
||||||
|
@Input() description?: string; // The description to display.
|
||||||
|
@Input() note?: string; // A note to display along with the description.
|
||||||
|
@Input() component?: string; // Component for format text directive.
|
||||||
|
@Input() componentId?: string | number; // Component ID to use in conjunction with the component.
|
||||||
|
@Input() showFull?: string | boolean; // Whether to always display the full description.
|
||||||
|
@Input() contextLevel?: string; // The context level.
|
||||||
|
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
||||||
|
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
<ion-item *ngIf="module && module.handlerData && module.visibleoncoursepage !== 0 && !module.handlerData.loading"
|
||||||
|
id="core-course-module-{{module.id}}" class="ion-text-wrap core-course-module-handler {{module.handlerData.class}}"
|
||||||
|
(click)="moduleClicked($event)" [title]="module.handlerData.a11yTitle" detail="false"
|
||||||
|
[ngClass]="{'item-media': module.handlerData.icon, 'item-dimmed': module.visible === 0 || module.uservisible === false,
|
||||||
|
'core-not-clickable': !module.handlerData.action || module.uservisible === false}">
|
||||||
|
|
||||||
|
<img slot="start" *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" [alt]="modNameTranslated"
|
||||||
|
[attr.aria-hidden]="true" class="core-module-icon">
|
||||||
|
|
||||||
|
<ion-label>
|
||||||
|
<div class="core-module-title">
|
||||||
|
<core-format-text [text]="module.handlerData.title" contextLevel="module" [contextInstanceId]="module.id"
|
||||||
|
[courseId]="courseId" [attr.aria-label]="module.handlerData.a11yTitle + ', ' + modNameTranslated">
|
||||||
|
</core-format-text>
|
||||||
|
|
||||||
|
<!-- Buttons. -->
|
||||||
|
<div slot="end" *ngIf="module.uservisible !== false" class="buttons core-module-buttons"
|
||||||
|
[ngClass]="{'core-button-completion': module.completiondata}">
|
||||||
|
<!-- Module completion. -->
|
||||||
|
<core-course-module-completion *ngIf="module.completiondata" [completion]="module.completiondata"
|
||||||
|
[moduleName]="module.name" [moduleId]="module.id" (completionChanged)="completionChanged.emit($event)">
|
||||||
|
</core-course-module-completion>
|
||||||
|
|
||||||
|
<div class="core-module-buttons-more">
|
||||||
|
<!-- @todo <core-download-refresh [status]="downloadStatus" [enabled]="downloadEnabled" [canTrustDownload]="canCheckUpdates"
|
||||||
|
[loading]="spinner || module.handlerData.spinner" (action)="download($event)">
|
||||||
|
</core-download-refresh> -->
|
||||||
|
|
||||||
|
<!-- Buttons defined by the module handler. -->
|
||||||
|
<ion-button fill="clear" *ngFor="let button of module.handlerData.buttons" color="dark"
|
||||||
|
[hidden]="button.hidden || spinner || module.handlerData.spinner" class="core-animate-show-hide"
|
||||||
|
(click)="buttonClicked($event, button)"
|
||||||
|
[attr.aria-label]="button.label | translate:{$a: module.handlerData.title}">
|
||||||
|
<ion-icon [name]="button.icon" slot="icon-only"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="core-module-more-info">
|
||||||
|
<ion-badge slot="end" *ngIf="module.handlerData.extraBadge" [color]="module.handlerData.extraBadgeColor"
|
||||||
|
class="ion-text-wrap ion-text-start">
|
||||||
|
<span [innerHTML]="module.handlerData.extraBadge"></span>
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge slot="end" *ngIf="module.visible === 0 && (!section || section.visible)" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge slot="end" *ngIf="module.visible !== 0 && module.isStealth" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenoncoursepage' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<div class="core-module-availabilityinfo" *ngIf="module.availabilityinfo" slot="end">
|
||||||
|
<ion-badge class="ion-text-wrap">{{ 'core.restricted' | translate }}</ion-badge>
|
||||||
|
<core-format-text [text]="module.availabilityinfo" contextLevel="module" [contextInstanceId]="module.id"
|
||||||
|
[courseId]="courseId" class="ion-text-wrap">
|
||||||
|
</core-format-text>
|
||||||
|
</div>
|
||||||
|
<ion-badge slot="end" *ngIf="module.completiondata?.offline" color="warning" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.manualcompletionnotsynced' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<core-format-text class="core-module-description" *ngIf="module.description" maxHeight="80" [text]="module.description"
|
||||||
|
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
|
||||||
|
<!-- Loading. -->
|
||||||
|
<ion-item *ngIf="module && module.handlerData && module.visibleoncoursepage !== 0 && module.handlerData.loading" role="status"
|
||||||
|
class="ion-text-wrap" id="core-course-module-{{module.id}}" [title]="module.handlerData.a11yTitle"
|
||||||
|
[ngClass]="['core-course-module-handler', 'core-module-loading', module.handlerData.class]" detail="false">
|
||||||
|
<ion-label><ion-spinner></ion-spinner></ion-label>
|
||||||
|
</ion-item>
|
|
@ -0,0 +1,164 @@
|
||||||
|
:host {
|
||||||
|
// @todo Review commented styles.
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.item.core-course-module-handler {
|
||||||
|
align-items: flex-start;
|
||||||
|
min-height: 52px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
// &.item .item-inner {
|
||||||
|
// @include safe-area-padding(null, 0px, null, null);
|
||||||
|
// }
|
||||||
|
// .label {
|
||||||
|
// @include margin(0, 0, 0, null);
|
||||||
|
// }
|
||||||
|
.core-module-icon {
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &.item-ios:active,
|
||||||
|
// &.item-ios.activated {
|
||||||
|
// background-color: $list-ios-activated-background-color;
|
||||||
|
// }
|
||||||
|
// &.item-md:active,
|
||||||
|
// &.item-md.activated {
|
||||||
|
// background-color: $list-md-activated-background-color;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-title {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
core-format-text {
|
||||||
|
flex-grow: 2;
|
||||||
|
}
|
||||||
|
.core-module-buttons,
|
||||||
|
.buttons.core-module-buttons {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-buttons,
|
||||||
|
.core-module-buttons-more {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-buttons core-course-module-completion,
|
||||||
|
.core-module-buttons-more button {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-buttons core-course-module-completion {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-more-info {
|
||||||
|
// ion-badge {
|
||||||
|
// @include text-align('start');
|
||||||
|
// }
|
||||||
|
|
||||||
|
.core-module-availabilityinfo {
|
||||||
|
font-size: 90%;
|
||||||
|
ul {
|
||||||
|
margin-block-start: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-not-clickable {
|
||||||
|
cursor: initial;
|
||||||
|
|
||||||
|
// &:active,
|
||||||
|
// &.activated {
|
||||||
|
// background-color: $list-background-color;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-module-loading {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 10px;
|
||||||
|
clear: both;
|
||||||
|
// @include darkmode() {
|
||||||
|
// color: $core-dark-text-color;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// @include darkmode() {
|
||||||
|
// .item.core-course-module-handler {
|
||||||
|
// background: $core-dark-item-bg-color;
|
||||||
|
// &.item-ios:active,
|
||||||
|
// &.item-ios.activated,
|
||||||
|
// &.item-md:active,
|
||||||
|
// &.item-md.activated {
|
||||||
|
// background-color: $core-dark-background-color;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .core-not-clickable:active,
|
||||||
|
// .core-not-clickable.activated {
|
||||||
|
// background-color: $core-dark-item-bg-color;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ion-app.app-root.md core-course-module {
|
||||||
|
// .core-module-description {
|
||||||
|
// @include padding(null, $label-md-margin-end, null, null);
|
||||||
|
// margin-bottom: $label-md-margin-bottom;
|
||||||
|
|
||||||
|
// .core-show-more {
|
||||||
|
// @include padding(null, $label-md-margin-end, null, null);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .core-module-title core-format-text {
|
||||||
|
// padding-top: $label-md-margin-top + 3;
|
||||||
|
// }
|
||||||
|
// .button-md {
|
||||||
|
// margin-top: 8px;
|
||||||
|
// margin-bottom: 8px;
|
||||||
|
// }
|
||||||
|
// .core-module-buttons-more {
|
||||||
|
// min-height: 52px;
|
||||||
|
// min-width: 53px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ion-app.app-root.ios core-course-module {
|
||||||
|
// .core-module-description {
|
||||||
|
// @include padding(null, $label-ios-margin-end, null, null);
|
||||||
|
// margin-bottom: $label-md-margin-bottom;
|
||||||
|
|
||||||
|
// .core-show-more {
|
||||||
|
// @include padding(null, $label-ios-margin-end, null, null);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .core-module-title core-format-text {
|
||||||
|
// padding-top: $label-ios-margin-top + 3;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .core-module-buttons-more {
|
||||||
|
// min-height: 53px;
|
||||||
|
// min-width: 58px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ion-app.app-root .core-course-module-handler.item [item-start] + .item-inner {
|
||||||
|
// @include margin-horizontal(4px, null);
|
||||||
|
// }
|
|
@ -0,0 +1,234 @@
|
||||||
|
// (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, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreEventObserver, CoreEventPackageStatusChanged, CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
CoreCourseHelper,
|
||||||
|
CoreCourseModule,
|
||||||
|
CoreCourseModuleCompletionData,
|
||||||
|
CoreCourseSection,
|
||||||
|
} from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreCourseModuleHandlerButton } from '@features/course/services/module-delegate';
|
||||||
|
import {
|
||||||
|
CoreCourseModulePrefetchDelegate,
|
||||||
|
CoreCourseModulePrefetchHandler,
|
||||||
|
} from '@features/course/services/module-prefetch-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display a module entry in a list of modules.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* <core-course-module [module]="module" [courseId]="courseId" (completionChanged)="onCompletionChange()"></core-course-module>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-module',
|
||||||
|
templateUrl: 'core-course-module.html',
|
||||||
|
styleUrls: ['module.scss'],
|
||||||
|
})
|
||||||
|
export class CoreCourseModuleComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@Input() module?: CoreCourseModule; // The module to render.
|
||||||
|
@Input() courseId?: number; // The course the module belongs to.
|
||||||
|
@Input() section?: CoreCourseSection; // The section the module belongs to.
|
||||||
|
// eslint-disable-next-line @angular-eslint/no-input-rename
|
||||||
|
@Input('downloadEnabled') set enabled(value: boolean) {
|
||||||
|
this.downloadEnabled = value;
|
||||||
|
|
||||||
|
if (!this.module?.handlerData?.showDownloadButton || !this.downloadEnabled || this.statusCalculated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First time that the download is enabled. Initialize the data.
|
||||||
|
this.statusCalculated = true;
|
||||||
|
this.spinner = true; // Show spinner while calculating the status.
|
||||||
|
|
||||||
|
// Get current status to decide which icon should be shown.
|
||||||
|
this.calculateAndShowStatus();
|
||||||
|
};
|
||||||
|
|
||||||
|
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when module completion changes.
|
||||||
|
@Output() statusChanged = new EventEmitter<CoreCourseModuleStatusChangedData>(); // Notify when the download status changes.
|
||||||
|
|
||||||
|
downloadStatus?: string;
|
||||||
|
canCheckUpdates?: boolean;
|
||||||
|
spinner?: boolean; // Whether to display a loading spinner.
|
||||||
|
downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
|
modNameTranslated = '';
|
||||||
|
|
||||||
|
protected prefetchHandler?: CoreCourseModulePrefetchHandler;
|
||||||
|
protected statusObserver?: CoreEventObserver;
|
||||||
|
protected statusCalculated = false;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.courseId = this.courseId || this.module.course;
|
||||||
|
this.modNameTranslated = CoreCourse.instance.translateModuleName(this.module.modname) || '';
|
||||||
|
|
||||||
|
if (!this.module.handlerData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.module.handlerData.a11yTitle = this.module.handlerData.a11yTitle ?? this.module.handlerData.title;
|
||||||
|
|
||||||
|
if (this.module.handlerData.showDownloadButton) {
|
||||||
|
// Listen for changes on this module status, even if download isn't enabled.
|
||||||
|
this.prefetchHandler = CoreCourseModulePrefetchDelegate.instance.getPrefetchHandlerFor(this.module);
|
||||||
|
this.canCheckUpdates = CoreCourseModulePrefetchDelegate.instance.canCheckUpdates();
|
||||||
|
|
||||||
|
this.statusObserver = CoreEvents.on<CoreEventPackageStatusChanged>(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
|
||||||
|
if (!this.module || data.componentId != this.module.id || !this.prefetchHandler ||
|
||||||
|
data.component != this.prefetchHandler.component) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call determineModuleStatus to get the right status to display.
|
||||||
|
const status = CoreCourseModulePrefetchDelegate.instance.determineModuleStatus(this.module, data.status);
|
||||||
|
|
||||||
|
if (this.downloadEnabled) {
|
||||||
|
// Download is enabled, show the status.
|
||||||
|
this.showStatus(status);
|
||||||
|
} else if (this.module.handlerData?.updateStatus) {
|
||||||
|
// Download isn't enabled but the handler defines a updateStatus function, call it anyway.
|
||||||
|
this.module.handlerData.updateStatus(status);
|
||||||
|
}
|
||||||
|
}, CoreSites.instance.getCurrentSiteId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when the module is clicked.
|
||||||
|
*
|
||||||
|
* @param event Click event.
|
||||||
|
*/
|
||||||
|
moduleClicked(event: Event): void {
|
||||||
|
if (this.module?.uservisible !== false && this.module?.handlerData?.action) {
|
||||||
|
this.module.handlerData.action(event, this.module, this.courseId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function called when a button is clicked.
|
||||||
|
*
|
||||||
|
* @param event Click event.
|
||||||
|
* @param button The clicked button.
|
||||||
|
*/
|
||||||
|
buttonClicked(event: Event, button: CoreCourseModuleHandlerButton): void {
|
||||||
|
if (!button || !button.action) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
button.action(event, this.module!, this.courseId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download the module.
|
||||||
|
*
|
||||||
|
* @param refresh Whether it's refreshing.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async download(refresh: boolean): Promise<void> {
|
||||||
|
if (!this.prefetchHandler || !this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show spinner since this operation might take a while.
|
||||||
|
this.spinner = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get download size to ask for confirm if it's high.
|
||||||
|
const size = await this.prefetchHandler.getDownloadSize(this.module, this.courseId!, true);
|
||||||
|
|
||||||
|
await CoreCourseHelper.instance.prefetchModule(this.prefetchHandler, this.module, size, this.courseId!, refresh);
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
sectionId: this.section?.id,
|
||||||
|
moduleId: this.module.id,
|
||||||
|
courseId: this.courseId!,
|
||||||
|
};
|
||||||
|
this.statusChanged.emit(eventData);
|
||||||
|
} catch (error) {
|
||||||
|
// Error, hide spinner.
|
||||||
|
this.spinner = false;
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show download buttons according to module status.
|
||||||
|
*
|
||||||
|
* @param status Module status.
|
||||||
|
*/
|
||||||
|
protected showStatus(status: string): void {
|
||||||
|
if (!status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.spinner = false;
|
||||||
|
this.downloadStatus = status;
|
||||||
|
|
||||||
|
this.module?.handlerData?.updateStatus?.(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate and show module status.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async calculateAndShowStatus(): Promise<void> {
|
||||||
|
if (!this.module || !this.courseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = await CoreCourseModulePrefetchDelegate.instance.getModuleStatus(this.module, this.courseId);
|
||||||
|
|
||||||
|
this.showStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
// this.statusObserver?.off();
|
||||||
|
this.module?.handlerData?.onDestroy?.();
|
||||||
|
this.isDestroyed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data sent to the status changed output.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModuleStatusChangedData = {
|
||||||
|
moduleId: number;
|
||||||
|
courseId: number;
|
||||||
|
sectionId?: number;
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>{{ 'core.course.sections' | translate }}</ion-title>
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||||
|
<ion-icon slot="icon-only" name="fas-times"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-list id="core-course-section-selector" role="menu">
|
||||||
|
<ng-container *ngFor="let section of sections">
|
||||||
|
<ion-item *ngIf="!section.hiddenbynumsections && section.id != stealthModulesSectionId" class="ion-text-wrap"
|
||||||
|
(click)="selectSection(section)" [class.core-selected-item]="selected?.id == section.id"
|
||||||
|
[class.item-dimmed]="section.visible === 0 || section.uservisible === false" detail="false" role="menuitem"
|
||||||
|
[attr.aria-hidden]="section.uservisible === false">
|
||||||
|
|
||||||
|
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h2><core-format-text [text]="section.name" contextLevel="course" [contextInstanceId]="course?.id">
|
||||||
|
</core-format-text></h2>
|
||||||
|
<core-progress-bar *ngIf="section.progress >= 0" [progress]="section.progress"></core-progress-bar>
|
||||||
|
|
||||||
|
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible !== false" class="ion-text-wrap">
|
||||||
|
{{ 'core.course.hiddenfromstudents' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" *ngIf="section.visible === 0 && section.uservisible === false" class="ion-text-wrap">
|
||||||
|
{{ 'core.notavailable' | translate }}
|
||||||
|
</ion-badge>
|
||||||
|
<ion-badge color="secondary" *ngIf="section.availabilityinfo" class="ion-text-wrap">
|
||||||
|
<core-format-text [text]=" section.availabilityinfo" contextLevel="course" [contextInstanceId]="course?.id">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-badge>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,17 @@
|
||||||
|
:host {
|
||||||
|
core-progress-bar {
|
||||||
|
.core-progress-text {
|
||||||
|
line-height: 24px;
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
progress {
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ion-badge {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
// (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 { CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseProvider } from '@features/course/services/course';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { ModalController } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display course section selector in a modal.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-section-selector',
|
||||||
|
templateUrl: 'section-selector.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseSectionSelectorComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() sections?: SectionWithProgress[];
|
||||||
|
@Input() selected?: CoreCourseSection;
|
||||||
|
@Input() course?: CoreCourseAnyCourseData;
|
||||||
|
|
||||||
|
stealthModulesSectionId = CoreCourseProvider.STEALTH_MODULES_SECTION_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
|
||||||
|
if (!this.course || !this.sections || !this.course.enablecompletion || !('courseformatoptions' in this.course) ||
|
||||||
|
!this.course.courseformatoptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatOptions = CoreUtils.instance.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value');
|
||||||
|
|
||||||
|
if (!formatOptions || formatOptions.coursedisplay != 1 || formatOptions.completionusertracked === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sections.forEach((section) => {
|
||||||
|
let complete = 0;
|
||||||
|
let total = 0;
|
||||||
|
section.modules.forEach((module) => {
|
||||||
|
if (!module.uservisible || module.completiondata === undefined || module.completiondata.tracking === undefined ||
|
||||||
|
module.completiondata.tracking <= CoreCourseProvider.COMPLETION_TRACKING_NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
total++;
|
||||||
|
if (module.completiondata.state == CoreCourseProvider.COMPLETION_COMPLETE ||
|
||||||
|
module.completiondata.state == CoreCourseProvider.COMPLETION_COMPLETE_PASS) {
|
||||||
|
complete++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
section.progress = complete / total * 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal.
|
||||||
|
*/
|
||||||
|
closeModal(): void {
|
||||||
|
ModalController.instance.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a section.
|
||||||
|
*
|
||||||
|
* @param section Selected section object.
|
||||||
|
*/
|
||||||
|
selectSection(section: SectionWithProgress): void {
|
||||||
|
if (section.uservisible !== false) {
|
||||||
|
ModalController.instance.dismiss(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type SectionWithProgress = CoreCourseSection & {
|
||||||
|
progress?: number;
|
||||||
|
};
|
|
@ -0,0 +1,7 @@
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let item of items" (click)="openCourse(item.courseId)" [title]="item.courseName">
|
||||||
|
<ion-icon name="fas-graduation-cap" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ item.courseName }}</h2>
|
||||||
|
<p *ngIf="item.categoryName">{{ 'core.category' | translate }}: {{ item.categoryName }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
|
@ -0,0 +1,42 @@
|
||||||
|
// (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 } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||||
|
import { CoreCouseTagItems } from '@features/course/services/handlers/course-tag-area';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that renders the course tag area.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-tag-area',
|
||||||
|
templateUrl: 'core-course-tag-area.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseTagAreaComponent {
|
||||||
|
|
||||||
|
@Input() items?: CoreCouseTagItems[]; // Area items to render.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a course.
|
||||||
|
*
|
||||||
|
* @param courseId The course to open.
|
||||||
|
*/
|
||||||
|
openCourse(courseId: number): void {
|
||||||
|
// @todo If this component is inside a split view, use the master nav to open it.
|
||||||
|
// const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
|
||||||
|
CoreCourseHelper.instance.getAndOpenCourse(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<div class="ion-padding">
|
||||||
|
<core-course-module-description [description]="module?.description" contextLevel="module"
|
||||||
|
[contextInstanceId]="module?.id" [courseId]="courseId">
|
||||||
|
</core-course-module-description>
|
||||||
|
|
||||||
|
<h2 *ngIf="!isDisabledInSite && isSupportedByTheApp">{{ 'core.whoops' | translate }}</h2>
|
||||||
|
<h2 *ngIf="isDisabledInSite || !isSupportedByTheApp">{{ 'core.uhoh' | translate }}</h2>
|
||||||
|
|
||||||
|
<p class="core-big" *ngIf="isDisabledInSite">{{ 'core.course.activitydisabled' | translate }}</p>
|
||||||
|
<p class="core-big" *ngIf="!isDisabledInSite && isSupportedByTheApp">
|
||||||
|
{{ 'core.course.activitynotyetviewablesiteupgradeneeded' | translate }}
|
||||||
|
</p>
|
||||||
|
<p class="core-big" *ngIf="!isDisabledInSite && !isSupportedByTheApp">
|
||||||
|
{{ 'core.course.activitynotyetviewableremoteaddon' | translate }}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="isDisabledInSite || !isSupportedByTheApp"><strong>{{ 'core.course.askadmintosupport' | translate }}</strong></p>
|
||||||
|
|
||||||
|
<div *ngIf="module && module.url">
|
||||||
|
<p><strong>{{ 'core.course.useactivityonbrowser' | translate }}</strong></p>
|
||||||
|
<ion-button expand="block" [href]="module.url" core-link>
|
||||||
|
{{ 'core.openinbrowser' | translate }}
|
||||||
|
<ion-icon name="fas-external-link-alt" slot="end"></ion-icon>
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,49 @@
|
||||||
|
// (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 { CoreCourse, CoreCourseWSModule } from '@features/course/services/course';
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays info about an unsupported module.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-unsupported-module',
|
||||||
|
templateUrl: 'core-course-unsupported-module.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseUnsupportedModuleComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() courseId?: number; // The course to module belongs to.
|
||||||
|
@Input() module?: CoreCourseWSModule; // The module to render.
|
||||||
|
|
||||||
|
isDisabledInSite?: boolean;
|
||||||
|
isSupportedByTheApp?: boolean;
|
||||||
|
moduleName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (!this.module) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDisabledInSite = CoreCourseModuleDelegate.instance.isModuleDisabledInSite(this.module.modname);
|
||||||
|
this.isSupportedByTheApp = CoreCourseModuleDelegate.instance.hasHandler(this.module.modname);
|
||||||
|
this.moduleName = CoreCourse.instance.translateModuleName(this.module.modname);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// (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 { RouterModule, Routes } from '@angular/router';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: 'index',
|
||||||
|
pathMatch: 'full',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
loadChildren: () => import('./pages/index/index.module').then( m => m.CoreCourseIndexPageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'unsupported-module',
|
||||||
|
loadChildren: () => import('./pages/unsupported-module/unsupported-module.module')
|
||||||
|
.then( m => m.CoreCourseUnsupportedModulePageModule),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'list-mod-type',
|
||||||
|
loadChildren: () => import('./pages/list-mod-type/list-mod-type').then( m => m.CoreCourseListModTypePage),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
})
|
||||||
|
export class CoreCourseLazyModule {}
|
|
@ -12,20 +12,70 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
|
||||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
import { CoreCourseComponentsModule } from './components/components.module';
|
||||||
|
import { CoreCourseDirectivesModule } from './directives/directives.module';
|
||||||
|
import { CoreCourseFormatModule } from './format/formats.module';
|
||||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
|
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/database/course';
|
||||||
|
import { SITE_SCHEMA as LOG_SITE_SCHEMA } from './services/database/log';
|
||||||
|
import { SITE_SCHEMA as PREFETCH_SITE_SCHEMA } from './services/database/module-prefetch';
|
||||||
|
import { CoreCourseIndexRoutingModule } from './pages/index/index-routing.module';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from './services/module-prefetch-delegate';
|
||||||
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { CoreCourseLogCronHandler } from './services/handlers/log-cron';
|
||||||
|
import { CoreCourseSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
|
import { CoreTagAreaDelegate } from '@features/tag/services/tag-area-delegate';
|
||||||
|
import { CoreCourseTagAreaHandler } from './services/handlers/course-tag-area';
|
||||||
|
import { CoreCourseModulesTagAreaHandler } from './services/handlers/modules-tag-area';
|
||||||
|
import { CoreCourse } from './services/course';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'course',
|
||||||
|
loadChildren: () => import('@features/course/course-lazy.module').then(m => m.CoreCourseLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const courseIndexRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'contents',
|
||||||
|
loadChildren: () => import('./pages/contents/contents.module').then(m => m.CoreCourseContentsPageModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||||
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
|
CoreCourseFormatModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
CoreCourseDirectivesModule,
|
||||||
|
],
|
||||||
|
exports: [CoreCourseIndexRoutingModule],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: CORE_SITE_SCHEMAS,
|
provide: CORE_SITE_SCHEMAS,
|
||||||
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
|
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA, LOG_SITE_SCHEMA, PREFETCH_SITE_SCHEMA],
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCronDelegate.instance.register(CoreCourseSyncCronHandler.instance);
|
||||||
|
CoreCronDelegate.instance.register(CoreCourseLogCronHandler.instance);
|
||||||
|
CoreTagAreaDelegate.instance.registerHandler(CoreCourseTagAreaHandler.instance);
|
||||||
|
CoreTagAreaDelegate.instance.registerHandler(CoreCourseModulesTagAreaHandler.instance);
|
||||||
|
|
||||||
|
CoreCourse.instance.initialize();
|
||||||
|
CoreCourseModulePrefetchDelegate.instance.initialize();
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreCourseModule {
|
export class CoreCourseModule {}
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
// (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 { CoreCourseDownloadModuleMainFileDirective } from './download-module-main-file';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreCourseDownloadModuleMainFileDirective,
|
||||||
|
],
|
||||||
|
imports: [],
|
||||||
|
exports: [
|
||||||
|
CoreCourseDownloadModuleMainFileDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseDirectivesModule {}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// (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 { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreCourse, CoreCourseModuleContentFile, CoreCourseWSModule } from '@features/course/services/course';
|
||||||
|
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Directive to allow downloading and open the main file of a module.
|
||||||
|
* When the item with this directive is clicked, the module will be downloaded (if needed) and opened.
|
||||||
|
* This is meant for modules like mod_resource.
|
||||||
|
*
|
||||||
|
* This directive must receive either a module or a moduleId. If no files are provided, it will use module.contents.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[core-course-download-module-main-file]',
|
||||||
|
})
|
||||||
|
export class CoreCourseDownloadModuleMainFileDirective implements OnInit {
|
||||||
|
|
||||||
|
@Input() module?: CoreCourseWSModule; // The module.
|
||||||
|
@Input() moduleId?: string | number; // The module ID. Required if module is not supplied.
|
||||||
|
@Input() courseId?: string | number; // The course ID.
|
||||||
|
@Input() component?: string; // Component to link the file to.
|
||||||
|
@Input() componentId?: string | number; // Component ID to use in conjunction with the component. If not defined, use moduleId.
|
||||||
|
@Input() files?: CoreCourseModuleContentFile[]; // List of files of the module. If not provided, use module.contents.
|
||||||
|
|
||||||
|
protected element: HTMLElement;
|
||||||
|
|
||||||
|
constructor(element: ElementRef) {
|
||||||
|
this.element = element.nativeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.element.addEventListener('click', async (ev: Event) => {
|
||||||
|
if (!this.module && !this.moduleId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||||
|
const courseId = typeof this.courseId == 'string' ? parseInt(this.courseId, 10) : this.courseId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.module) {
|
||||||
|
// Try to get the module from cache.
|
||||||
|
this.moduleId = typeof this.moduleId == 'string' ? parseInt(this.moduleId, 10) : this.moduleId;
|
||||||
|
this.module = await CoreCourse.instance.getModule(this.moduleId!, courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentId = this.componentId || module.id;
|
||||||
|
|
||||||
|
await CoreCourseHelper.instance.downloadModuleAndOpenFile(
|
||||||
|
this.module,
|
||||||
|
courseId ?? this.module.course!,
|
||||||
|
this.component,
|
||||||
|
componentId,
|
||||||
|
this.files,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.errordownloading', true);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { CoreCourseFormatSingleActivityModule } from './singleactivity/singleactivity.module';
|
||||||
|
import { CoreCourseFormatSocialModule } from './social/social.module';
|
||||||
|
import { CoreCourseFormatTopicsModule } from './topics/topics.module';
|
||||||
|
import { CoreCourseFormatWeeksModule } from './weeks/weeks.module';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [
|
||||||
|
CoreCourseFormatSingleActivityModule,
|
||||||
|
CoreCourseFormatSocialModule,
|
||||||
|
CoreCourseFormatTopicsModule,
|
||||||
|
CoreCourseFormatWeeksModule,
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatModule { }
|
|
@ -0,0 +1 @@
|
||||||
|
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
|
@ -0,0 +1,104 @@
|
||||||
|
// (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, OnChanges, SimpleChange, ViewChild, Output, EventEmitter, Type } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module';
|
||||||
|
import { CoreDynamicComponent } from '@components/dynamic-component/dynamic-component';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreCourseModuleCompletionData, CoreCourseSectionWithStatus } from '@features/course/services/course-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to display single activity format. It will determine the right component to use and instantiate it.
|
||||||
|
*
|
||||||
|
* The instantiated component will receive the course and the module as inputs.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-course-format-single-activity',
|
||||||
|
templateUrl: 'core-course-format-single-activity.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatSingleActivityComponent implements OnChanges {
|
||||||
|
|
||||||
|
@Input() course?: CoreCourseAnyCourseData; // The course to render.
|
||||||
|
@Input() sections?: CoreCourseSectionWithStatus[]; // List of course sections.
|
||||||
|
@Input() downloadEnabled?: boolean; // Whether the download of sections and modules is enabled.
|
||||||
|
@Input() initialSectionId?: number; // The section to load first (by ID).
|
||||||
|
@Input() initialSectionNumber?: number; // The section to load first (by number).
|
||||||
|
@Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section.
|
||||||
|
@Output() completionChanged = new EventEmitter<CoreCourseModuleCompletionData>(); // Notify when any module completion changes.
|
||||||
|
|
||||||
|
@ViewChild(CoreDynamicComponent) dynamicComponent?: CoreDynamicComponent;
|
||||||
|
|
||||||
|
componentClass?: Type<unknown>; // The class of the component to render.
|
||||||
|
data: Record<string | number, unknown> = {}; // Data to pass to the component.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect changes on input properties.
|
||||||
|
*/
|
||||||
|
async ngOnChanges(changes: { [name: string]: SimpleChange }): Promise<void> {
|
||||||
|
if (!changes.course || !changes.sections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.course || !this.sections || !this.sections.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In single activity the module should only have 1 section and 1 module. Get the module.
|
||||||
|
const module = this.sections?.[0].modules?.[0];
|
||||||
|
|
||||||
|
this.data.courseId = this.course.id;
|
||||||
|
this.data.module = module;
|
||||||
|
|
||||||
|
if (module && !this.componentClass) {
|
||||||
|
// We haven't obtained the class yet. Get it now.
|
||||||
|
const component = await CoreCourseModuleDelegate.instance.getMainComponent(this.course, module);
|
||||||
|
this.componentClass = component || CoreCourseUnsupportedModuleComponent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @param done Function to call when done.
|
||||||
|
* @param afterCompletionChange Whether the refresh is due to a completion change.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: CustomEvent<IonRefresher>, done?: () => void, afterCompletionChange?: boolean): Promise<void> {
|
||||||
|
if (afterCompletionChange) {
|
||||||
|
// Don't refresh the view after a completion change since completion isn't displayed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dynamicComponent?.callComponentFunction('doRefresh', [refresher, done]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.dynamicComponent?.callComponentFunction('ionViewDidEnter');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page that contains the component.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.dynamicComponent?.callComponentFunction('ionViewDidLeave');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
// (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, Type } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCourseWSSection } from '@features/course/services/course';
|
||||||
|
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import { CoreCourseFormatSingleActivityComponent } from '../../components/singleactivity';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support singleactivity course format.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseFormatSingleActivityHandlerService implements CoreCourseFormatHandler {
|
||||||
|
|
||||||
|
name = 'CoreCourseFormatSingleActivity';
|
||||||
|
format = 'singleactivity';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether it allows seeing all sections at the same time. Defaults to true.
|
||||||
|
*
|
||||||
|
* @param course The course to check.
|
||||||
|
* @return Whether it can view all sections.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
canViewAllSections(course: CoreCourseAnyCourseData): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the option blocks should be displayed. Defaults to true.
|
||||||
|
*
|
||||||
|
* @param course The course to check.
|
||||||
|
* @return Whether it can display blocks.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
displayBlocks(course: CoreCourseAnyCourseData): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title to use in course page. If not defined, course displayname or fullname.
|
||||||
|
* This function will be called without sections first, and then call it again when the sections are retrieved.
|
||||||
|
*
|
||||||
|
* @param course The course.
|
||||||
|
* @param sections List of sections.
|
||||||
|
* @return Title.
|
||||||
|
*/
|
||||||
|
getCourseTitle(course: CoreCourseAnyCourseData, sections?: CoreCourseWSSection[]): string {
|
||||||
|
if (sections?.[0]?.modules?.[0]) {
|
||||||
|
return sections[0].modules[0].name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (course.displayname) {
|
||||||
|
return course.displayname;
|
||||||
|
} else if (course.fullname) {
|
||||||
|
return course.fullname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the option to enable section/module download should be displayed. Defaults to true.
|
||||||
|
*
|
||||||
|
* @param course The course to check.
|
||||||
|
* @return Whether the option to enable section/module download should be displayed
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
displayEnableDownload(course: CoreCourseAnyCourseData): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the default section selector should be displayed. Defaults to true.
|
||||||
|
*
|
||||||
|
* @param course The course to check.
|
||||||
|
* @return Whether the default section selector should be displayed.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
displaySectionSelector(course: CoreCourseAnyCourseData): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the course refresher should be displayed. If it returns false, a refresher must be included in the course format,
|
||||||
|
* and the doRefresh method of CoreCourseSectionPage must be called on refresh. Defaults to true.
|
||||||
|
*
|
||||||
|
* @param course The course to check.
|
||||||
|
* @param sections List of course sections.
|
||||||
|
* @return Whether the refresher should be displayed.
|
||||||
|
*/
|
||||||
|
displayRefresher(course: CoreCourseAnyCourseData, sections: CoreCourseWSSection[]): boolean {
|
||||||
|
if (sections?.[0]?.modules?.[0]) {
|
||||||
|
return CoreCourseModuleDelegate.instance.displayRefresherInSingleActivity(sections[0].modules[0].modname);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the Component to use to display the course format instead of using the default one.
|
||||||
|
* Use it if you want to display a format completely different from the default one.
|
||||||
|
* If you want to customize the default format there are several methods to customize parts of it.
|
||||||
|
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||||
|
*
|
||||||
|
* @param injector Injector.
|
||||||
|
* @param course The course to render.
|
||||||
|
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async getCourseFormatComponent(course: CoreCourseAnyCourseData): Promise<Type<unknown>> {
|
||||||
|
return CoreCourseFormatSingleActivityComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the view should be refreshed when completion changes. If your course format doesn't display
|
||||||
|
* activity completion then you should return false.
|
||||||
|
*
|
||||||
|
* @param course The course.
|
||||||
|
* @return Whether course view should be refreshed when an activity completion changes.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async shouldRefreshWhenCompletionChanges(course: CoreCourseAnyCourseData): Promise<boolean> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseFormatSingleActivityHandler extends makeSingleton(CoreCourseFormatSingleActivityHandlerService) {}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseFormatSingleActivityComponent } from './components/singleactivity';
|
||||||
|
import { CoreCourseFormatSingleActivityHandler } from './services/handlers/singleactivity-format';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreCourseFormatSingleActivityComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatSingleActivityHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
CoreCourseFormatSingleActivityComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatSingleActivityModule {}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// (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 { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
import { CoreCourseFormatSingleActivityHandlerService } from '../../../singleactivity/services/handlers/singleactivity-format';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support social course format.
|
||||||
|
* This format is like singleactivity in the mobile app, so we'll use the same implementation for both.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseFormatSocialHandlerService extends CoreCourseFormatSingleActivityHandlerService {
|
||||||
|
|
||||||
|
name = 'CoreCourseFormatSocial';
|
||||||
|
format = 'social';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseFormatSocialHandler extends makeSingleton(CoreCourseFormatSocialHandlerService) {}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// (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 { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseFormatSocialHandler } from './services/handlers/social-format';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatSocialHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatSocialModule {}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support topics course format.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseFormatTopicsHandlerService implements CoreCourseFormatHandler {
|
||||||
|
|
||||||
|
name = 'CoreCourseFormatTopics';
|
||||||
|
format = 'topics';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseFormatTopicsHandler extends makeSingleton(CoreCourseFormatTopicsHandlerService) {}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// (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 { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseFormatTopicsHandler } from './services/handlers/topics-format';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatTopicsHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatTopicsModule {}
|
|
@ -0,0 +1,96 @@
|
||||||
|
// (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 { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { CoreCourseFormatHandler } from '@features/course/services/format-delegate';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import { CoreCourseWSSection } from '@features/course/services/course';
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support weeks course format.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class CoreCourseFormatWeeksHandlerService implements CoreCourseFormatHandler {
|
||||||
|
|
||||||
|
name = 'CoreCourseFormatWeeks';
|
||||||
|
format = 'weeks';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the handler is enabled on a site level.
|
||||||
|
*
|
||||||
|
* @return True or promise resolved with true if enabled.
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a list of sections, get the "current" section that should be displayed first.
|
||||||
|
*
|
||||||
|
* @param course The course to get the title.
|
||||||
|
* @param sections List of sections.
|
||||||
|
* @return Current section (or promise resolved with current section).
|
||||||
|
*/
|
||||||
|
async getCurrentSection(course: CoreCourseAnyCourseData, sections: CoreCourseSection[]): Promise<CoreCourseSection> {
|
||||||
|
const now = CoreTimeUtils.instance.timestamp();
|
||||||
|
|
||||||
|
if ((course.startdate && now < course.startdate) || (course.enddate && now > course.enddate)) {
|
||||||
|
// Course hasn't started yet or it has ended already. Return all sections.
|
||||||
|
return sections[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < sections.length; i++) {
|
||||||
|
const section = sections[i];
|
||||||
|
if (typeof section.section == 'undefined' || section.section < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dates = this.getSectionDates(section, course.startdate || 0);
|
||||||
|
if (now >= dates.start && now < dates.end) {
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The section wasn't found, return all sections.
|
||||||
|
return sections[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the start and end date of a section.
|
||||||
|
*
|
||||||
|
* @param section The section to treat.
|
||||||
|
* @param startDate The course start date (in seconds).
|
||||||
|
* @return An object with the start and end date of the section.
|
||||||
|
*/
|
||||||
|
protected getSectionDates(section: CoreCourseWSSection, startDate: number): { start: number; end: number } {
|
||||||
|
// Hack alert. We add 2 hours to avoid possible DST problems. (e.g. we go into daylight savings and the date changes).
|
||||||
|
startDate = startDate + 7200;
|
||||||
|
|
||||||
|
const dates = {
|
||||||
|
start: startDate + (CoreConstants.SECONDS_WEEK * (section.section! - 1)),
|
||||||
|
end: 0,
|
||||||
|
};
|
||||||
|
dates.end = dates.start + CoreConstants.SECONDS_WEEK;
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CoreCourseFormatWeeksHandler extends makeSingleton(CoreCourseFormatWeeksHandlerService) {}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// (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 { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseFormatWeeksHandler } from './services/handlers/weeks-format';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [],
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseFormatDelegate.instance.registerHandler(CoreCourseFormatWeeksHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class CoreCourseFormatWeeksModule {}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<core-navbar-buttons slot="end">
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item [hidden]="!displayEnableDownload" [priority]="2000" [iconAction]="downloadEnabledIcon"
|
||||||
|
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item [hidden]="!downloadCourseEnabled" [priority]="1900"
|
||||||
|
[content]="prefetchCourseData.statusTranslatable | translate" (action)="prefetchCourse()"
|
||||||
|
[iconAction]="prefetchCourseData.icon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="1800" [content]="'core.course.coursesummary' | translate" (action)="openCourseSummary()"
|
||||||
|
iconAction="fa-graduation-cap">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngFor="let item of courseMenuHandlers" [priority]="item.priority" (action)="openMenuItem(item)"
|
||||||
|
[content]="item.data.title | translate" [iconAction]="item.data.icon" [class]="item.data.class">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</core-navbar-buttons>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!dataLoaded || !displayRefresher" (ionRefresh)="doRefresh($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<core-loading [hideUntil]="dataLoaded">
|
||||||
|
<core-course-format [course]="course" [sections]="sections" [initialSectionId]="sectionId"
|
||||||
|
[initialSectionNumber]="sectionNumber" [downloadEnabled]="downloadEnabled" [moduleId]="moduleId"
|
||||||
|
(completionChanged)="onCompletionChange($event)" class="core-course-format-{{course.format}}">
|
||||||
|
</core-course-format>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -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 { 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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseContentsPage } from './contents';
|
||||||
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreCourseContentsPage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreCourseContentsPage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreCourseContentsPageModule {}
|
|
@ -0,0 +1,501 @@
|
||||||
|
// (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, ViewChild, OnInit, OnDestroy } from '@angular/core';
|
||||||
|
import { IonContent, IonRefresher } from '@ionic/angular';
|
||||||
|
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreCourses, CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import {
|
||||||
|
CoreCourse,
|
||||||
|
CoreCourseCompletionActivityStatus,
|
||||||
|
CoreCourseProvider,
|
||||||
|
} from '@features/course/services/course';
|
||||||
|
import {
|
||||||
|
CoreCourseHelper,
|
||||||
|
CoreCourseModuleCompletionData,
|
||||||
|
CoreCourseSection,
|
||||||
|
CorePrefetchStatusInfo,
|
||||||
|
} from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate';
|
||||||
|
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
|
||||||
|
import {
|
||||||
|
CoreCourseOptionsDelegate,
|
||||||
|
CoreCourseOptionsMenuHandlerToDisplay,
|
||||||
|
} from '@features/course/services/course-options-delegate';
|
||||||
|
import { CoreCourseAutoSyncData, CoreCourseSync, CoreCourseSyncProvider } from '@features/course/services/sync';
|
||||||
|
import { CoreCourseFormatComponent } from '../../components/format/format';
|
||||||
|
import {
|
||||||
|
CoreEvents,
|
||||||
|
CoreEventObserver,
|
||||||
|
CoreEventCourseStatusChanged,
|
||||||
|
CoreEventCompletionModuleViewedData,
|
||||||
|
} from '@singletons/events';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the contents of a course.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-course-contents',
|
||||||
|
templateUrl: 'contents.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseContentsPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(IonContent) content?: IonContent;
|
||||||
|
@ViewChild(CoreCourseFormatComponent) formatComponent?: CoreCourseFormatComponent;
|
||||||
|
|
||||||
|
course!: CoreCourseAnyCourseData;
|
||||||
|
sections?: CoreCourseSection[];
|
||||||
|
sectionId?: number;
|
||||||
|
sectionNumber?: number;
|
||||||
|
courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
||||||
|
dataLoaded = false;
|
||||||
|
downloadEnabled = false;
|
||||||
|
downloadEnabledIcon = 'far-square'; // Disabled by default.
|
||||||
|
downloadCourseEnabled = false;
|
||||||
|
moduleId?: number;
|
||||||
|
displayEnableDownload = false;
|
||||||
|
displayRefresher = false;
|
||||||
|
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||||
|
icon: 'spinner',
|
||||||
|
statusTranslatable: 'core.course.downloadcourse',
|
||||||
|
status: '',
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected formatOptions?: Record<string, unknown>;
|
||||||
|
protected completionObserver?: CoreEventObserver;
|
||||||
|
protected courseStatusObserver?: CoreEventObserver;
|
||||||
|
protected syncObserver?: CoreEventObserver;
|
||||||
|
protected isDestroyed = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
const course = CoreNavigator.instance.getRouteParam<CoreCourseAnyCourseData>('course');
|
||||||
|
|
||||||
|
if (!course) {
|
||||||
|
CoreDomUtils.instance.showErrorModal('Missing required course parameter.');
|
||||||
|
CoreNavigator.instance.back();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.course = course;
|
||||||
|
this.sectionId = CoreNavigator.instance.getRouteNumberParam('sectionId');
|
||||||
|
this.sectionNumber = CoreNavigator.instance.getRouteNumberParam('sectionNumber');
|
||||||
|
this.moduleId = CoreNavigator.instance.getRouteNumberParam('moduleId');
|
||||||
|
|
||||||
|
this.displayEnableDownload = !CoreSites.instance.getCurrentSite()?.isOfflineDisabled() &&
|
||||||
|
CoreCourseFormatDelegate.instance.displayEnableDownload(this.course);
|
||||||
|
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||||
|
|
||||||
|
this.initListeners();
|
||||||
|
|
||||||
|
await this.loadData(false, true);
|
||||||
|
|
||||||
|
this.dataLoaded = true;
|
||||||
|
|
||||||
|
this.initPrefetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init listeners.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async initListeners(): Promise<void> {
|
||||||
|
if (this.downloadCourseEnabled) {
|
||||||
|
// Listen for changes in course status.
|
||||||
|
this.courseStatusObserver = CoreEvents.on<CoreEventCourseStatusChanged>(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
|
||||||
|
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||||
|
this.updateCourseStatus(data.status);
|
||||||
|
}
|
||||||
|
}, CoreSites.instance.getCurrentSiteId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the course format requires the view to be refreshed when completion changes.
|
||||||
|
const shouldRefresh = await CoreCourseFormatDelegate.instance.shouldRefreshWhenCompletionChanges(this.course);
|
||||||
|
if (!shouldRefresh) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completionObserver = CoreEvents.on<CoreEventCompletionModuleViewedData>(
|
||||||
|
CoreEvents.COMPLETION_MODULE_VIEWED,
|
||||||
|
(data) => {
|
||||||
|
if (data && data.courseId == this.course.id) {
|
||||||
|
this.refreshAfterCompletionChange(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.syncObserver = CoreEvents.on<CoreCourseAutoSyncData>(CoreCourseSyncProvider.AUTO_SYNCED, (data) => {
|
||||||
|
if (!data || data.courseId != this.course.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.refreshAfterCompletionChange(false);
|
||||||
|
|
||||||
|
if (data.warnings && data.warnings[0]) {
|
||||||
|
CoreDomUtils.instance.showErrorModal(data.warnings[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init prefetch data if needed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async initPrefetch(): Promise<void> {
|
||||||
|
if (!this.downloadCourseEnabled) {
|
||||||
|
// Cannot download the whole course, stop.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the course prefetch status.
|
||||||
|
await this.determineCoursePrefetchIcon();
|
||||||
|
|
||||||
|
if (this.prefetchCourseData.icon != 'spinner') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Course is being downloaded. Get the download promise.
|
||||||
|
const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id);
|
||||||
|
if (promise) {
|
||||||
|
// There is a download promise. Show an error if it fails.
|
||||||
|
promise.catch((error) => {
|
||||||
|
if (!this.isDestroyed) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No download, this probably means that the app was closed while downloading. Set previous status.
|
||||||
|
const status = await CoreCourse.instance.setCoursePreviousStatus(this.course.id);
|
||||||
|
|
||||||
|
this.updateCourseStatus(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch and load all the data required for the view.
|
||||||
|
*
|
||||||
|
* @param refresh If it's refreshing content.
|
||||||
|
* @param sync If it should try to sync.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadData(refresh?: boolean, sync?: boolean): Promise<void> {
|
||||||
|
// First of all, get the course because the data might have changed.
|
||||||
|
const result = await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.getCourse(this.course.id));
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
if (this.course.id === result.course.id && 'displayname' in this.course && !('displayname' in result.course)) {
|
||||||
|
result.course.displayname = this.course.displayname;
|
||||||
|
}
|
||||||
|
this.course = result.course;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the course data.
|
||||||
|
// For now we don't allow manual syncing, so ignore errors.
|
||||||
|
const result = await CoreUtils.instance.ignoreErrors(CoreCourseSync.instance.syncCourse(this.course.id));
|
||||||
|
if (result?.warnings?.length) {
|
||||||
|
CoreDomUtils.instance.showErrorModal(result.warnings[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadSections(refresh),
|
||||||
|
this.loadMenuHandlers(refresh),
|
||||||
|
this.loadCourseFormatOptions(),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load course sections.
|
||||||
|
*
|
||||||
|
* @param refresh If it's refreshing content.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadSections(refresh?: boolean): Promise<void> {
|
||||||
|
// Get all the sections.
|
||||||
|
const sections = await CoreCourse.instance.getSections(this.course.id, false, true);
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
// Invalidate the recently downloaded module list. To ensure info can be prefetched.
|
||||||
|
const modules = CoreCourse.instance.getSectionsModules(sections);
|
||||||
|
|
||||||
|
await CoreCourseModulePrefetchDelegate.instance.invalidateModules(modules, this.course.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let completionStatus: Record<string, CoreCourseCompletionActivityStatus> = {};
|
||||||
|
|
||||||
|
// Get the completion status.
|
||||||
|
if (this.course.enablecompletion !== false) {
|
||||||
|
const sectionWithModules = sections.find((section) => section.modules.length > 0);
|
||||||
|
|
||||||
|
if (sectionWithModules && typeof sectionWithModules.modules[0].completion != 'undefined') {
|
||||||
|
// The module already has completion (3.6 onwards). Load the offline completion.
|
||||||
|
await CoreUtils.instance.ignoreErrors(CoreCourseHelper.instance.loadOfflineCompletion(this.course.id, sections));
|
||||||
|
} else {
|
||||||
|
const fetchedData = await CoreUtils.instance.ignoreErrors(
|
||||||
|
CoreCourse.instance.getActivitiesCompletionStatus(this.course.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
completionStatus = fetchedData || completionStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add handlers
|
||||||
|
const result = CoreCourseHelper.instance.addHandlerDataForModules(
|
||||||
|
sections,
|
||||||
|
this.course.id,
|
||||||
|
completionStatus,
|
||||||
|
this.course.fullname,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
this.sections = result.sections;
|
||||||
|
|
||||||
|
if (CoreCourseFormatDelegate.instance.canViewAllSections(this.course)) {
|
||||||
|
// Add a fake first section (all sections).
|
||||||
|
this.sections.unshift(CoreCourseHelper.instance.createAllSectionsSection());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get whether to show the refresher now that we have sections.
|
||||||
|
this.displayRefresher = CoreCourseFormatDelegate.instance.displayRefresher(this.course, this.sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the course menu handlers.
|
||||||
|
*
|
||||||
|
* @param refresh If it's refreshing content.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadMenuHandlers(refresh?: boolean): Promise<void> {
|
||||||
|
this.courseMenuHandlers = await CoreCourseOptionsDelegate.instance.getMenuHandlersToDisplay(this.course, refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load course format options if needed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadCourseFormatOptions(): Promise<void> {
|
||||||
|
|
||||||
|
// Load the course format options when course completion is enabled to show completion progress on sections.
|
||||||
|
if (!this.course.enablecompletion || !CoreCourses.instance.isGetCoursesByFieldAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('courseformatoptions' in this.course && this.course.courseformatoptions) {
|
||||||
|
// Already loaded.
|
||||||
|
this.formatOptions = CoreUtils.instance.objectToKeyValueMap(this.course.courseformatoptions, 'name', 'value');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const course = await CoreUtils.instance.ignoreErrors(CoreCourses.instance.getCourseByField('id', this.course.id));
|
||||||
|
|
||||||
|
course && Object.assign(this.course, course);
|
||||||
|
|
||||||
|
if (course?.courseformatoptions) {
|
||||||
|
this.formatOptions = CoreUtils.instance.objectToKeyValueMap(course.courseformatoptions, 'name', 'value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async doRefresh(refresher?: CustomEvent<IonRefresher>): Promise<void> {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateData());
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loadData(true, true);
|
||||||
|
} finally {
|
||||||
|
// Do not call doRefresh on the format component if the refresher is defined in the format component
|
||||||
|
// to prevent an inifinite loop.
|
||||||
|
if (this.displayRefresher && this.formatComponent) {
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.formatComponent.doRefresh(refresher));
|
||||||
|
}
|
||||||
|
|
||||||
|
refresher?.detail.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The completion of any of the modules has changed.
|
||||||
|
*
|
||||||
|
* @param completionData Completion data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async onCompletionChange(completionData: CoreCourseModuleCompletionData): Promise<void> {
|
||||||
|
const shouldReload = typeof completionData.valueused == 'undefined' || completionData.valueused;
|
||||||
|
|
||||||
|
if (!shouldReload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CoreUtils.instance.ignoreErrors(this.invalidateData());
|
||||||
|
|
||||||
|
await this.refreshAfterCompletionChange(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the data.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async invalidateData(): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
promises.push(CoreCourse.instance.invalidateSections(this.course.id));
|
||||||
|
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||||
|
promises.push(CoreCourseFormatDelegate.instance.invalidateData(this.course, this.sections || []));
|
||||||
|
|
||||||
|
if (this.sections) {
|
||||||
|
promises.push(CoreCourseModulePrefetchDelegate.instance.invalidateCourseUpdates(this.course.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh list after a completion change since there could be new activities.
|
||||||
|
*
|
||||||
|
* @param sync If it should try to sync.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async refreshAfterCompletionChange(sync?: boolean): Promise<void> {
|
||||||
|
// Save scroll position to restore it once done.
|
||||||
|
const scrollElement = await this.content?.getScrollElement();
|
||||||
|
const scrollTop = scrollElement?.scrollTop || 0;
|
||||||
|
const scrollLeft = scrollElement?.scrollLeft || 0;
|
||||||
|
|
||||||
|
this.dataLoaded = false;
|
||||||
|
this.content?.scrollToTop(0); // Scroll top so the spinner is seen.
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.loadData(true, sync);
|
||||||
|
|
||||||
|
await this.formatComponent?.doRefresh(undefined, undefined, true);
|
||||||
|
} finally {
|
||||||
|
this.dataLoaded = true;
|
||||||
|
|
||||||
|
// Wait for new content height to be calculated and scroll without animation.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.content?.scrollToPoint(scrollLeft, scrollTop, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the prefetch icon of the course.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async determineCoursePrefetchIcon(): Promise<void> {
|
||||||
|
this.prefetchCourseData = await CoreCourseHelper.instance.getCourseStatusIconAndTitle(this.course.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch the whole course.
|
||||||
|
*/
|
||||||
|
async prefetchCourse(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreCourseHelper.instance.confirmAndPrefetchCourse(
|
||||||
|
this.prefetchCourseData,
|
||||||
|
this.course,
|
||||||
|
this.sections,
|
||||||
|
undefined,
|
||||||
|
this.courseMenuHandlers,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (this.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle download enabled.
|
||||||
|
*/
|
||||||
|
toggleDownload(): void {
|
||||||
|
this.downloadEnabled = !this.downloadEnabled;
|
||||||
|
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the course status icon and title.
|
||||||
|
*
|
||||||
|
* @param status Status to show.
|
||||||
|
*/
|
||||||
|
protected updateCourseStatus(status: string): void {
|
||||||
|
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the course summary
|
||||||
|
*/
|
||||||
|
openCourseSummary(): void {
|
||||||
|
CoreNavigator.instance.navigateToSitePath('/courses/preview', { params: { course: this.course, avoidOpenCourse: true } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a menu item registered to the delegate.
|
||||||
|
*
|
||||||
|
* @param item Item to open
|
||||||
|
*/
|
||||||
|
openMenuItem(item: CoreCourseOptionsMenuHandlerToDisplay): void {
|
||||||
|
const params = Object.assign({ course: this.course }, item.data.pageParams);
|
||||||
|
CoreNavigator.instance.navigateToSitePath(item.data.page, { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.isDestroyed = true;
|
||||||
|
this.completionObserver?.off();
|
||||||
|
this.courseStatusObserver?.off();
|
||||||
|
this.syncObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.formatComponent?.ionViewDidEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.formatComponent?.ionViewDidLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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 { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { ModuleRoutesConfig } from '@/app/app-routing.module';
|
||||||
|
|
||||||
|
export const COURSE_INDEX_ROUTES = new InjectionToken('COURSE_INDEX_ROUTES');
|
||||||
|
|
||||||
|
@NgModule()
|
||||||
|
export class CoreCourseIndexRoutingModule {
|
||||||
|
|
||||||
|
static forChild(routes: ModuleRoutesConfig): ModuleWithProviders<CoreCourseIndexRoutingModule> {
|
||||||
|
return {
|
||||||
|
ngModule: CoreCourseIndexRoutingModule,
|
||||||
|
providers: [
|
||||||
|
{ provide: COURSE_INDEX_ROUTES, multi: true, useValue: routes },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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="course" [contextInstanceId]="course?.id"></core-format-text>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end"></ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-tabs [tabs]="tabs" [hideUntil]="loaded"></core-tabs>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,54 @@
|
||||||
|
// (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 { Injector, NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { RouterModule, ROUTES, Routes } from '@angular/router';
|
||||||
|
import { IonicModule } from '@ionic/angular';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { resolveModuleRoutes } from '@/app/app-routing.module';
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseIndexPage } from './index';
|
||||||
|
import { COURSE_INDEX_ROUTES } from './index-routing.module';
|
||||||
|
|
||||||
|
function buildRoutes(injector: Injector): Routes {
|
||||||
|
const routes = resolveModuleRoutes(injector, COURSE_INDEX_ROUTES);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreCourseIndexPage,
|
||||||
|
children: routes.children,
|
||||||
|
},
|
||||||
|
...routes.siblings,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
providers: [
|
||||||
|
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreCourseIndexPage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreCourseIndexPageModule {}
|
|
@ -0,0 +1,188 @@
|
||||||
|
// (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, ViewChild, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
|
||||||
|
import { CoreTab, CoreTabsComponent } from '@components/tabs/tabs';
|
||||||
|
import { CoreCourseFormatDelegate } from '../../services/format-delegate';
|
||||||
|
import { CoreCourseOptionsDelegate } from '../../services/course-options-delegate';
|
||||||
|
import { CoreCourseAnyCourseData } from '@features/courses/services/courses';
|
||||||
|
import { CoreEventObserver, CoreEvents, CoreEventSelectCourseTabData } from '@singletons/events';
|
||||||
|
import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course';
|
||||||
|
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays the list of courses the user is enrolled in.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-course-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseIndexPage implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
|
||||||
|
|
||||||
|
title?: string;
|
||||||
|
course?: CoreCourseAnyCourseData;
|
||||||
|
tabs: CourseTab[] = [];
|
||||||
|
loaded = false;
|
||||||
|
|
||||||
|
protected currentPagePath = '';
|
||||||
|
protected selectTabObserver: CoreEventObserver;
|
||||||
|
protected firstTabName?: string;
|
||||||
|
protected contentsTab: CoreTab = {
|
||||||
|
page: 'contents',
|
||||||
|
title: 'core.course.contents',
|
||||||
|
pageParams: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.selectTabObserver = CoreEvents.on<CoreEventSelectCourseTabData>(CoreEvents.SELECT_COURSE_TAB, (data) => {
|
||||||
|
if (!data.name) {
|
||||||
|
// If needed, set sectionId and sectionNumber. They'll only be used if the content tabs hasn't been loaded yet.
|
||||||
|
if (data.sectionId) {
|
||||||
|
this.contentsTab.pageParams!.sectionId = data.sectionId;
|
||||||
|
}
|
||||||
|
if (data.sectionNumber) {
|
||||||
|
this.contentsTab.pageParams!.sectionNumber = data.sectionNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select course contents.
|
||||||
|
this.tabsComponent?.selectByIndex(0);
|
||||||
|
} else if (this.tabs) {
|
||||||
|
const index = this.tabs.findIndex((tab) => tab.name == data.name);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.tabsComponent?.selectByIndex(index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component being initialized.
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
// Get params.
|
||||||
|
this.course = CoreNavigator.instance.getRouteParam('course');
|
||||||
|
this.firstTabName = CoreNavigator.instance.getRouteParam('selectedTab');
|
||||||
|
const module = CoreNavigator.instance.getRouteParam<CoreCourseWSModule>('module');
|
||||||
|
const modParams = CoreNavigator.instance.getRouteParam<Params>('modParams');
|
||||||
|
|
||||||
|
this.currentPagePath = CoreNavigator.instance.getCurrentPath();
|
||||||
|
this.contentsTab.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, this.contentsTab.page);
|
||||||
|
this.contentsTab.pageParams = {
|
||||||
|
course: this.course,
|
||||||
|
sectionId: CoreNavigator.instance.getRouteNumberParam('sectionId'),
|
||||||
|
sectionNumber: CoreNavigator.instance.getRouteNumberParam('sectionNumber'),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
this.contentsTab.pageParams!.moduleId = module.id;
|
||||||
|
CoreCourseHelper.instance.openModule(module, this.course!.id, this.contentsTab.pageParams!.sectionId, modParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tabs.push(this.contentsTab);
|
||||||
|
this.loaded = true;
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.loadCourseHandlers(),
|
||||||
|
this.loadTitle(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load course option handlers.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadCourseHandlers(): Promise<void> {
|
||||||
|
// Load the course handlers.
|
||||||
|
const handlers = await CoreCourseOptionsDelegate.instance.getHandlersToDisplay(this.course!, false, false);
|
||||||
|
|
||||||
|
this.tabs.concat(handlers.map(handler => handler.data));
|
||||||
|
|
||||||
|
let tabToLoad: number | undefined;
|
||||||
|
|
||||||
|
// Add the courseId to the handler component data.
|
||||||
|
handlers.forEach((handler, index) => {
|
||||||
|
handler.data.page = CoreTextUtils.instance.concatenatePaths(this.currentPagePath, handler.data.page);
|
||||||
|
handler.data.pageParams = handler.data.pageParams || {};
|
||||||
|
handler.data.pageParams.courseId = this.course!.id;
|
||||||
|
|
||||||
|
// Check if this handler should be the first selected tab.
|
||||||
|
if (this.firstTabName && handler.name == this.firstTabName) {
|
||||||
|
tabToLoad = index + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select the tab if needed.
|
||||||
|
this.firstTabName = undefined;
|
||||||
|
if (tabToLoad) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.tabsComponent?.selectByIndex(tabToLoad!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load title for the page.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async loadTitle(): Promise<void> {
|
||||||
|
// Get the title to display initially.
|
||||||
|
this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!);
|
||||||
|
|
||||||
|
// Load sections.
|
||||||
|
const sections = await CoreUtils.instance.ignoreErrors(CoreCourse.instance.getSections(this.course!.id, false, true));
|
||||||
|
|
||||||
|
if (!sections) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the title again now that we have sections.
|
||||||
|
this.title = CoreCourseFormatDelegate.instance.getCourseTitle(this.course!, sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page destroyed.
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.selectTabObserver?.off();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User entered the page.
|
||||||
|
*/
|
||||||
|
ionViewDidEnter(): void {
|
||||||
|
this.tabsComponent?.ionViewDidEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User left the page.
|
||||||
|
*/
|
||||||
|
ionViewDidLeave(): void {
|
||||||
|
this.tabsComponent?.ionViewDidLeave();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type CourseTab = CoreTab & {
|
||||||
|
name?: string;
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>{{ title }}</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<ion-refresher slot="fixed" [disabled]="!loaded" (ionRefresh)="refreshData($event)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
<core-loading [hideUntil]="loaded">
|
||||||
|
<core-empty-box *ngIf="!sections || !sections.length" icon="qr-scanner"
|
||||||
|
[message]="'core.course.nocontentavailable' | translate">
|
||||||
|
</core-empty-box>
|
||||||
|
|
||||||
|
<ion-list>
|
||||||
|
<ng-container *ngFor="let section of sections" >
|
||||||
|
<ng-container *ngFor="let module of section.modules">
|
||||||
|
<core-course-module *ngIf="module.visibleoncoursepage !== 0" [module]="module" [section]="section"
|
||||||
|
[courseId]="courseId" [downloadEnabled]="downloadEnabled">
|
||||||
|
</core-course-module>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</ion-list>
|
||||||
|
</core-loading>
|
||||||
|
</ion-content>
|
|
@ -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 { 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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseListModTypePage } from './list-mod-type';
|
||||||
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreCourseListModTypePage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
CoreCourseListModTypePage,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreCourseListModTypePageModule {}
|
|
@ -0,0 +1,132 @@
|
||||||
|
// (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 { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreConstants } from '@/core/constants';
|
||||||
|
import { IonRefresher } from '@ionic/angular';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays all modules of a certain type in a course.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-course-list-mod-type',
|
||||||
|
templateUrl: 'list-mod-type.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseListModTypePage implements OnInit {
|
||||||
|
|
||||||
|
sections: CoreCourseSection[] = [];
|
||||||
|
title = '';
|
||||||
|
loaded = false;
|
||||||
|
downloadEnabled = false;
|
||||||
|
courseId?: number;
|
||||||
|
|
||||||
|
protected modName?: string;
|
||||||
|
protected archetypes: Record<string, number> = {}; // To speed up the check of modules.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
this.title = CoreNavigator.instance.getRouteParam('title') || '';
|
||||||
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
|
||||||
|
this.modName = CoreNavigator.instance.getRouteParam('modName');
|
||||||
|
this.downloadEnabled = !CoreSites.instance.getCurrentSite()?.isOfflineDisabled();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchData();
|
||||||
|
} finally {
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the data.
|
||||||
|
*
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchData(): Promise<void> {
|
||||||
|
if (!this.courseId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all the modules in the course.
|
||||||
|
let sections = await CoreCourse.instance.getSections(this.courseId, false, true);
|
||||||
|
|
||||||
|
sections = sections.filter((section) => {
|
||||||
|
if (!section.modules) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.modules = section.modules.filter((mod) => {
|
||||||
|
if (mod.uservisible === false || !CoreCourse.instance.moduleHasView(mod)) {
|
||||||
|
// Ignore this module.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.modName === 'resources') {
|
||||||
|
// Check that the module is a resource.
|
||||||
|
if (typeof this.archetypes[mod.modname] == 'undefined') {
|
||||||
|
this.archetypes[mod.modname] = CoreCourseModuleDelegate.instance.supportsFeature<number>(
|
||||||
|
mod.modname,
|
||||||
|
CoreConstants.FEATURE_MOD_ARCHETYPE,
|
||||||
|
CoreConstants.MOD_ARCHETYPE_OTHER,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.archetypes[mod.modname] == CoreConstants.MOD_ARCHETYPE_RESOURCE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (mod.modname == this.modName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return section.modules.length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = CoreCourseHelper.instance.addHandlerDataForModules(sections, this.courseId);
|
||||||
|
|
||||||
|
this.sections = result.sections;
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the data.
|
||||||
|
*
|
||||||
|
* @param refresher Refresher.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
async refreshData(refresher: CustomEvent<IonRefresher>): Promise<void> {
|
||||||
|
await CoreUtils.instance.ignoreErrors(CoreCourse.instance.invalidateSections(this.courseId || 0));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.fetchData();
|
||||||
|
} finally {
|
||||||
|
refresher.detail.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<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]="module?.name" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</ion-title>
|
||||||
|
|
||||||
|
<ion-buttons slot="end">
|
||||||
|
<core-context-menu>
|
||||||
|
<core-context-menu-item [priority]="900" *ngIf="module?.url" [href]="module!.url"
|
||||||
|
[content]="'core.openinbrowser' | translate" iconAction="fas-external-link-alt">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item [priority]="800" *ngIf="module?.description" [content]="'core.moduleintro' | translate"
|
||||||
|
(action)="expandDescription()" iconAction="fas-arrow-right">
|
||||||
|
</core-context-menu-item>
|
||||||
|
</core-context-menu>
|
||||||
|
</ion-buttons>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
<core-course-unsupported-module [module]="module" [courseId]="courseId"></core-course-unsupported-module>
|
||||||
|
</ion-content>
|
|
@ -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 { 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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { CoreCourseUnsupportedModulePage } from './unsupported-module';
|
||||||
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CoreCourseUnsupportedModulePage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CommonModule,
|
||||||
|
IonicModule,
|
||||||
|
TranslateModule.forChild(),
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
CoreCourseUnsupportedModulePage,
|
||||||
|
],
|
||||||
|
exports: [RouterModule],
|
||||||
|
})
|
||||||
|
export class CoreCourseUnsupportedModulePageModule {}
|
|
@ -0,0 +1,54 @@
|
||||||
|
// (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 { CoreCourseWSModule } from '@features/course/services/course';
|
||||||
|
import { CoreNavigator } from '@services/navigator';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays info about an unsupported module.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-core-course-unsupported-module',
|
||||||
|
templateUrl: 'unsupported-module.html',
|
||||||
|
})
|
||||||
|
export class CoreCourseUnsupportedModulePage implements OnInit {
|
||||||
|
|
||||||
|
module?: CoreCourseWSModule;
|
||||||
|
courseId?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.module = CoreNavigator.instance.getRouteParam('module');
|
||||||
|
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand the description.
|
||||||
|
*/
|
||||||
|
expandDescription(): void {
|
||||||
|
CoreTextUtils.instance.viewText(Translate.instance.instant('core.description'), this.module!.description!, {
|
||||||
|
filter: true,
|
||||||
|
contextLevel: 'module',
|
||||||
|
instanceId: this.module!.id,
|
||||||
|
courseId: this.courseId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,14 +11,19 @@
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
// @todo test delegate
|
|
||||||
|
|
||||||
import { Injectable, Type } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate';
|
||||||
import { CoreEvents } from '@singletons/events';
|
import { CoreEvents } from '@singletons/events';
|
||||||
import { CoreSites } from '@services/sites';
|
import { CoreSites } from '@services/sites';
|
||||||
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
||||||
import { CoreCourses, CoreCoursesProvider, CoreCourseUserAdminOrNavOptionIndexed } from '@features/courses/services/courses';
|
import {
|
||||||
|
CoreCourseAnyCourseData,
|
||||||
|
CoreCourseAnyCourseDataWithOptions,
|
||||||
|
CoreCourses,
|
||||||
|
CoreCoursesProvider,
|
||||||
|
CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
|
} from '@features/courses/services/courses';
|
||||||
import { CoreCourseProvider } from './course';
|
import { CoreCourseProvider } from './course';
|
||||||
import { Params } from '@angular/router';
|
import { Params } from '@angular/router';
|
||||||
import { makeSingleton } from '@singletons';
|
import { makeSingleton } from '@singletons';
|
||||||
|
@ -47,8 +52,9 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
||||||
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||||
* @return True or promise resolved with true if enabled.
|
* @return True or promise resolved with true if enabled.
|
||||||
*/
|
*/
|
||||||
isEnabledForCourse(courseId: number,
|
isEnabledForCourse(
|
||||||
accessData: CoreCourseAccessData,
|
courseId: number,
|
||||||
|
accessData: CoreCourseAccess,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
): boolean | Promise<boolean>;
|
): boolean | Promise<boolean>;
|
||||||
|
@ -56,11 +62,11 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
||||||
/**
|
/**
|
||||||
* Returns the data needed to render the handler.
|
* Returns the data needed to render the handler.
|
||||||
*
|
*
|
||||||
* @param course The course. // @todo: define type in the whole file.
|
* @param course The course.
|
||||||
* @return Data or promise resolved with the data.
|
* @return Data or promise resolved with the data.
|
||||||
*/
|
*/
|
||||||
getDisplayData?(
|
getDisplayData?(
|
||||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
course: CoreCourseAnyCourseDataWithOptions,
|
||||||
): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>;
|
): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,7 +103,7 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler {
|
||||||
* @return Data or promise resolved with data.
|
* @return Data or promise resolved with data.
|
||||||
*/
|
*/
|
||||||
getMenuDisplayData(
|
getMenuDisplayData(
|
||||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
course: CoreCourseAnyCourseDataWithOptions,
|
||||||
): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>;
|
): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,15 +122,14 @@ export interface CoreCourseOptionsHandlerData {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The component to render the handler. It must be the component class, not the name or an instance.
|
* Path of the page to load for the handler.
|
||||||
* When the component is created, it will receive the courseId as input.
|
|
||||||
*/
|
*/
|
||||||
component: Type<unknown>;
|
page: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data to pass to the component. All the properties in this object will be passed to the component as inputs.
|
* Params to pass to the page (other than 'courseId' which is always sent).
|
||||||
*/
|
*/
|
||||||
componentData?: Record<string | number, unknown>;
|
pageParams?: Params;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,7 +147,7 @@ export interface CoreCourseOptionsMenuHandlerData {
|
||||||
class?: string;
|
class?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Name of the page to load for the handler.
|
* Path of the page to load for the handler.
|
||||||
*/
|
*/
|
||||||
page: string;
|
page: string;
|
||||||
|
|
||||||
|
@ -160,29 +165,19 @@ export interface CoreCourseOptionsMenuHandlerData {
|
||||||
/**
|
/**
|
||||||
* Data returned by the delegate for each handler.
|
* Data returned by the delegate for each handler.
|
||||||
*/
|
*/
|
||||||
export interface CoreCourseOptionsHandlerToDisplay {
|
export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay {
|
||||||
/**
|
/**
|
||||||
* Data to display.
|
* Data to display.
|
||||||
*/
|
*/
|
||||||
data: CoreCourseOptionsHandlerData;
|
data: CoreCourseOptionsHandlerData;
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of the handler, or name and sub context (AddonMessages, AddonMessages:blockContact, ...).
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The highest priority is displayed first.
|
|
||||||
*/
|
|
||||||
priority?: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
|
* Called when a course is downloaded. It should prefetch all the data to be able to see the addon in offline.
|
||||||
*
|
*
|
||||||
* @param course The course.
|
* @param course The course.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void>;
|
prefetch?(course: CoreCourseAnyCourseData): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -210,7 +205,7 @@ export interface CoreCourseOptionsMenuHandlerToDisplay {
|
||||||
* @param course The course.
|
* @param course The course.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
prefetch?(course: CoreEnrolledCourseDataWithExtraInfoAndOptions): Promise<void>;
|
prefetch?(course: CoreCourseAnyCourseData): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -226,7 +221,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
|
|
||||||
protected coursesHandlers: {
|
protected coursesHandlers: {
|
||||||
[courseId: number]: {
|
[courseId: number]: {
|
||||||
access: any;
|
access: CoreCourseAccess;
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||||
deferred: PromiseDefer<void>;
|
deferred: PromiseDefer<void>;
|
||||||
|
@ -320,7 +315,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
protected async getHandlersForAccess(
|
protected async getHandlersForAccess(
|
||||||
courseId: number,
|
courseId: number,
|
||||||
refresh: boolean,
|
refresh: boolean,
|
||||||
accessData: any,
|
accessData: CoreCourseAccess,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
): Promise<CoreCourseOptionsHandler[]> {
|
): Promise<CoreCourseOptionsHandler[]> {
|
||||||
|
@ -367,7 +362,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
* @return Promise resolved with array of handlers.
|
* @return Promise resolved with array of handlers.
|
||||||
*/
|
*/
|
||||||
getHandlersToDisplay(
|
getHandlersToDisplay(
|
||||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
course: CoreCourseAnyCourseData,
|
||||||
refresh = false,
|
refresh = false,
|
||||||
isGuest = false,
|
isGuest = false,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
|
@ -389,7 +384,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
* @return Promise resolved with array of handlers.
|
* @return Promise resolved with array of handlers.
|
||||||
*/
|
*/
|
||||||
getMenuHandlersToDisplay(
|
getMenuHandlersToDisplay(
|
||||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
course: CoreCourseAnyCourseData,
|
||||||
refresh = false,
|
refresh = false,
|
||||||
isGuest = false,
|
isGuest = false,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
|
@ -413,28 +408,31 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
*/
|
*/
|
||||||
protected async getHandlersToDisplayInternal(
|
protected async getHandlersToDisplayInternal(
|
||||||
menu: boolean,
|
menu: boolean,
|
||||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
course: CoreCourseAnyCourseData,
|
||||||
refresh = false,
|
refresh = false,
|
||||||
isGuest = false,
|
isGuest = false,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> {
|
): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> {
|
||||||
|
|
||||||
|
const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course;
|
||||||
const accessData = {
|
const accessData = {
|
||||||
type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT,
|
type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT,
|
||||||
};
|
};
|
||||||
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
||||||
|
|
||||||
if (navOptions) {
|
if (navOptions) {
|
||||||
course.navOptions = navOptions;
|
courseWithOptions.navOptions = navOptions;
|
||||||
}
|
}
|
||||||
if (admOptions) {
|
if (admOptions) {
|
||||||
course.admOptions = admOptions;
|
courseWithOptions.admOptions = admOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.loadCourseOptions(course, refresh);
|
await this.loadCourseOptions(courseWithOptions, refresh);
|
||||||
|
|
||||||
// Call getHandlersForAccess to make sure the handlers have been loaded.
|
// Call getHandlersForAccess to make sure the handlers have been loaded.
|
||||||
await this.getHandlersForAccess(course.id, refresh, accessData, course.navOptions, course.admOptions);
|
await this.getHandlersForAccess(course.id, refresh, accessData, courseWithOptions.navOptions, courseWithOptions.admOptions);
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[];
|
let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[];
|
||||||
|
@ -449,7 +447,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
|
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
|
||||||
: (handler as CoreCourseOptionsHandler).getDisplayData;
|
: (handler as CoreCourseOptionsHandler).getDisplayData;
|
||||||
|
|
||||||
promises.push(Promise.resolve(getFunction!.call(handler, course)).then((data) => {
|
promises.push(Promise.resolve(getFunction!.call(handler, courseWithOptions)).then((data) => {
|
||||||
handlersToDisplay.push({
|
handlersToDisplay.push({
|
||||||
data: data,
|
data: data,
|
||||||
priority: handler.priority,
|
priority: handler.priority,
|
||||||
|
@ -586,7 +584,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
* @param refresh True if it should refresh the list.
|
* @param refresh True if it should refresh the list.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
protected async loadCourseOptions(course: CoreEnrolledCourseDataWithExtraInfoAndOptions, refresh = false): Promise<void> {
|
protected async loadCourseOptions(course: CoreCourseAnyCourseDataWithOptions, refresh = false): Promise<void> {
|
||||||
if (CoreCourses.instance.canGetAdminAndNavOptions() &&
|
if (CoreCourses.instance.canGetAdminAndNavOptions() &&
|
||||||
(typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) {
|
(typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) {
|
||||||
|
|
||||||
|
@ -618,7 +616,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
*/
|
*/
|
||||||
async updateHandlersForCourse(
|
async updateHandlersForCourse(
|
||||||
courseId: number,
|
courseId: number,
|
||||||
accessData: any,
|
accessData: CoreCourseAccess,
|
||||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -673,5 +671,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
||||||
|
|
||||||
export class CoreCourseOptionsDelegate extends makeSingleton(CoreCourseOptionsDelegateService) {}
|
export class CoreCourseOptionsDelegate extends makeSingleton(CoreCourseOptionsDelegateService) {}
|
||||||
|
|
||||||
// @todo define
|
export type CoreCourseAccess = {
|
||||||
export type CoreCourseAccessData = any;
|
type: string; // Either CoreCourseProvider.ACCESS_GUEST or CoreCourseProvider.ACCESS_DEFAULT.
|
||||||
|
};
|
||||||
|
|
|
@ -23,19 +23,24 @@ import { CoreTimeUtils } from '@services/utils/time';
|
||||||
import { CoreUtils } from '@services/utils/utils';
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
import { CoreSiteWSPreSets, CoreSite } from '@classes/site';
|
import { CoreSiteWSPreSets, CoreSite } from '@classes/site';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { makeSingleton, Translate } from '@singletons';
|
import { makeSingleton, Platform, Translate } from '@singletons';
|
||||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile } from '@services/ws';
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile } from '@services/ws';
|
||||||
|
|
||||||
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
|
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
|
||||||
import { CoreCourseOffline } from './course-offline';
|
import { CoreCourseOffline } from './course-offline';
|
||||||
import { CoreError } from '@classes/errors/error';
|
import { CoreError } from '@classes/errors/error';
|
||||||
import {
|
import {
|
||||||
CoreCourses,
|
CoreCourseAnyCourseData,
|
||||||
|
CoreCoursesMyCoursesUpdatedEventData,
|
||||||
CoreCoursesProvider,
|
CoreCoursesProvider,
|
||||||
} from '../../courses/services/courses';
|
} from '../../courses/services/courses';
|
||||||
import { CoreDomUtils } from '@services/utils/dom';
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
import { CoreWSError } from '@classes/errors/wserror';
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
import { CorePushNotifications } from '@features/pushnotifications/services/pushnotifications';
|
||||||
|
import { CoreCourseHelper, CoreCourseModuleCompletionData } from './course-helper';
|
||||||
|
import { CoreCourseFormatDelegate } from './format-delegate';
|
||||||
|
import { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { CoreCourseLogCronHandler } from './handlers/log-cron';
|
||||||
|
|
||||||
const ROOT_CACHE_KEY = 'mmCourse:';
|
const ROOT_CACHE_KEY = 'mmCourse:';
|
||||||
|
|
||||||
|
@ -71,12 +76,30 @@ export class CoreCourseProvider {
|
||||||
protected logger: CoreLogger;
|
protected logger: CoreLogger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// @todo
|
|
||||||
// protected courseFormatDelegate: CoreCourseFormatDelegate,
|
|
||||||
// protected sitePluginsProvider: CoreSitePluginsProvider,
|
|
||||||
this.logger = CoreLogger.getInstance('CoreCourseProvider');
|
this.logger = CoreLogger.getInstance('CoreCourseProvider');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize.
|
||||||
|
*/
|
||||||
|
initialize(): void {
|
||||||
|
Platform.instance.resume.subscribe(() => {
|
||||||
|
// Run the handler the app is open to keep user in online status.
|
||||||
|
setTimeout(() => {
|
||||||
|
CoreCronDelegate.instance.forceCronHandlerExecution(CoreCourseLogCronHandler.instance.name);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
CoreEvents.on(CoreEvents.LOGIN, () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Ignore errors here, since probably login is not complete: it happens on token invalid.
|
||||||
|
CoreUtils.instance.ignoreErrors(
|
||||||
|
CoreCronDelegate.instance.forceCronHandlerExecution(CoreCourseLogCronHandler.instance.name),
|
||||||
|
);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the get course blocks WS is available in current site.
|
* Check if the get course blocks WS is available in current site.
|
||||||
*
|
*
|
||||||
|
@ -109,9 +132,8 @@ export class CoreCourseProvider {
|
||||||
*
|
*
|
||||||
* @param courseId Course ID.
|
* @param courseId Course ID.
|
||||||
* @param completion Completion status of the module.
|
* @param completion Completion status of the module.
|
||||||
* @todo Add completion type.
|
|
||||||
*/
|
*/
|
||||||
checkModuleCompletion(courseId: number, completion: any): void {
|
checkModuleCompletion(courseId: number, completion: CoreCourseModuleCompletionData): void {
|
||||||
if (completion && completion.tracking === 2 && completion.state === 0) {
|
if (completion && completion.tracking === 2 && completion.state === 0) {
|
||||||
this.invalidateSections(courseId).finally(() => {
|
this.invalidateSections(courseId).finally(() => {
|
||||||
CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { courseId: courseId });
|
CoreEvents.trigger(CoreEvents.COMPLETION_MODULE_VIEWED, { courseId: courseId });
|
||||||
|
@ -134,9 +156,8 @@ export class CoreCourseProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the current view in a NavController is a certain course initial page.
|
* Check if the current view is a certain course initial page.
|
||||||
*
|
*
|
||||||
* @param navCtrl NavController.
|
|
||||||
* @param courseId Course ID.
|
* @param courseId Course ID.
|
||||||
* @return Whether the current view is a certain course.
|
* @return Whether the current view is a certain course.
|
||||||
*/
|
*/
|
||||||
|
@ -346,7 +367,7 @@ export class CoreCourseProvider {
|
||||||
ignoreCache: boolean = false,
|
ignoreCache: boolean = false,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
modName?: string,
|
modName?: string,
|
||||||
): Promise<CoreCourseModuleData> {
|
): Promise<CoreCourseWSModule> {
|
||||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||||
|
|
||||||
// Helper function to do the WS request without processing the result.
|
// Helper function to do the WS request without processing the result.
|
||||||
|
@ -356,7 +377,7 @@ export class CoreCourseProvider {
|
||||||
modName: string | undefined,
|
modName: string | undefined,
|
||||||
includeStealth: boolean,
|
includeStealth: boolean,
|
||||||
preferCache: boolean,
|
preferCache: boolean,
|
||||||
): Promise<CoreCourseSection[]> => {
|
): Promise<CoreCourseWSSection[]> => {
|
||||||
const params: CoreCourseGetContentsParams = {
|
const params: CoreCourseGetContentsParams = {
|
||||||
courseid: courseId!,
|
courseid: courseId!,
|
||||||
options: [],
|
options: [],
|
||||||
|
@ -394,7 +415,7 @@ export class CoreCourseProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sections: CoreCourseSection[] = await site.read('core_course_get_contents', params, preSets);
|
const sections = await site.read<CoreCourseWSSection[]>('core_course_get_contents', params, preSets);
|
||||||
|
|
||||||
return sections;
|
return sections;
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -419,7 +440,7 @@ export class CoreCourseProvider {
|
||||||
courseId = module.course;
|
courseId = module.course;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sections: CoreCourseSection[];
|
let sections: CoreCourseWSSection[];
|
||||||
try {
|
try {
|
||||||
const site = await CoreSites.instance.getSite(siteId);
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
// We have courseId, we can use core_course_get_contents for compatibility.
|
// We have courseId, we can use core_course_get_contents for compatibility.
|
||||||
|
@ -440,7 +461,7 @@ export class CoreCourseProvider {
|
||||||
sections = await this.getSections(courseId, false, false, preSets, siteId);
|
sections = await this.getSections(courseId, false, false, preSets, siteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundModule: CoreCourseModuleData | undefined;
|
let foundModule: CoreCourseWSModule | undefined;
|
||||||
|
|
||||||
const foundSection = sections.some((section) => {
|
const foundSection = sections.some((section) => {
|
||||||
if (sectionId != null &&
|
if (sectionId != null &&
|
||||||
|
@ -637,7 +658,7 @@ export class CoreCourseProvider {
|
||||||
excludeModules?: boolean,
|
excludeModules?: boolean,
|
||||||
excludeContents?: boolean,
|
excludeContents?: boolean,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
): Promise<CoreCourseSection> {
|
): Promise<CoreCourseWSSection> {
|
||||||
|
|
||||||
if (sectionId < 0) {
|
if (sectionId < 0) {
|
||||||
throw new CoreError('Invalid section ID');
|
throw new CoreError('Invalid section ID');
|
||||||
|
@ -671,7 +692,7 @@ export class CoreCourseProvider {
|
||||||
preSets?: CoreSiteWSPreSets,
|
preSets?: CoreSiteWSPreSets,
|
||||||
siteId?: string,
|
siteId?: string,
|
||||||
includeStealthModules: boolean = true,
|
includeStealthModules: boolean = true,
|
||||||
): Promise<CoreCourseSection[]> {
|
): Promise<CoreCourseWSSection[]> {
|
||||||
|
|
||||||
const site = await CoreSites.instance.getSite(siteId);
|
const site = await CoreSites.instance.getSite(siteId);
|
||||||
preSets = preSets || {};
|
preSets = preSets || {};
|
||||||
|
@ -698,7 +719,7 @@ export class CoreCourseProvider {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sections: CoreCourseSection[];
|
let sections: CoreCourseWSSection[];
|
||||||
try {
|
try {
|
||||||
sections = await site.read('core_course_get_contents', params, preSets);
|
sections = await site.read('core_course_get_contents', params, preSets);
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -740,12 +761,12 @@ export class CoreCourseProvider {
|
||||||
* @param sections Sections.
|
* @param sections Sections.
|
||||||
* @return Modules.
|
* @return Modules.
|
||||||
*/
|
*/
|
||||||
getSectionsModules(sections: CoreCourseSection[]): CoreCourseModuleData[] {
|
getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseWSModule[] {
|
||||||
if (!sections || !sections.length) {
|
if (!sections || !sections.length) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections.reduce((previous: CoreCourseModuleData[], section) => previous.concat(section.modules || []), []);
|
return sections.reduce((previous: CoreCourseWSModule[], section) => previous.concat(section.modules || []), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -830,7 +851,7 @@ export class CoreCourseProvider {
|
||||||
* @return Promise resolved when loaded.
|
* @return Promise resolved when loaded.
|
||||||
*/
|
*/
|
||||||
async loadModuleContents(
|
async loadModuleContents(
|
||||||
module: CoreCourseModuleData & CoreCourseModuleBasicInfo,
|
module: CoreCourseWSModule,
|
||||||
courseId?: number,
|
courseId?: number,
|
||||||
sectionId?: number,
|
sectionId?: number,
|
||||||
preferCache?: boolean,
|
preferCache?: boolean,
|
||||||
|
@ -856,7 +877,6 @@ export class CoreCourseProvider {
|
||||||
* @param siteId Site ID. If not defined, current site.
|
* @param siteId Site ID. If not defined, current site.
|
||||||
* @param name Name of the course.
|
* @param name Name of the course.
|
||||||
* @return Promise resolved when the WS call is successful.
|
* @return Promise resolved when the WS call is successful.
|
||||||
* @todo use logHelper. Remove eslint disable when done.
|
|
||||||
*/
|
*/
|
||||||
async logView(courseId: number, sectionNumber?: number, siteId?: string, name?: string): Promise<void> {
|
async logView(courseId: number, sectionNumber?: number, siteId?: string, name?: string): Promise<void> {
|
||||||
const params: CoreCourseViewCourseWSParams = {
|
const params: CoreCourseViewCourseWSParams = {
|
||||||
|
@ -875,7 +895,7 @@ export class CoreCourseProvider {
|
||||||
if (!response.status) {
|
if (!response.status) {
|
||||||
throw Error('WS core_course_view_course failed.');
|
throw Error('WS core_course_view_course failed.');
|
||||||
} else {
|
} else {
|
||||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
CoreEvents.trigger<CoreCoursesMyCoursesUpdatedEventData>(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||||
courseId: courseId,
|
courseId: courseId,
|
||||||
action: CoreCoursesProvider.ACTION_VIEW,
|
action: CoreCoursesProvider.ACTION_VIEW,
|
||||||
}, site.getId());
|
}, site.getId());
|
||||||
|
@ -954,7 +974,20 @@ export class CoreCourseProvider {
|
||||||
completed: completed,
|
completed: completed,
|
||||||
};
|
};
|
||||||
|
|
||||||
return site.write('core_completion_update_activity_completion_status_manually', params);
|
const result = await site.write<CoreStatusWithWarningsWSResponse>(
|
||||||
|
'core_completion_update_activity_completion_status_manually',
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.status) {
|
||||||
|
if (result.warnings && result.warnings.length) {
|
||||||
|
throw new CoreWSError(result.warnings[0]);
|
||||||
|
} else {
|
||||||
|
throw new CoreError('Cannot change completion.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -963,7 +996,7 @@ export class CoreCourseProvider {
|
||||||
* @param module The module object.
|
* @param module The module object.
|
||||||
* @return Whether the module has a view page.
|
* @return Whether the module has a view page.
|
||||||
*/
|
*/
|
||||||
moduleHasView(module: CoreCourseModuleSummary | CoreCourseModuleData): boolean {
|
moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean {
|
||||||
return !!module.url;
|
return !!module.url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -981,63 +1014,42 @@ export class CoreCourseProvider {
|
||||||
* @param params Other params to pass to the course page.
|
* @param params Other params to pass to the course page.
|
||||||
* @return Promise resolved when done.
|
* @return Promise resolved when done.
|
||||||
*/
|
*/
|
||||||
async openCourse(
|
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> {
|
||||||
course: { id: number ; format?: string },
|
const loading = await CoreDomUtils.instance.showModalLoading();
|
||||||
params?: Params, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
): Promise<void> {
|
|
||||||
// @todo const loading = await CoreDomUtils.instance.showModalLoading();
|
|
||||||
|
|
||||||
// Wait for site plugins to be fetched.
|
// Wait for site plugins to be fetched.
|
||||||
// @todo await this.sitePluginsProvider.waitFetchPlugins();
|
// @todo await this.sitePluginsProvider.waitFetchPlugins();
|
||||||
|
|
||||||
if (typeof course.format == 'undefined') {
|
if (!('format' in course) || typeof course.format == 'undefined') {
|
||||||
// This block can be replaced by a call to CourseHelper.getCourse(), but it is circular dependant.
|
const result = await CoreCourseHelper.instance.getCourse(course.id);
|
||||||
const coursesProvider = CoreCourses.instance;
|
|
||||||
try {
|
course = result.course;
|
||||||
course = await coursesProvider.getUserCourse(course.id, true);
|
|
||||||
} catch (error) {
|
|
||||||
// Not enrolled or an error happened. Try to use another WebService.
|
|
||||||
const available = coursesProvider.isGetCoursesByFieldAvailableInSite();
|
|
||||||
try {
|
|
||||||
if (available) {
|
|
||||||
course = await coursesProvider.getCourseByField('id', course.id);
|
|
||||||
} else {
|
|
||||||
course = await coursesProvider.getCourse(course.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* @todo
|
if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
||||||
if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
|
||||||
// No custom format plugin. We don't need to wait for anything.
|
// No custom format plugin. We don't need to wait for anything.
|
||||||
await this.courseFormatDelegate.openCourse(course, params);
|
await CoreCourseFormatDelegate.instance.openCourse(<CoreCourseAnyCourseData> course, params);
|
||||||
loading.dismiss();
|
loading.dismiss();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} */
|
}
|
||||||
|
|
||||||
// This course uses a custom format plugin, wait for the format plugin to finish loading.
|
// This course uses a custom format plugin, wait for the format plugin to finish loading.
|
||||||
try {
|
try {
|
||||||
/* @todo await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format);
|
/* @todo await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format);
|
||||||
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
|
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
|
||||||
if (this.sitePluginsProvider.sitePluginsFinishedLoading) {
|
if (this.sitePluginsProvider.sitePluginsFinishedLoading) {
|
||||||
return this.courseFormatDelegate.openCourse(course, params);
|
return CoreCourseFormatDelegate.instance.openCourse(course, params);
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// Wait for plugins to be loaded.
|
// Wait for plugins to be loaded.
|
||||||
const deferred = CoreUtils.instance.promiseDefer<void>();
|
const deferred = CoreUtils.instance.promiseDefer<void>();
|
||||||
|
|
||||||
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
|
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
|
||||||
observer && observer.off();
|
observer?.off();
|
||||||
|
|
||||||
/* @todo this.courseFormatDelegate.openCourse(course, params).then((response) => {
|
CoreCourseFormatDelegate.instance.openCourse(<CoreCourseAnyCourseData> course, params)
|
||||||
deferred.resolve(response);
|
.then(deferred.resolve).catch(deferred.reject);
|
||||||
}).catch((error) => {
|
|
||||||
deferred.reject(error);
|
|
||||||
});*/
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
|
@ -1334,7 +1346,7 @@ export type CoreCourseGetContentsParams = {
|
||||||
/**
|
/**
|
||||||
* Data returned by core_course_get_contents WS.
|
* Data returned by core_course_get_contents WS.
|
||||||
*/
|
*/
|
||||||
export type CoreCourseSection = {
|
export type CoreCourseWSSection = {
|
||||||
id: number; // Section ID.
|
id: number; // Section ID.
|
||||||
name: string; // Section name.
|
name: string; // Section name.
|
||||||
visible?: number; // Is the section visible.
|
visible?: number; // Is the section visible.
|
||||||
|
@ -1344,7 +1356,7 @@ export type CoreCourseSection = {
|
||||||
hiddenbynumsections?: number; // Whether is a section hidden in the course format.
|
hiddenbynumsections?: number; // Whether is a section hidden in the course format.
|
||||||
uservisible?: boolean; // Is the section visible for the user?.
|
uservisible?: boolean; // Is the section visible for the user?.
|
||||||
availabilityinfo?: string; // Availability information.
|
availabilityinfo?: string; // Availability information.
|
||||||
modules: CoreCourseModuleData[];
|
modules: CoreCourseWSModule[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1371,9 +1383,9 @@ export type CoreCourseGetCourseModuleWSResponse = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Course module type.
|
* Course module data returned by the WS.
|
||||||
*/
|
*/
|
||||||
export type CoreCourseModuleData = { // List of module.
|
export type CoreCourseWSModule = {
|
||||||
id: number; // Activity id.
|
id: number; // Activity id.
|
||||||
course?: number; // The course id.
|
course?: number; // The course id.
|
||||||
url?: string; // Activity url.
|
url?: string; // Activity url.
|
||||||
|
@ -1395,12 +1407,7 @@ export type CoreCourseModuleData = { // List of module.
|
||||||
customdata?: string; // Custom data (JSON encoded).
|
customdata?: string; // Custom data (JSON encoded).
|
||||||
noviewlink?: boolean; // Whether the module has no view page.
|
noviewlink?: boolean; // Whether the module has no view page.
|
||||||
completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic.
|
completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic.
|
||||||
completiondata?: { // Module completion data.
|
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
||||||
state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail.
|
|
||||||
timecompleted: number; // Timestamp for completion status.
|
|
||||||
overrideby: number; // The user id who has overriden the status.
|
|
||||||
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
|
||||||
};
|
|
||||||
contents: CoreCourseModuleContentFile[];
|
contents: CoreCourseModuleContentFile[];
|
||||||
contentsinfo?: { // Contents summary information.
|
contentsinfo?: { // Contents summary information.
|
||||||
filescount: number; // Total number of files.
|
filescount: number; // Total number of files.
|
||||||
|
@ -1411,19 +1418,28 @@ export type CoreCourseModuleData = { // List of module.
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module completion data.
|
||||||
|
*/
|
||||||
|
export type CoreCourseModuleWSCompletionData = {
|
||||||
|
state: number; // Completion state value: 0 means incomplete, 1 complete, 2 complete pass, 3 complete fail.
|
||||||
|
timecompleted: number; // Timestamp for completion status.
|
||||||
|
overrideby: number; // The user id who has overriden the status.
|
||||||
|
valueused?: boolean; // Whether the completion status affects the availability of another activity.
|
||||||
|
};
|
||||||
|
|
||||||
export type CoreCourseModuleContentFile = {
|
export type CoreCourseModuleContentFile = {
|
||||||
type: string; // A file or a folder or external link.
|
type: string; // A file or a folder or external link.
|
||||||
filename: string; // Filename.
|
filename: string; // Filename.
|
||||||
filepath: string; // Filepath.
|
filepath: string; // Filepath.
|
||||||
filesize: number; // Filesize.
|
filesize: number; // Filesize.
|
||||||
fileurl?: string; // Downloadable file url.
|
fileurl: string; // Downloadable file url.
|
||||||
url?: string; // @deprecated. Use fileurl instead.
|
|
||||||
content?: string; // Raw content, will be used when type is content.
|
content?: string; // Raw content, will be used when type is content.
|
||||||
timecreated: number; // Time created.
|
timecreated: number; // Time created.
|
||||||
timemodified: number; // Time modified.
|
timemodified: number; // Time modified.
|
||||||
sortorder: number; // Content sort order.
|
sortorder: number; // Content sort order.
|
||||||
mimetype?: string; // File mime type.
|
mimetype?: string; // File mime type.
|
||||||
isexternalfile?: boolean; // Whether is an external file.
|
isexternalfile?: number; // Whether is an external file.
|
||||||
repositorytype?: string; // The repository type for external files.
|
repositorytype?: string; // The repository type for external files.
|
||||||
userid: number; // User who added this content to moodle.
|
userid: number; // User who added this content to moodle.
|
||||||
author: string; // Content owner.
|
author: string; // Content owner.
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
// (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 { CoreSiteSchema } from '@services/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for CoreCourse service.
|
||||||
|
*/
|
||||||
|
export const ACTIVITY_LOG_TABLE = 'course_activity_log';
|
||||||
|
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'CoreCourseLogHelperProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: ACTIVITY_LOG_TABLE,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'component',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'componentid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ws',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'time',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['component', 'componentid', 'ws', 'time'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CoreCourseActivityLogDBRecord = {
|
||||||
|
component: string;
|
||||||
|
componentid: number;
|
||||||
|
ws: string;
|
||||||
|
time: number;
|
||||||
|
data?: string;
|
||||||
|
};
|