|
@ -201,6 +201,7 @@ const appConfig = {
|
|||
'no-duplicate-imports': 'error',
|
||||
'no-empty': 'error',
|
||||
'no-eval': 'error',
|
||||
'no-fallthrough': 'off',
|
||||
'no-invalid-this': 'error',
|
||||
'no-irregular-whitespace': 'error',
|
||||
'no-multiple-empty-lines': 'error',
|
||||
|
|
|
@ -20,8 +20,8 @@ import { CoreSites } from '@services/sites';
|
|||
import { CoreUser, CoreUserProfile } from '@features/user/services/user';
|
||||
import { AddonBadges, AddonBadgesUserBadge } from '../../services/badges';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
/**
|
||||
* Page that displays the list of calendar events.
|
||||
|
@ -42,18 +42,13 @@ export class AddonBadgesIssuedBadgePage implements OnInit {
|
|||
badgeLoaded = false;
|
||||
currentTime = 0;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
|
||||
this.userId = this.route.snapshot.queryParams['userId'] ||
|
||||
CoreSites.instance.getCurrentSite()?.getUserId();
|
||||
this.badgeHash = this.route.snapshot.queryParams['badgeHash'];
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||
this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSite()!.getUserId();
|
||||
this.badgeHash = CoreNavigator.instance.getRouteParam('badgeHash') || '';
|
||||
|
||||
this.fetchIssuedBadge().finally(() => {
|
||||
this.badgeLoaded = true;
|
||||
|
|
|
@ -20,7 +20,6 @@ import { CoreDomUtils } from '@services/utils/dom';
|
|||
import { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
// @todo import { CoreSplitViewComponent } from '@components/split-view/split-view';
|
||||
|
||||
/**
|
||||
|
@ -42,18 +41,13 @@ export class AddonBadgesUserBadgesPage implements OnInit {
|
|||
currentTime = 0;
|
||||
badgeHash!: string;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || this.courseId; // Use 0 for site badges.
|
||||
this.userId = this.route.snapshot.queryParams['userId'] ||
|
||||
CoreSites.instance.getCurrentSite()?.getUserId();
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || this.courseId; // Use 0 for site badges.
|
||||
this.userId = CoreNavigator.instance.getRouteNumberParam('userId') || CoreSites.instance.getCurrentSite()!.getUserId();
|
||||
|
||||
this.fetchBadges().finally(() => {
|
||||
// @todo splitview
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
|
||||
import { CoreCourseHelper } from '@features/course/services/course-helper';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CoreCourseSection } from '@features/course/services/course-helper';
|
||||
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';
|
||||
|
||||
/**
|
||||
|
@ -63,7 +63,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
|
||||
if (this.mainMenuBlock && this.mainMenuBlock.modules) {
|
||||
// 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);
|
||||
|
@ -77,8 +77,8 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
protected async fetchContent(): Promise<void> {
|
||||
const sections = await CoreCourse.instance.getSections(this.siteHomeId, false, true);
|
||||
|
||||
this.mainMenuBlock = sections.find((section) => section.section == 0);
|
||||
if (!this.mainMenuBlock) {
|
||||
const mainMenuBlock = sections.find((section) => section.section == 0);
|
||||
if (!mainMenuBlock) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -91,10 +91,17 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
|
|||
const items = config.frontpageloggedin.split(',');
|
||||
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
|
||||
|
||||
const hasContent = CoreCourseHelper.instance.sectionHasContent(this.mainMenuBlock);
|
||||
CoreCourseHelper.instance.addHandlerDataForModules([this.mainMenuBlock], this.siteHomeId, undefined, undefined, true);
|
||||
const result = await CoreCourseHelper.instance.addHandlerDataForModules(
|
||||
[mainMenuBlock],
|
||||
this.siteHomeId,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!hasNewsItem || !hasContent) {
|
||||
this.mainMenuBlock = result.sections[0];
|
||||
|
||||
if (!hasNewsItem || !this.mainMenuBlock.hasContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -149,6 +149,9 @@
|
|||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.core-module-icon[slot="start"] {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
:host-context([dir=rtl]) {
|
||||
|
|
|
@ -37,7 +37,7 @@ import { AddonCalendarFilterPopoverComponent } from '../../components/filter/fil
|
|||
import moment from 'moment';
|
||||
import { Network, NgZone } from '@singletons';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Params } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
|
@ -101,7 +101,6 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
private popoverCtrl: PopoverController,
|
||||
) {
|
||||
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
@ -235,19 +234,18 @@ export class AddonCalendarDayPage implements OnInit, OnDestroy {
|
|||
|
||||
CoreUtils.instance.enumKeys(AddonCalendarEventType).forEach((name) => {
|
||||
const value = AddonCalendarEventType[name];
|
||||
const filter = this.route.snapshot.queryParams[name];
|
||||
this.filter[name] = typeof filter == 'undefined' ? true : filter;
|
||||
this.filter[name] = CoreNavigator.instance.getRouteBooleanParam(name) ?? true;
|
||||
types.push(value);
|
||||
});
|
||||
this.filter.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || -1;
|
||||
this.filter.categoryId = parseInt(this.route.snapshot.queryParams['categoryId'], 10) || undefined;
|
||||
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||
this.filter.categoryId = CoreNavigator.instance.getRouteNumberParam('categoryId');
|
||||
|
||||
this.filter.filtered = typeof this.filter.courseId != 'undefined' || types.some((name) => !this.filter[name]);
|
||||
|
||||
const now = new Date();
|
||||
this.year = this.route.snapshot.queryParams['year'] || now.getFullYear();
|
||||
this.month = this.route.snapshot.queryParams['month'] || (now.getMonth() + 1);
|
||||
this.day = this.route.snapshot.queryParams['day'] || now.getDate();
|
||||
this.year = CoreNavigator.instance.getRouteNumberParam('year') || now.getFullYear();
|
||||
this.month = CoreNavigator.instance.getRouteNumberParam('month') || (now.getMonth() + 1);
|
||||
this.day = CoreNavigator.instance.getRouteNumberParam('day') || now.getDate();
|
||||
|
||||
this.calculateCurrentMoment();
|
||||
this.calculateIsCurrentDay();
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
||||
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 { CoreGroup, CoreGroups } from '@services/groups';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
@ -40,9 +40,9 @@ import { AddonCalendarSync, AddonCalendarSyncProvider } from '../../services/cal
|
|||
import { CoreSite } from '@classes/site';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { AddonCalendarOfflineEventDBRecord } from '../../services/database/calendar-offline';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
/**
|
||||
* Page that displays a form to create/edit an event.
|
||||
|
@ -90,8 +90,6 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
|
|||
protected gotEventData = false;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
protected fb: FormBuilder,
|
||||
) {
|
||||
|
||||
|
@ -128,11 +126,11 @@ export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
|
|||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.eventId = this.route.snapshot.queryParams['eventId'];
|
||||
this.courseId = parseInt(this.route.snapshot.queryParams['courseId'], 10) || 0;
|
||||
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || 0;
|
||||
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);
|
||||
this.form.addControl('timestart', this.fb.control(currentDate, Validators.required));
|
||||
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);
|
||||
} else {*/
|
||||
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 { CoreUtils } from '@services/utils/utils';
|
||||
import { AddonCalendarReminderDBRecord } from '../../services/database/calendar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Page that displays a single calendar event.
|
||||
|
@ -86,11 +85,7 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
|||
syncIcon = 'spinner'; // Sync icon.
|
||||
isSplitViewOn = false;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
// @Optional() private svComponent: CoreSplitViewComponent,
|
||||
) {
|
||||
|
||||
constructor() {
|
||||
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
||||
this.siteHomeId = CoreSites.instance.getCurrentSiteHomeId();
|
||||
this.currentSiteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
@ -150,8 +145,15 @@ export class AddonCalendarEventPage implements OnInit, OnDestroy {
|
|||
* View loaded.
|
||||
*/
|
||||
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.fetchEvent();
|
||||
|
|
|
@ -168,12 +168,12 @@ export class AddonCalendarIndexPage implements OnInit, OnDestroy {
|
|||
ngOnInit(): void {
|
||||
this.notificationsEnabled = CoreLocalNotifications.instance.isAvailable();
|
||||
|
||||
this.route.queryParams.subscribe(params => {
|
||||
this.eventId = parseInt(params['eventId'], 10) || undefined;
|
||||
this.filter.courseId = parseInt(params['courseId'], 10) || -1;
|
||||
this.year = parseInt(params['year'], 10) || undefined;
|
||||
this.month = parseInt(params['month'], 10) || undefined;
|
||||
this.loadUpcoming = !!params['upcoming'];
|
||||
this.route.queryParams.subscribe(() => {
|
||||
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||
this.year = CoreNavigator.instance.getRouteNumberParam('year');
|
||||
this.month = CoreNavigator.instance.getRouteNumberParam('month');
|
||||
this.loadUpcoming = !!CoreNavigator.instance.getRouteBooleanParam('upcoming');
|
||||
this.showCalendar = !this.loadUpcoming;
|
||||
this.filter.filtered = this.filter.courseId > 0;
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import { CoreApp } from '@services/app';
|
|||
import moment from 'moment';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { AddonCalendarFilterPopoverComponent } from '../../components/filter/filter';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { Params } from '@angular/router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { Network, NgZone } from '@singletons';
|
||||
import { CoreCoursesHelper } from '@features/courses/services/courses-helper';
|
||||
|
@ -102,7 +102,6 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
|
|||
};
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
private popoverCtrl: PopoverController,
|
||||
) {
|
||||
|
||||
|
@ -248,8 +247,8 @@ export class AddonCalendarListPage implements OnInit, OnDestroy {
|
|||
* View loaded.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.eventId = this.route.snapshot.queryParams['eventId'] || undefined;
|
||||
this.filter.courseId = this.route.snapshot.queryParams['courseId'];
|
||||
this.eventId = CoreNavigator.instance.getRouteNumberParam('eventId');
|
||||
this.filter.courseId = CoreNavigator.instance.getRouteNumberParam('courseId') || -1;
|
||||
|
||||
if (this.eventId) {
|
||||
// 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.
|
||||
*
|
||||
* @param el Video element.
|
||||
* @param navCtrl NavController to use.
|
||||
*/
|
||||
protected treatVideoFilters(video: HTMLElement): void {
|
||||
// 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.
|
||||
*
|
||||
* @param siteId Site ID to use.
|
||||
* @param navCtrl NavController.
|
||||
*/
|
||||
protected async openInBrowser(siteId?: string): Promise<void> {
|
||||
const url = <string> this.data?.appurl || this.contextUrl;
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreLocalNotifications } from '@services/local-notifications';
|
||||
|
@ -61,10 +61,7 @@ export class AddonNotificationsSettingsPage implements OnInit, OnDestroy {
|
|||
|
||||
protected updateTimeout?: number;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
// @Optional() protected svComponent: CoreSplitViewComponent,
|
||||
) {
|
||||
constructor() { // @todo @Optional() protected svComponent: CoreSplitViewComponent,
|
||||
this.notifPrefsEnabled = AddonNotifications.instance.isNotificationPreferencesEnabled();
|
||||
this.canChangeSound = CoreLocalNotifications.instance.canDisableSound();
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
import { Md5 } from 'ts-md5/dist/md5';
|
||||
|
||||
import { CoreApp } from '@services/app';
|
||||
|
@ -32,6 +31,7 @@ import {
|
|||
} from '@/addons/privatefiles/services/privatefiles';
|
||||
import { AddonPrivateFilesHelper } from '@/addons/privatefiles/services/privatefiles-helper';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
/**
|
||||
* Page that displays the list of files.
|
||||
|
@ -58,10 +58,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
protected updateSiteObserver: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
constructor() {
|
||||
// Update visibility if current site info is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.setVisibility();
|
||||
|
@ -72,17 +69,18 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
* Component being initialized.
|
||||
*/
|
||||
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.
|
||||
this.path = {
|
||||
contextid: this.route.snapshot.queryParams['contextid'],
|
||||
component: this.route.snapshot.queryParams['component'],
|
||||
filearea: this.route.snapshot.queryParams['filearea'],
|
||||
itemid: this.route.snapshot.queryParams['itemid'],
|
||||
filepath: this.route.snapshot.queryParams['filepath'],
|
||||
filename: this.route.snapshot.queryParams['filename'],
|
||||
contextid: contextId,
|
||||
component: CoreNavigator.instance.getRouteParam<string>('component')!,
|
||||
filearea: CoreNavigator.instance.getRouteParam<string>('filearea')!,
|
||||
itemid: CoreNavigator.instance.getRouteNumberParam('itemid')!,
|
||||
filepath: CoreNavigator.instance.getRouteParam<string>('filepath')!,
|
||||
filename: CoreNavigator.instance.getRouteParam<string>('filename')!,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -254,10 +252,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy {
|
|||
|
||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(params));
|
||||
|
||||
this.navCtrl.navigateForward([`../${hash}`], {
|
||||
relativeTo: this.route,
|
||||
queryParams: params,
|
||||
});
|
||||
CoreNavigator.instance.navigate(`../${hash}`, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { AppComponent } from '@/app/app.component';
|
||||
import { CoreApp } from '@services/app';
|
||||
|
@ -22,11 +21,12 @@ import { CoreLangProvider } from '@services/lang';
|
|||
import { Network, Platform, NgZone } from '@singletons';
|
||||
|
||||
import { mock, mockSingleton, renderComponent, RenderConfig } from '@/testing/utils';
|
||||
import { CoreNavigator, CoreNavigatorService } from '@services/navigator';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
|
||||
let langProvider: CoreLangProvider;
|
||||
let navController: NavController;
|
||||
let navigator: CoreNavigatorService;
|
||||
let config: Partial<RenderConfig>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -35,12 +35,11 @@ describe('AppComponent', () => {
|
|||
mockSingleton(Platform, { ready: () => Promise.resolve() });
|
||||
mockSingleton(NgZone, { run: jest.fn() });
|
||||
|
||||
navigator = mockSingleton(CoreNavigator, ['navigate']);
|
||||
langProvider = mock<CoreLangProvider>(['clearCustomStrings']);
|
||||
navController = mock<NavController>(['navigateRoot']);
|
||||
config = {
|
||||
providers: [
|
||||
{ provide: CoreLangProvider, useValue: langProvider },
|
||||
{ provide: NavController, useValue: navController },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
@ -59,7 +58,7 @@ describe('AppComponent', () => {
|
|||
CoreEvents.trigger(CoreEvents.LOGOUT);
|
||||
|
||||
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');
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreLangProvider } from '@services/lang';
|
||||
import { CoreLoginHelperProvider } from '@features/login/services/login-helper';
|
||||
|
@ -27,6 +26,7 @@ import {
|
|||
import { Network, NgZone, Platform } from '@singletons';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -37,7 +37,6 @@ export class AppComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
protected langProvider: CoreLangProvider,
|
||||
protected navCtrl: NavController,
|
||||
protected loginHelper: CoreLoginHelperProvider,
|
||||
) {
|
||||
}
|
||||
|
@ -56,7 +55,7 @@ export class AppComponent implements OnInit {
|
|||
ngOnInit(): void {
|
||||
CoreEvents.on(CoreEvents.LOGOUT, () => {
|
||||
// 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.
|
||||
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,
|
||||
CoreWSExternalWarning,
|
||||
CoreWSUploadFileResult,
|
||||
CoreWSPreSetsSplitRequest,
|
||||
} from '@services/ws';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
|
@ -516,6 +517,7 @@ export class CoreSite {
|
|||
cleanUnicode: this.cleanUnicode,
|
||||
typeExpected: preSets.typeExpected,
|
||||
responseExpected: preSets.responseExpected,
|
||||
splitRequest: preSets.splitRequest,
|
||||
};
|
||||
|
||||
if (wsPreSets.cleanUnicode && CoreTextUtils.instance.hasUnicodeData(data)) {
|
||||
|
@ -2052,6 +2054,12 @@ export type CoreSiteWSPreSets = {
|
|||
* Component id. Optionally included when 'component' is set.
|
||||
*/
|
||||
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,
|
||||
SimpleChange,
|
||||
ChangeDetectorRef,
|
||||
Optional,
|
||||
ElementRef,
|
||||
KeyValueDiffer,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
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
|
||||
* 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,
|
||||
* 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(
|
||||
protected factoryResolver: ComponentFactoryResolver,
|
||||
differs: KeyValueDiffers,
|
||||
@Optional() protected navCtrl: NavController,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
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.instance['ChangeDetectorRef'] = this.cdr;
|
||||
this.instance['NavController'] = this.navCtrl;
|
||||
this.instance['componentContainer'] = this.element.nativeElement;
|
||||
} else {
|
||||
try {
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
Component, Input, Output, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange,
|
||||
} from '@angular/core';
|
||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreFile } from '@services/file';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
@ -47,7 +46,6 @@ export class CoreIframeComponent implements OnChanges {
|
|||
|
||||
constructor(
|
||||
protected sanitizer: DomSanitizer,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
|
||||
this.logger = CoreLogger.getInstance('CoreIframe');
|
||||
|
@ -77,7 +75,8 @@ export class CoreIframeComponent implements OnChanges {
|
|||
this.loading = !this.src || !CoreUrlUtils.instance.isLocalFileUrl(this.src);
|
||||
|
||||
// @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', () => {
|
||||
this.loading = false;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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-row *ngIf="hideUntil">
|
||||
<ion-col class="col-with-arrow ion-no-padding" (click)="slidePrev()" size="1">
|
||||
|
|
|
@ -24,18 +24,18 @@ import {
|
|||
ViewChild,
|
||||
ElementRef,
|
||||
} 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 { Subscription } from 'rxjs';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreConfig } from '@services/config';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
|
||||
import { Params } from '@angular/router';
|
||||
import { CoreNavBarButtonsComponent } from '../navbar-buttons/navbar-buttons';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
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.
|
||||
|
@ -106,7 +106,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
protected unregisterBackButtonAction: any;
|
||||
protected languageChangedSubscription: Subscription;
|
||||
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 stackEventsSubscription?: Subscription;
|
||||
|
||||
|
@ -114,7 +114,6 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
protected element: ElementRef,
|
||||
platform: Platform,
|
||||
translate: TranslateService,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
this.direction = platform.isRTL ? 'rtl' : 'ltr';
|
||||
|
||||
|
@ -338,7 +337,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
return;
|
||||
}
|
||||
|
||||
this.firstSelectedTab = selectedTab.id;
|
||||
this.firstSelectedTab = selectedTab.id!;
|
||||
this.selectTab(this.firstSelectedTab);
|
||||
|
||||
// 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.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
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 (this.selected) {
|
||||
// Invalid index do not change tab.
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -568,12 +580,11 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
index = 0;
|
||||
}
|
||||
|
||||
const selectedTab = this.tabs[index];
|
||||
if (tabId == this.selected || !selectedTab || !selectedTab.enabled) {
|
||||
const tabToSelect = this.tabs[index];
|
||||
if (!tabToSelect || !tabToSelect.enabled || tabToSelect.id == this.selected) {
|
||||
// Already selected or not enabled.
|
||||
|
||||
e && e.preventDefault();
|
||||
e && e.stopPropagation();
|
||||
e?.preventDefault();
|
||||
e?.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -582,18 +593,16 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
await this.slides!.slideTo(index);
|
||||
}
|
||||
|
||||
const pageParams: NavigationOptions = {};
|
||||
if (selectedTab.pageParams) {
|
||||
pageParams.queryParams = selectedTab.pageParams;
|
||||
}
|
||||
const ok = await this.navCtrl.navigateForward(selectedTab.page, pageParams);
|
||||
const ok = await CoreNavigator.instance.navigate(tabToSelect.page, {
|
||||
params: tabToSelect.pageParams,
|
||||
});
|
||||
|
||||
if (ok !== false) {
|
||||
this.selectHistory.push(tabId);
|
||||
this.selected = tabId;
|
||||
this.selectHistory.push(tabToSelect.id!);
|
||||
this.selected = tabToSelect.id;
|
||||
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.
|
||||
*/
|
||||
class CoreTab {
|
||||
|
||||
id = ''; // Unique tab id.
|
||||
class = ''; // Class, if needed.
|
||||
title = ''; // The translatable tab title.
|
||||
export type CoreTab = {
|
||||
page: string; // Page to navigate to.
|
||||
title: string; // The translatable tab title.
|
||||
id?: string; // Unique tab id.
|
||||
class?: string; // Class, if needed.
|
||||
icon?: string; // The tab icon.
|
||||
badge?: string; // A badge to add in the tab.
|
||||
badgeStyle?: string; // The badge color.
|
||||
enabled = true; // Whether the tab is enabled.
|
||||
page = ''; // Page to navigate to.
|
||||
enabled?: boolean; // Whether the tab is enabled.
|
||||
pageParams?: Params; // Page params.
|
||||
|
||||
}
|
||||
};
|
||||
|
|
|
@ -13,15 +13,13 @@
|
|||
// limitations under the License.
|
||||
|
||||
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 { CoreSites } from '@services/sites';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreObject } from '@singletons/object';
|
||||
import { CoreUserProvider, CoreUserBasicData, CoreUserProfilePictureUpdatedData } from '@features/user/services/user';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
/**
|
||||
* Component to display a "user avatar".
|
||||
|
@ -53,10 +51,7 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
|||
protected currentUserId: number;
|
||||
protected pictureObserver: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
constructor() {
|
||||
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
|
||||
|
||||
this.pictureObserver = CoreEvents.on<CoreUserProfilePictureUpdatedData>(
|
||||
|
@ -143,12 +138,11 @@ export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
|
|||
event.stopPropagation();
|
||||
|
||||
// @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'], {
|
||||
relativeTo: this.route,
|
||||
queryParams: CoreObject.withoutEmpty({
|
||||
CoreNavigator.instance.navigateToSitePath('user', {
|
||||
params: {
|
||||
userId: this.userId,
|
||||
courseId: this.courseId,
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
Optional,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { NavController, IonContent } from '@ionic/angular';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
|
||||
import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
@ -84,7 +84,6 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
|
||||
constructor(
|
||||
element: ElementRef,
|
||||
@Optional() protected navCtrl: NavController,
|
||||
@Optional() protected content: IonContent,
|
||||
protected viewContainerRef: ViewContainerRef,
|
||||
) {
|
||||
|
@ -471,7 +470,8 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
*/
|
||||
protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
|
||||
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 anchors = Array.from(div.querySelectorAll('a'));
|
||||
|
@ -521,7 +521,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
});
|
||||
|
||||
iframes.forEach((iframe) => {
|
||||
this.treatIframe(iframe, site, canTreatVimeo, navCtrl);
|
||||
this.treatIframe(iframe, site, canTreatVimeo);
|
||||
});
|
||||
|
||||
svgImages.forEach((image) => {
|
||||
|
@ -554,7 +554,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
|
||||
// Handle all kind of frames.
|
||||
frames.forEach((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => {
|
||||
CoreIframeUtils.instance.treatFrame(frame, false, navCtrl);
|
||||
CoreIframeUtils.instance.treatFrame(frame, false);
|
||||
});
|
||||
|
||||
CoreDomUtils.instance.handleBootstrapTooltips(div);
|
||||
|
@ -671,13 +671,11 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
* @param iframe Iframe to treat.
|
||||
* @param site Site instance.
|
||||
* @param canTreatVimeo Whether Vimeo videos can be treated in the site.
|
||||
* @param navCtrl NavController to use.
|
||||
*/
|
||||
protected async treatIframe(
|
||||
iframe: HTMLIFrameElement,
|
||||
site: CoreSite | undefined,
|
||||
canTreatVimeo: boolean,
|
||||
navCtrl: NavController,
|
||||
): Promise<void> {
|
||||
const src = iframe.src;
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
|
@ -689,8 +687,7 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
const finalUrl = await currentSite.getAutoLoginUrl(src, false);
|
||||
|
||||
iframe.src = finalUrl;
|
||||
|
||||
CoreIframeUtils.instance.treatFrame(iframe, false, navCtrl);
|
||||
CoreIframeUtils.instance.treatFrame(iframe, false);
|
||||
|
||||
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.
|
||||
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
import { IonContent, NavController } from '@ionic/angular';
|
||||
import { IonContent } from '@ionic/angular';
|
||||
import { NgZone } from '@angular/core';
|
||||
import Faker from 'faker';
|
||||
|
||||
|
@ -44,7 +44,6 @@ describe('CoreFormatTextDirective', () => {
|
|||
|
||||
config = {
|
||||
providers: [
|
||||
{ provide: NavController, useValue: null },
|
||||
{ provide: IonContent, useValue: null },
|
||||
],
|
||||
};
|
||||
|
|
|
@ -13,8 +13,6 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Directive, Input, OnInit, ElementRef } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
import { CoreObject } from '@singletons/object';
|
||||
|
@ -34,8 +32,6 @@ export class CoreUserLinkDirective implements OnInit {
|
|||
|
||||
constructor(
|
||||
element: ElementRef,
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
this.element = element.nativeElement;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
<ion-list>
|
||||
<!-- Course expand="block"s. -->
|
||||
<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>
|
||||
</ion-list>
|
||||
</core-loading>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
max-width: var(--side-blocks-max-width);
|
||||
min-width: var(--side-blocks-min-width);
|
||||
box-shadow: -4px 0px 16px rgba(0, 0, 0, 0.18);
|
||||
z-index: 2;
|
||||
// @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 { CoreBlockHelper } from '../../services/block-helper';
|
||||
import { CoreBlockComponent } from '../block/block';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
* 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 { Params } from '@angular/router';
|
||||
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.
|
||||
|
@ -55,16 +56,15 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa
|
|||
getActions(siteIds: string[], url: string, params: Params): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
|
||||
return [{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
action: (siteId): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const stateParams = {
|
||||
CoreNavigator.instance.navigateToSitePath('course/list-mod-type', {
|
||||
params: {
|
||||
courseId: params.id,
|
||||
modName: this.modName,
|
||||
title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'),
|
||||
};
|
||||
|
||||
// @todo CoreNavigator.instance.goInSite('CoreCourseListModTypePage', stateParams, siteId);
|
||||
},
|
||||
siteId,
|
||||
});
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -13,13 +13,11 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreContentLinksAction } from '../../services/contentlinks-delegate';
|
||||
import { CoreContentLinksHelper } from '../../services/contentlinks-helper';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
|
||||
|
@ -34,27 +32,22 @@ import { CoreNavigator } from '@services/navigator';
|
|||
})
|
||||
export class CoreContentLinksChooseSitePage implements OnInit {
|
||||
|
||||
url: string;
|
||||
url!: string;
|
||||
sites: CoreSiteBasicInfo[] = [];
|
||||
loaded = false;
|
||||
protected action?: CoreContentLinksAction;
|
||||
protected isRootURL = false;
|
||||
|
||||
constructor(
|
||||
route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
this.url = route.snapshot.queryParamMap.get('url')!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.url) {
|
||||
const url = CoreNavigator.instance.getRouteParam<string>('url');
|
||||
if (!url) {
|
||||
return this.leaveView();
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
let siteIds: string[] | undefined = [];
|
||||
|
||||
try {
|
||||
|
@ -115,7 +108,7 @@ export class CoreContentLinksChooseSitePage implements OnInit {
|
|||
try {
|
||||
await CoreSites.instance.logout();
|
||||
} 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 { makeSingleton, Translate } from '@singletons';
|
||||
import { CoreNavigator } from '@services/navigator';
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding content links.
|
||||
|
@ -27,10 +28,6 @@ import { CoreNavigator } from '@services/navigator';
|
|||
@Injectable({ providedIn: 'root' })
|
||||
export class CoreContentLinksHelperProvider {
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Check whether a link can be handled by the app.
|
||||
*
|
||||
|
@ -93,8 +90,7 @@ export class CoreContentLinksHelperProvider {
|
|||
* @return Promise resolved when done.
|
||||
* @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: any, siteId?: string): Promise<void> {
|
||||
async goInSite(navCtrl: NavController, pageName: string, pageParams: Params, siteId?: string): Promise<void> {
|
||||
await CoreNavigator.instance.navigateToSitePath(pageName, { params: pageParams, siteId });
|
||||
}
|
||||
|
||||
|
@ -105,7 +101,7 @@ export class CoreContentLinksHelperProvider {
|
|||
* @todo set correct root.
|
||||
*/
|
||||
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
|
||||
// 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 { 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 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({
|
||||
imports: [
|
||||
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||
CoreCourseFormatModule,
|
||||
CoreCourseComponentsModule,
|
||||
CoreCourseDirectivesModule,
|
||||
],
|
||||
exports: [CoreCourseIndexRoutingModule],
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA],
|
||||
useValue: [SITE_SCHEMA, OFFLINE_SITE_SCHEMA, LOG_SITE_SCHEMA, PREFETCH_SITE_SCHEMA],
|
||||
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.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// @todo test delegate
|
||||
|
||||
import { Injectable, Type } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreDelegate, CoreDelegateHandler, CoreDelegateToDisplay } from '@classes/delegate';
|
||||
import { CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
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 { Params } from '@angular/router';
|
||||
import { makeSingleton } from '@singletons';
|
||||
|
@ -47,8 +52,9 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
|||
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
|
||||
* @return True or promise resolved with true if enabled.
|
||||
*/
|
||||
isEnabledForCourse(courseId: number,
|
||||
accessData: CoreCourseAccessData,
|
||||
isEnabledForCourse(
|
||||
courseId: number,
|
||||
accessData: CoreCourseAccess,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): boolean | Promise<boolean>;
|
||||
|
@ -56,11 +62,11 @@ export interface CoreCourseOptionsHandler extends CoreDelegateHandler {
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
getDisplayData?(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseDataWithOptions,
|
||||
): CoreCourseOptionsHandlerData | Promise<CoreCourseOptionsHandlerData>;
|
||||
|
||||
/**
|
||||
|
@ -97,7 +103,7 @@ export interface CoreCourseOptionsMenuHandler extends CoreCourseOptionsHandler {
|
|||
* @return Data or promise resolved with data.
|
||||
*/
|
||||
getMenuDisplayData(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseDataWithOptions,
|
||||
): CoreCourseOptionsMenuHandlerData | Promise<CoreCourseOptionsMenuHandlerData>;
|
||||
}
|
||||
|
||||
|
@ -116,15 +122,14 @@ export interface CoreCourseOptionsHandlerData {
|
|||
class?: string;
|
||||
|
||||
/**
|
||||
* The component to render the handler. It must be the component class, not the name or an instance.
|
||||
* When the component is created, it will receive the courseId as input.
|
||||
* Path of the page to load for the handler.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Name of the page to load for the handler.
|
||||
* Path of the page to load for the handler.
|
||||
*/
|
||||
page: string;
|
||||
|
||||
|
@ -160,29 +165,19 @@ export interface CoreCourseOptionsMenuHandlerData {
|
|||
/**
|
||||
* Data returned by the delegate for each handler.
|
||||
*/
|
||||
export interface CoreCourseOptionsHandlerToDisplay {
|
||||
export interface CoreCourseOptionsHandlerToDisplay extends CoreDelegateToDisplay {
|
||||
/**
|
||||
* Data to display.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param course The course.
|
||||
* @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.
|
||||
* @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: {
|
||||
[courseId: number]: {
|
||||
access: any;
|
||||
access: CoreCourseAccess;
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
deferred: PromiseDefer<void>;
|
||||
|
@ -320,7 +315,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
protected async getHandlersForAccess(
|
||||
courseId: number,
|
||||
refresh: boolean,
|
||||
accessData: any,
|
||||
accessData: CoreCourseAccess,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): Promise<CoreCourseOptionsHandler[]> {
|
||||
|
@ -367,7 +362,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @return Promise resolved with array of handlers.
|
||||
*/
|
||||
getHandlersToDisplay(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
|
@ -389,7 +384,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @return Promise resolved with array of handlers.
|
||||
*/
|
||||
getMenuHandlersToDisplay(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
|
@ -413,28 +408,31 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
*/
|
||||
protected async getHandlersToDisplayInternal(
|
||||
menu: boolean,
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
course: CoreCourseAnyCourseData,
|
||||
refresh = false,
|
||||
isGuest = false,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> {
|
||||
|
||||
const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course;
|
||||
const accessData = {
|
||||
type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT,
|
||||
};
|
||||
const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = [];
|
||||
|
||||
if (navOptions) {
|
||||
course.navOptions = navOptions;
|
||||
courseWithOptions.navOptions = navOptions;
|
||||
}
|
||||
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.
|
||||
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>[] = [];
|
||||
|
||||
let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[];
|
||||
|
@ -449,7 +447,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
? (handler as CoreCourseOptionsMenuHandler).getMenuDisplayData
|
||||
: (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({
|
||||
data: data,
|
||||
priority: handler.priority,
|
||||
|
@ -586,7 +584,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
* @param refresh True if it should refresh the list.
|
||||
* @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() &&
|
||||
(typeof course.navOptions == 'undefined' || typeof course.admOptions == 'undefined' || refresh)) {
|
||||
|
||||
|
@ -618,7 +616,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
*/
|
||||
async updateHandlersForCourse(
|
||||
courseId: number,
|
||||
accessData: any,
|
||||
accessData: CoreCourseAccess,
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed,
|
||||
): Promise<void> {
|
||||
|
@ -673,5 +671,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt
|
|||
|
||||
export class CoreCourseOptionsDelegate extends makeSingleton(CoreCourseOptionsDelegateService) {}
|
||||
|
||||
// @todo define
|
||||
export type CoreCourseAccessData = any;
|
||||
export type CoreCourseAccess = {
|
||||
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 { CoreSiteWSPreSets, CoreSite } from '@classes/site';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { makeSingleton, Translate } from '@singletons';
|
||||
import { makeSingleton, Platform, Translate } from '@singletons';
|
||||
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile } from '@services/ws';
|
||||
|
||||
import { CoreCourseStatusDBRecord, COURSE_STATUS_TABLE } from './database/course';
|
||||
import { CoreCourseOffline } from './course-offline';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import {
|
||||
CoreCourses,
|
||||
CoreCourseAnyCourseData,
|
||||
CoreCoursesMyCoursesUpdatedEventData,
|
||||
CoreCoursesProvider,
|
||||
} from '../../courses/services/courses';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreWSError } from '@classes/errors/wserror';
|
||||
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:';
|
||||
|
||||
|
@ -71,12 +76,30 @@ export class CoreCourseProvider {
|
|||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
// @todo
|
||||
// protected courseFormatDelegate: CoreCourseFormatDelegate,
|
||||
// protected sitePluginsProvider: CoreSitePluginsProvider,
|
||||
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.
|
||||
*
|
||||
|
@ -109,9 +132,8 @@ export class CoreCourseProvider {
|
|||
*
|
||||
* @param courseId Course ID.
|
||||
* @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) {
|
||||
this.invalidateSections(courseId).finally(() => {
|
||||
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.
|
||||
* @return Whether the current view is a certain course.
|
||||
*/
|
||||
|
@ -346,7 +367,7 @@ export class CoreCourseProvider {
|
|||
ignoreCache: boolean = false,
|
||||
siteId?: string,
|
||||
modName?: string,
|
||||
): Promise<CoreCourseModuleData> {
|
||||
): Promise<CoreCourseWSModule> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
// Helper function to do the WS request without processing the result.
|
||||
|
@ -356,7 +377,7 @@ export class CoreCourseProvider {
|
|||
modName: string | undefined,
|
||||
includeStealth: boolean,
|
||||
preferCache: boolean,
|
||||
): Promise<CoreCourseSection[]> => {
|
||||
): Promise<CoreCourseWSSection[]> => {
|
||||
const params: CoreCourseGetContentsParams = {
|
||||
courseid: courseId!,
|
||||
options: [],
|
||||
|
@ -394,7 +415,7 @@ export class CoreCourseProvider {
|
|||
}
|
||||
|
||||
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;
|
||||
} catch {
|
||||
|
@ -419,7 +440,7 @@ export class CoreCourseProvider {
|
|||
courseId = module.course;
|
||||
}
|
||||
|
||||
let sections: CoreCourseSection[];
|
||||
let sections: CoreCourseWSSection[];
|
||||
try {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
// 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);
|
||||
}
|
||||
|
||||
let foundModule: CoreCourseModuleData | undefined;
|
||||
let foundModule: CoreCourseWSModule | undefined;
|
||||
|
||||
const foundSection = sections.some((section) => {
|
||||
if (sectionId != null &&
|
||||
|
@ -637,7 +658,7 @@ export class CoreCourseProvider {
|
|||
excludeModules?: boolean,
|
||||
excludeContents?: boolean,
|
||||
siteId?: string,
|
||||
): Promise<CoreCourseSection> {
|
||||
): Promise<CoreCourseWSSection> {
|
||||
|
||||
if (sectionId < 0) {
|
||||
throw new CoreError('Invalid section ID');
|
||||
|
@ -671,7 +692,7 @@ export class CoreCourseProvider {
|
|||
preSets?: CoreSiteWSPreSets,
|
||||
siteId?: string,
|
||||
includeStealthModules: boolean = true,
|
||||
): Promise<CoreCourseSection[]> {
|
||||
): Promise<CoreCourseWSSection[]> {
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
preSets = preSets || {};
|
||||
|
@ -698,7 +719,7 @@ export class CoreCourseProvider {
|
|||
});
|
||||
}
|
||||
|
||||
let sections: CoreCourseSection[];
|
||||
let sections: CoreCourseWSSection[];
|
||||
try {
|
||||
sections = await site.read('core_course_get_contents', params, preSets);
|
||||
} catch {
|
||||
|
@ -740,12 +761,12 @@ export class CoreCourseProvider {
|
|||
* @param sections Sections.
|
||||
* @return Modules.
|
||||
*/
|
||||
getSectionsModules(sections: CoreCourseSection[]): CoreCourseModuleData[] {
|
||||
getSectionsModules(sections: CoreCourseWSSection[]): CoreCourseWSModule[] {
|
||||
if (!sections || !sections.length) {
|
||||
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.
|
||||
*/
|
||||
async loadModuleContents(
|
||||
module: CoreCourseModuleData & CoreCourseModuleBasicInfo,
|
||||
module: CoreCourseWSModule,
|
||||
courseId?: number,
|
||||
sectionId?: number,
|
||||
preferCache?: boolean,
|
||||
|
@ -856,7 +877,6 @@ export class CoreCourseProvider {
|
|||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param name Name of the course.
|
||||
* @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> {
|
||||
const params: CoreCourseViewCourseWSParams = {
|
||||
|
@ -875,7 +895,7 @@ export class CoreCourseProvider {
|
|||
if (!response.status) {
|
||||
throw Error('WS core_course_view_course failed.');
|
||||
} else {
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||
CoreEvents.trigger<CoreCoursesMyCoursesUpdatedEventData>(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||
courseId: courseId,
|
||||
action: CoreCoursesProvider.ACTION_VIEW,
|
||||
}, site.getId());
|
||||
|
@ -954,7 +974,20 @@ export class CoreCourseProvider {
|
|||
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.
|
||||
* @return Whether the module has a view page.
|
||||
*/
|
||||
moduleHasView(module: CoreCourseModuleSummary | CoreCourseModuleData): boolean {
|
||||
moduleHasView(module: CoreCourseModuleSummary | CoreCourseWSModule): boolean {
|
||||
return !!module.url;
|
||||
}
|
||||
|
||||
|
@ -981,63 +1014,42 @@ export class CoreCourseProvider {
|
|||
* @param params Other params to pass to the course page.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async openCourse(
|
||||
course: { id: number ; format?: string },
|
||||
params?: Params, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
): Promise<void> {
|
||||
// @todo const loading = await CoreDomUtils.instance.showModalLoading();
|
||||
async openCourse(course: CoreCourseAnyCourseData | { id: number }, params?: Params): Promise<void> {
|
||||
const loading = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
// Wait for site plugins to be fetched.
|
||||
// @todo await this.sitePluginsProvider.waitFetchPlugins();
|
||||
|
||||
if (typeof course.format == 'undefined') {
|
||||
// This block can be replaced by a call to CourseHelper.getCourse(), but it is circular dependant.
|
||||
const coursesProvider = CoreCourses.instance;
|
||||
try {
|
||||
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.
|
||||
}
|
||||
}
|
||||
if (!('format' in course) || typeof course.format == 'undefined') {
|
||||
const result = await CoreCourseHelper.instance.getCourse(course.id);
|
||||
|
||||
course = result.course;
|
||||
}
|
||||
|
||||
/* @todo
|
||||
if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
||||
if (course) { // @todo Replace with: if (!this.sitePluginsProvider.sitePluginPromiseExists('format_' + course.format)) {
|
||||
// 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();
|
||||
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
// This course uses a custom format plugin, wait for the format plugin to finish loading.
|
||||
try {
|
||||
/* @todo await this.sitePluginsProvider.sitePluginLoaded('format_' + course.format);
|
||||
// The format loaded successfully, but the handlers wont be registered until all site plugins have loaded.
|
||||
if (this.sitePluginsProvider.sitePluginsFinishedLoading) {
|
||||
return this.courseFormatDelegate.openCourse(course, params);
|
||||
return CoreCourseFormatDelegate.instance.openCourse(course, params);
|
||||
}*/
|
||||
|
||||
// Wait for plugins to be loaded.
|
||||
const deferred = CoreUtils.instance.promiseDefer<void>();
|
||||
|
||||
const observer = CoreEvents.on(CoreEvents.SITE_PLUGINS_LOADED, () => {
|
||||
observer && observer.off();
|
||||
observer?.off();
|
||||
|
||||
/* @todo this.courseFormatDelegate.openCourse(course, params).then((response) => {
|
||||
deferred.resolve(response);
|
||||
}).catch((error) => {
|
||||
deferred.reject(error);
|
||||
});*/
|
||||
CoreCourseFormatDelegate.instance.openCourse(<CoreCourseAnyCourseData> course, params)
|
||||
.then(deferred.resolve).catch(deferred.reject);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
|
@ -1334,7 +1346,7 @@ export type CoreCourseGetContentsParams = {
|
|||
/**
|
||||
* Data returned by core_course_get_contents WS.
|
||||
*/
|
||||
export type CoreCourseSection = {
|
||||
export type CoreCourseWSSection = {
|
||||
id: number; // Section ID.
|
||||
name: string; // Section name.
|
||||
visible?: number; // Is the section visible.
|
||||
|
@ -1344,7 +1356,7 @@ export type CoreCourseSection = {
|
|||
hiddenbynumsections?: number; // Whether is a section hidden in the course format.
|
||||
uservisible?: boolean; // Is the section visible for the user?.
|
||||
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.
|
||||
course?: number; // The course id.
|
||||
url?: string; // Activity url.
|
||||
|
@ -1395,12 +1407,7 @@ export type CoreCourseModuleData = { // List of module.
|
|||
customdata?: string; // Custom data (JSON encoded).
|
||||
noviewlink?: boolean; // Whether the module has no view page.
|
||||
completion?: number; // Type of completion tracking: 0 means none, 1 manual, 2 automatic.
|
||||
completiondata?: { // 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.
|
||||
};
|
||||
completiondata?: CoreCourseModuleWSCompletionData; // Module completion data.
|
||||
contents: CoreCourseModuleContentFile[];
|
||||
contentsinfo?: { // Contents summary information.
|
||||
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 = {
|
||||
type: string; // A file or a folder or external link.
|
||||
filename: string; // Filename.
|
||||
filepath: string; // Filepath.
|
||||
filesize: number; // Filesize.
|
||||
fileurl?: string; // Downloadable file url.
|
||||
url?: string; // @deprecated. Use fileurl instead.
|
||||
fileurl: string; // Downloadable file url.
|
||||
content?: string; // Raw content, will be used when type is content.
|
||||
timecreated: number; // Time created.
|
||||
timemodified: number; // Time modified.
|
||||
sortorder: number; // Content sort order.
|
||||
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.
|
||||
userid: number; // User who added this content to moodle.
|
||||
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;
|
||||
};
|