commit
e070c7864e
|
@ -390,6 +390,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@angular/animations": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-11.0.1.tgz",
|
||||
"integrity": "sha512-RS2ZsO3yidn/dMAllR+V0EX5BOQLQDi5s2kvd4wANHYAkU/yVXWKl09nbe8LTwLVH+iOYX7AAcAUUokQPEEHxQ==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@angular/cli": {
|
||||
"version": "10.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-10.0.8.tgz",
|
||||
|
@ -6514,7 +6522,7 @@
|
|||
"integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ=="
|
||||
},
|
||||
"cordova-plugin-wkuserscript": {
|
||||
"version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#aa77d0f98a3fb106f2e798e5adf5882f01a2c947",
|
||||
"version": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git#6413f4bb3c2565f353e690b5c1450b69ad9e860e",
|
||||
"from": "git+https://github.com/moodlemobile/cordova-plugin-wkuserscript.git"
|
||||
},
|
||||
"cordova-plugin-wkwebview-cookies": {
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
"ionic:build:before": "npx gulp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^11.0.1",
|
||||
"@angular/common": "~10.0.0",
|
||||
"@angular/core": "~10.0.0",
|
||||
"@angular/forms": "~10.0.0",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<!-- Upload a private file. -->
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end" *ngIf="showUpload && root != 'site' && !path">
|
||||
<ion-fab-button (click)="uploadFile()" [attr.aria-label]="'core.fileuploader.uploadafile' | translate">
|
||||
<ion-icon name="fa-plus"></ion-icon>
|
||||
<ion-icon name="fas-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
|
|
@ -20,6 +20,8 @@ import { CoreWSExternalWarning } from '@services/ws';
|
|||
import { CoreSite } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmaFiles:';
|
||||
|
||||
/**
|
||||
* Service to handle my files and site files.
|
||||
*/
|
||||
|
@ -32,8 +34,6 @@ export class AddonPrivateFilesProvider {
|
|||
static readonly PRIVATE_FILES_COMPONENT = 'mmaFilesMy';
|
||||
static readonly SITE_FILES_COMPONENT = 'mmaFilesSite';
|
||||
|
||||
protected readonly ROOT_CACHE_KEY = 'mmaFiles:';
|
||||
|
||||
/**
|
||||
* Check if core_user_get_private_files_info WS call is available.
|
||||
*
|
||||
|
@ -125,7 +125,7 @@ export class AddonPrivateFilesProvider {
|
|||
protected getFilesListCacheKey(params: AddonPrivateFilesGetFilesWSParams): string {
|
||||
const root = !params.component ? 'site' : 'my';
|
||||
|
||||
return this.ROOT_CACHE_KEY + 'list:' + root + ':' + params.contextid + ':' + params.filepath;
|
||||
return ROOT_CACHE_KEY + 'list:' + root + ':' + params.contextid + ':' + params.filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,7 +194,7 @@ export class AddonPrivateFilesProvider {
|
|||
* @return Cache key.
|
||||
*/
|
||||
protected getPrivateFilesInfoCommonCacheKey(): string {
|
||||
return this.ROOT_CACHE_KEY + 'privateInfo';
|
||||
return ROOT_CACHE_KEY + 'privateInfo';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -474,7 +474,7 @@ export type AddonPrivateFilesGetFilesWSResult = {
|
|||
/**
|
||||
* Params of core_user_get_private_files_info WS.
|
||||
*/
|
||||
export type AddonPrivateFilesGetUserInfoWSParams = {
|
||||
type AddonPrivateFilesGetUserInfoWSParams = {
|
||||
userid?: number; // Id of the user, default to current user.
|
||||
};
|
||||
|
||||
|
@ -492,6 +492,6 @@ export type AddonPrivateFilesGetUserInfoWSResult = {
|
|||
/**
|
||||
* Params of core_user_add_user_private_files WS.
|
||||
*/
|
||||
export type AddonPrivateFilesAddUserPrivateFilesWSParams = {
|
||||
type AddonPrivateFilesAddUserPrivateFilesWSParams = {
|
||||
draftid: number; // Draft area id.
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouteReuseStrategy } from '@angular/router';
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
|
||||
|
@ -38,6 +39,7 @@ export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
|||
entryComponents: [],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
IonicModule.forRoot(),
|
||||
HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content.
|
||||
TranslateModule.forRoot({
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
// (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 { trigger, style, transition, animate, keyframes } from '@angular/animations';
|
||||
|
||||
export const coreShowHideAnimation = trigger('coreShowHideAnimation', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('500ms ease-in-out', style({ opacity: 1 })),
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({ opacity: 1 }),
|
||||
animate('500ms ease-in-out', style({ opacity: 0 })),
|
||||
]),
|
||||
]);
|
||||
|
||||
export const coreSlideInOut = trigger('coreSlideInOut', [
|
||||
// Enter animation.
|
||||
transition('void => fromLeft', [
|
||||
style({ transform: 'translateX(0)', opacity: 1 }),
|
||||
animate(300, keyframes([
|
||||
style({ opacity: 0, transform: 'translateX(-100%)', offset: 0 }),
|
||||
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.7 }),
|
||||
style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }),
|
||||
])),
|
||||
]),
|
||||
// Leave animation.
|
||||
transition('fromLeft => void', [
|
||||
style({ transform: 'translateX(-100%)', opacity: 0 }),
|
||||
animate(300, keyframes([
|
||||
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
||||
style({ opacity: 1, transform: 'translateX(5%)', offset: 0.3 }),
|
||||
style({ opacity: 0, transform: 'translateX(-100%)', offset: 1.0 }),
|
||||
])),
|
||||
]),
|
||||
// Enter animation.
|
||||
transition('void => fromRight', [
|
||||
style({ transform: 'translateX(0)', opacity: 1 }),
|
||||
animate(300, keyframes([
|
||||
style({ opacity: 0, transform: 'translateX(100%)', offset: 0 }),
|
||||
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.7 }),
|
||||
style({ opacity: 1, transform: 'translateX(0)', offset: 1.0 }),
|
||||
])),
|
||||
]),
|
||||
// Leave animation.
|
||||
transition('fromRight => void', [
|
||||
style({ transform: 'translateX(-100%)', opacity: 0 }),
|
||||
animate(300, keyframes([
|
||||
style({ opacity: 1, transform: 'translateX(0)', offset: 0 }),
|
||||
style({ opacity: 1, transform: 'translateX(-5%)', offset: 0.3 }),
|
||||
style({ opacity: 0, transform: 'translateX(100%)', offset: 1.0 }),
|
||||
])),
|
||||
]),
|
||||
]);
|
|
@ -30,9 +30,15 @@ import { CoreRecaptchaModalComponent } from './recaptcha/recaptchamodal';
|
|||
import { CoreShowPasswordComponent } from './show-password/show-password';
|
||||
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
|
||||
import { CoreTabsComponent } from './tabs/tabs';
|
||||
import { CoreInfiniteLoadingComponent } from './infinite-loading/infinite-loading';
|
||||
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
|
||||
import { CoreContextMenuComponent } from './context-menu/context-menu';
|
||||
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
||||
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -49,6 +55,12 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
|||
CoreShowPasswordComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -71,6 +83,12 @@ import { CorePipesModule } from '@pipes/pipes.module';
|
|||
CoreShowPasswordComponent,
|
||||
CoreEmptyBoxComponent,
|
||||
CoreTabsComponent,
|
||||
CoreInfiniteLoadingComponent,
|
||||
CoreProgressBarComponent,
|
||||
CoreContextMenuComponent,
|
||||
CoreContextMenuItemComponent,
|
||||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
// (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, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
|
||||
import { CoreContextMenuComponent } from './context-menu';
|
||||
|
||||
/**
|
||||
* This directive adds a item to the Context Menu popover.
|
||||
*
|
||||
* @description
|
||||
* This directive defines and item to be added to the popover generated in CoreContextMenu.
|
||||
*
|
||||
* It is required to place this tag inside a core-context-menu tag.
|
||||
*
|
||||
* <core-context-menu>
|
||||
* <core-context-menu-item [hidden]="showGrid" [priority]="601" [content]="'core.layoutgrid' | translate"
|
||||
* (action)="switchGrid()" [iconAction]="'apps'"></core-context-menu-item>
|
||||
* </core-context-menu>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-context-menu-item',
|
||||
template: '',
|
||||
})
|
||||
export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||
|
||||
@Input() content?: string; // Content of the item.
|
||||
@Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
|
||||
@Input() iconAction?: string; // Name of the icon to show on the right side of the item. Represents the action to do on click.
|
||||
// If is "spinner" an spinner will be shown.
|
||||
// If no icon or spinner is selected, no action or link will work.
|
||||
// If href but no iconAction is provided arrow-right will be used.
|
||||
@Input() iconSlash?: boolean; // Display a red slash over the icon.
|
||||
@Input() ariaDescription?: string; // Aria label to add to iconDescription.
|
||||
@Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
|
||||
@Input() href?: string; // Link to go if no action provided.
|
||||
@Input() captureLink?: boolean | string; // Whether the link needs to be captured by the app.
|
||||
@Input() autoLogin?: string; // Whether the link needs to be opened using auto-login.
|
||||
@Input() closeOnClick = true; // Whether to close the popover when the item is clicked.
|
||||
@Input() priority?: number; // Used to sort items. The highest priority, the highest position.
|
||||
@Input() badge?: string; // A badge to show in the item.
|
||||
@Input() badgeClass?: number; // A class to set in the badge.
|
||||
@Input() hidden?: boolean; // Whether the item should be hidden.
|
||||
@Output() action?: EventEmitter<() => void>; // Will emit an event when the item clicked.
|
||||
@Output() onClosed?: EventEmitter<() => void>; // Will emit an event when the popover is closed because the item was clicked.
|
||||
|
||||
protected hasAction = false;
|
||||
protected destroyed = false;
|
||||
|
||||
constructor(
|
||||
protected ctxtMenu: CoreContextMenuComponent,
|
||||
) {
|
||||
this.action = new EventEmitter();
|
||||
this.onClosed = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// Initialize values.
|
||||
this.priority = this.priority || 1;
|
||||
this.hasAction = !!this.action && this.action.observers.length > 0;
|
||||
this.ariaAction = this.ariaAction || this.content;
|
||||
|
||||
if (this.hasAction) {
|
||||
this.href = '';
|
||||
}
|
||||
|
||||
// Navigation help if href provided.
|
||||
this.captureLink = this.href && this.captureLink ? this.captureLink : false;
|
||||
this.autoLogin = this.autoLogin || 'check';
|
||||
|
||||
if (!this.destroyed) {
|
||||
this.ctxtMenu.addItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.destroyed = true;
|
||||
this.ctxtMenu.removeItem(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.hidden && !changes.hidden.firstChange) {
|
||||
this.ctxtMenu.itemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
:host {
|
||||
ion-list {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { NavParams, PopoverController } from '@ionic/angular';
|
||||
import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||
|
||||
/**
|
||||
* Component to display a list of items received by param in a popover.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-context-menu-popover',
|
||||
templateUrl: 'core-context-menu-popover.html',
|
||||
styleUrls: ['context-menu-popover.scss'],
|
||||
})
|
||||
export class CoreContextMenuPopoverComponent {
|
||||
|
||||
title: string;
|
||||
uniqueId: string;
|
||||
items: CoreContextMenuItemComponent[];
|
||||
|
||||
constructor(
|
||||
navParams: NavParams,
|
||||
protected popoverCtrl: PopoverController,
|
||||
) {
|
||||
this.title = navParams.get('title');
|
||||
this.items = navParams.get('items') || [];
|
||||
this.uniqueId = navParams.get('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the popover.
|
||||
*/
|
||||
closeMenu(item?: CoreContextMenuItemComponent): void {
|
||||
this.popoverCtrl.dismiss(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when an item is clicked.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param item Item clicked.
|
||||
* @return Return true if success, false if error.
|
||||
*/
|
||||
itemClicked(event: Event, item: CoreContextMenuItemComponent): boolean {
|
||||
if (!!item.action && item.action.observers.length > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (item.iconAction == 'spinner') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.closeOnClick) {
|
||||
this.closeMenu(item);
|
||||
}
|
||||
|
||||
item.action.emit(this.closeMenu.bind(this, item));
|
||||
} else if (item.closeOnClick && (item.href || (!!item.onClosed && item.onClosed.observers.length > 0))) {
|
||||
this.closeMenu(item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
// (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, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { auditTime } from 'rxjs/operators';
|
||||
import { PopoverController } from '@ionic/angular';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreContextMenuItemComponent } from './context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
|
||||
|
||||
/**
|
||||
* This component adds a button (usually in the navigation bar) that displays a context menu popover.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-context-menu',
|
||||
templateUrl: 'core-context-menu.html',
|
||||
})
|
||||
export class CoreContextMenuComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
|
||||
@Input() title?: string; // Text to be shown on the top of the popover.
|
||||
@Input('aria-label') ariaLabel?: string; // Aria label to be shown on the top of the popover.
|
||||
|
||||
hideMenu = true; // It will be unhidden when items are added.
|
||||
expanded = false;
|
||||
protected items: CoreContextMenuItemComponent[] = [];
|
||||
protected itemsMovedToParent: CoreContextMenuItemComponent[] = [];
|
||||
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
|
||||
protected instanceId: string;
|
||||
protected parentContextMenu?: CoreContextMenuComponent;
|
||||
protected uniqueId: string;
|
||||
|
||||
constructor(
|
||||
protected popoverCtrl: PopoverController,
|
||||
elementRef: ElementRef,
|
||||
) {
|
||||
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
|
||||
this.itemsChangedStream = new Subject<void>();
|
||||
this.itemsChangedStream.pipe(auditTime(250));
|
||||
this.itemsChangedStream.subscribe(() => {
|
||||
// Hide the menu if all items are hidden.
|
||||
this.hideMenu = !this.items.some((item) => !item.hidden);
|
||||
|
||||
// Sort the items by priority.
|
||||
this.items.sort((a, b) => (a.priority || 0) <= (b.priority || 0) ? 1 : -1);
|
||||
});
|
||||
|
||||
// Calculate the unique ID.
|
||||
this.uniqueId = 'core-context-menu-' + CoreUtils.instance.getUniqueId('CoreContextMenuComponent');
|
||||
|
||||
this.instanceId = CoreDomUtils.instance.storeInstanceByElement(elementRef.nativeElement, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.icon = this.icon || 'ellipsis-vertical';
|
||||
this.ariaLabel = this.ariaLabel || this.title || Translate.instance.instant('core.displayoptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a context menu item.
|
||||
*
|
||||
* @param item The item to add.
|
||||
*/
|
||||
addItem(item: CoreContextMenuItemComponent): void {
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu. Add the item in there.
|
||||
this.parentContextMenu.addItem(item);
|
||||
|
||||
if (this.itemsMovedToParent.indexOf(item) == -1) {
|
||||
this.itemsMovedToParent.push(item);
|
||||
}
|
||||
} else if (this.items.indexOf(item) == -1) {
|
||||
this.items.push(item);
|
||||
this.itemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function called when the items change.
|
||||
*/
|
||||
itemsChanged(): void {
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu, call the function in there.
|
||||
this.parentContextMenu.itemsChanged();
|
||||
} else {
|
||||
this.itemsChangedStream.next();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the current context menu with the one passed as parameter. All the items in this menu will be moved to the
|
||||
* one passed as parameter.
|
||||
*
|
||||
* @param contextMenu The context menu where to move the items.
|
||||
*/
|
||||
mergeContextMenus(contextMenu: CoreContextMenuComponent): void {
|
||||
this.parentContextMenu = contextMenu;
|
||||
|
||||
// Add all the items to the other menu.
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
const item = this.items[i];
|
||||
contextMenu.addItem(item);
|
||||
this.itemsMovedToParent.push(item);
|
||||
}
|
||||
|
||||
// Remove all items from the current menu.
|
||||
this.items = [];
|
||||
this.itemsChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the context menu.
|
||||
*
|
||||
* @param item The item to remove.
|
||||
*/
|
||||
removeItem(item: CoreContextMenuItemComponent): void {
|
||||
if (this.parentContextMenu) {
|
||||
// All items were moved to the "parent" menu. Remove the item from there.
|
||||
this.parentContextMenu.removeItem(item);
|
||||
|
||||
const index = this.itemsMovedToParent.indexOf(item);
|
||||
if (index >= 0) {
|
||||
this.itemsMovedToParent.splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
const index = this.items.indexOf(item);
|
||||
if (index >= 0) {
|
||||
this.items.splice(index, 1);
|
||||
}
|
||||
this.itemsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the items that were merged to a parent context menu.
|
||||
*/
|
||||
removeMergedItems(): void {
|
||||
if (this.parentContextMenu) {
|
||||
for (let i = 0; i < this.itemsMovedToParent.length; i++) {
|
||||
this.parentContextMenu.removeItem(this.itemsMovedToParent[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the items that were merged to a parent context menu.
|
||||
*/
|
||||
restoreMergedItems(): void {
|
||||
if (this.parentContextMenu) {
|
||||
for (let i = 0; i < this.itemsMovedToParent.length; i++) {
|
||||
this.parentContextMenu.addItem(this.itemsMovedToParent[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu.
|
||||
*
|
||||
* @param event Event.
|
||||
*/
|
||||
async showContextMenu(event: MouseEvent): Promise<void> {
|
||||
if (!this.expanded) {
|
||||
const popover = await this.popoverCtrl.create(
|
||||
{
|
||||
event,
|
||||
component: CoreContextMenuPopoverComponent,
|
||||
componentProps: {
|
||||
title: this.title,
|
||||
items: this.items,
|
||||
},
|
||||
showBackdrop: true,
|
||||
id: this.uniqueId,
|
||||
},
|
||||
);
|
||||
await popover.present();
|
||||
this.expanded = true;
|
||||
|
||||
const data = await popover.onDidDismiss<CoreContextMenuItemComponent>();
|
||||
this.expanded = false;
|
||||
|
||||
if (data.data) {
|
||||
data.data.onClosed?.emit();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
CoreDomUtils.instance.removeInstanceById(this.instanceId);
|
||||
this.removeMergedItems();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<ion-list [id]="uniqueId" role="menu">
|
||||
<ion-list-header *ngIf="title">{{title}}</ion-list-header>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin"
|
||||
[href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden"
|
||||
[detail]="(item.href && !item.iconAction) || null" role="menuitem">
|
||||
<ion-icon *ngIf="item.iconDescription" [name]="item.iconDescription" [attr.aria-label]="item.ariaDescription" slot="start">
|
||||
</ion-icon>
|
||||
<core-format-text [clean]="true" [text]="item.content" [filter]="false"></core-format-text>
|
||||
<ion-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction"
|
||||
[class.icon-slash]="item.iconSlash" slot="end">
|
||||
</ion-icon>
|
||||
<ion-spinner *ngIf="(item.href || item.action) && item.iconAction == 'spinner'" slot="end"></ion-spinner>
|
||||
<ion-badge class="{{item.badgeClass}}" slot="end" *ngIf="item.badge">{{item.badge}}</ion-badge>
|
||||
</ion-item>
|
||||
</ion-list>
|
|
@ -0,0 +1,6 @@
|
|||
<ion-button [hidden]="hideMenu" fill="clear" [attr.aria-label]="ariaLabel"
|
||||
(click)="showContextMenu($event)" aria-haspopup="true" [attr.aria-expanded]="expanded" [attr.aria-controls]="uniqueId">
|
||||
<ion-icon [name]="icon" slot="icon-only">
|
||||
</ion-icon>
|
||||
</ion-button>
|
||||
<ng-content></ng-content>
|
|
@ -1,20 +1,25 @@
|
|||
<ng-container *ngIf="enabled && !(loading || status === statusDownloading)">
|
||||
<ng-container *ngIf="enabled && !loading">
|
||||
<!-- Download button. -->
|
||||
<ion-button *ngIf="status == statusNotDownloaded" fill="clear" (click)="download($event, false)" color="dark"
|
||||
class="core-animate-show-hide" [attr.aria-label]="'core.download' | translate">
|
||||
[@coreShowHideAnimation] [attr.aria-label]="(statusTranslatable || 'core.download') | translate">
|
||||
<ion-icon slot="icon-only" name="cloud-download"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Refresh button. -->
|
||||
<ion-button *ngIf="status == statusOutdated || (status == statusDownloaded && !canTrustDownload)" fill="clear"
|
||||
(click)="download($event, true)" color="dark" class="core-animate-show-hide" [attr.aria-label]="'core.refresh' | translate">
|
||||
<ion-icon slot="icon-only" name="fas-sync"></ion-icon>
|
||||
(click)="download($event, true)" color="dark" [@coreShowHideAnimation]
|
||||
attr.aria-label]="(statusTranslatable || 'core.refresh') | translate">
|
||||
<ion-icon slot="icon-only" name="fas-redo-alt"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Downloaded status icon. -->
|
||||
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal" color="success"
|
||||
name="cloud-done" [attr.aria-label]="'core.downloaded' | translate" role="status"></ion-icon>
|
||||
<ion-icon *ngIf="status == statusDownloaded && canTrustDownload" class="core-icon-downloaded ion-padding-horizontal"
|
||||
color="success" name="cloud-done" [attr.aria-label]="(statusTranslatable || 'core.downloaded') | translate"
|
||||
role="status"></ion-icon>
|
||||
|
||||
<ion-spinner *ngIf="status === statusDownloading" [@coreShowHideAnimation]
|
||||
[attr.aria-label]="(statusTranslatable || 'core.downloading') | translate"></ion-spinner>
|
||||
</ng-container>
|
||||
|
||||
<!-- Spinner. -->
|
||||
<ion-spinner *ngIf="loading || status === statusDownloading" class="core-animate-show-hide"></ion-spinner>
|
||||
<ion-spinner *ngIf="loading" [@coreShowHideAnimation] [attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
|
|
|
@ -14,21 +14,24 @@
|
|||
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { coreShowHideAnimation } from '@classes/animations';
|
||||
|
||||
/**
|
||||
* Component to show a download button with refresh option, the spinner and the status of it.
|
||||
*
|
||||
* Usage:
|
||||
* <core-download-refresh [status]="status" enabled="true" canCheckUpdates="true" action="download()"></core-download-refresh>
|
||||
* <core-download-refresh [status]="status" enabled="true" canTrustDownload="true" action="download()"></core-download-refresh>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-download-refresh',
|
||||
templateUrl: 'core-download-refresh.html',
|
||||
styleUrls: ['download-refresh.scss'],
|
||||
animations: [coreShowHideAnimation],
|
||||
})
|
||||
export class CoreDownloadRefreshComponent {
|
||||
|
||||
@Input() status?: string; // Download status.
|
||||
@Input() statusTranslatable?: string; // Download status translatable string.
|
||||
@Input() enabled = false; // Whether the download is enabled.
|
||||
@Input() loading = true; // Force loading status when is not downloading.
|
||||
@Input() canTrustDownload = false; // If false, refresh will be shown if downloaded.
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Component, Input, OnChanges, ElementRef, SimpleChange } from '@angular/
|
|||
*
|
||||
* Check available icons at https://fontawesome.com/icons?d=gallery&m=free
|
||||
*
|
||||
* @deprecated since 3.9.3. Please use <ion-icon name="fa-icon"> instead.
|
||||
* @deprecated since 3.9.3. Please use <ion-icon name="fas-icon"> instead.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-icon',
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<ng-container *ngIf="!loadingMore && position != 'top'">
|
||||
<div *ngIf="enabled || error" class="ion-padding-horizontal" #bottombutton>
|
||||
<ion-button *ngIf="!error" expand="block" (click)="loadMore()" color="light">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="error" expand="block" (click)="loadMore()" color="light">
|
||||
{{ 'core.tryagain' | translate }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ion-infinite-scroll [disabled]="!enabled || error || loadingMore" (ionInfinite)="loadMore($event)" [position]="position">
|
||||
<ion-infinite-scroll-content></ion-infinite-scroll-content>
|
||||
</ion-infinite-scroll>
|
||||
|
||||
<ng-container *ngIf="!loadingMore && position == 'top'">
|
||||
<div *ngIf="enabled || error" class="ion-padding-horizontal" #topbutton>
|
||||
<ion-button *ngIf="!error" expand="block" (click)="loadMore()" color="light">
|
||||
{{ 'core.loadmore' | translate }}
|
||||
</ion-button>
|
||||
<ion-button *ngIf="error" expand="block" (click)="loadMore()" color="light">
|
||||
{{ 'core.tryagain' | translate }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="loadingMore" class="ion-padding ion-text-center" #spinnercontainer>
|
||||
<ion-spinner></ion-spinner>
|
||||
</div>
|
|
@ -0,0 +1,158 @@
|
|||
// (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, Optional, ViewChild, ElementRef } from '@angular/core';
|
||||
import { IonContent, IonInfiniteScroll } from '@ionic/angular';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
||||
/**
|
||||
* Component to show a infinite loading trigger and spinner while more data is being loaded.
|
||||
*
|
||||
* Usage:
|
||||
* <core-infinite-loading [action]="loadingAction" [enabled]="dataLoaded"></core-inifinite-loading>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-infinite-loading',
|
||||
templateUrl: 'core-infinite-loading.html',
|
||||
})
|
||||
export class CoreInfiniteLoadingComponent implements OnChanges {
|
||||
|
||||
@Input() enabled!: boolean;
|
||||
@Input() error = false;
|
||||
@Input() position: 'top' | 'bottom' = 'bottom';
|
||||
@Output() action: EventEmitter<() => void>; // Will emit an event when triggered.
|
||||
|
||||
@ViewChild('topbutton') topButton?: ElementRef;
|
||||
@ViewChild('infinitescroll') infiniteEl?: ElementRef;
|
||||
@ViewChild('bottombutton') bottomButton?: ElementRef;
|
||||
@ViewChild('spinnercontainer') spinnerContainer?: ElementRef;
|
||||
@ViewChild(IonInfiniteScroll) infiniteScroll?: IonInfiniteScroll;
|
||||
|
||||
loadingMore = false; // Hide button and avoid loading more.
|
||||
|
||||
protected threshold = parseFloat('15%') / 100;
|
||||
|
||||
constructor(
|
||||
protected element: ElementRef,
|
||||
@Optional() protected content: IonContent,
|
||||
) {
|
||||
this.action = new EventEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*
|
||||
* @param changes Changes.
|
||||
*/
|
||||
ngOnChanges(changes: {[name: string]: SimpleChange}): void {
|
||||
if (changes.enabled && this.enabled && this.position == 'bottom') {
|
||||
|
||||
// Infinite scroll enabled. If the list doesn't fill the full height, infinite scroll isn't triggered automatically.
|
||||
this.checkScrollDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks scroll distance to the beginning/end to load more items if needed.
|
||||
*
|
||||
* Previously, this function what firing an scroll event but now we have to calculate the distance
|
||||
* like the Ionic component does.
|
||||
*/
|
||||
protected async checkScrollDistance(): Promise<void> {
|
||||
if (this.enabled) {
|
||||
const scrollElement = await this.content.getScrollElement();
|
||||
|
||||
const infiniteHeight = this.element.nativeElement.getBoundingClientRect().height;
|
||||
|
||||
const scrollTop = scrollElement.scrollTop;
|
||||
const height = scrollElement.offsetHeight;
|
||||
const threshold = height * this.threshold;
|
||||
|
||||
const distanceFromInfinite = (this.position === 'bottom')
|
||||
? scrollElement.scrollHeight - infiniteHeight - scrollTop - threshold - height
|
||||
: scrollTop - infiniteHeight - threshold;
|
||||
|
||||
if (distanceFromInfinite < 0 && !this.loadingMore && this.enabled) {
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load More items calling the action provided.
|
||||
*/
|
||||
loadMore(): void {
|
||||
if (this.loadingMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingMore = true;
|
||||
this.action.emit(this.complete.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete loading.
|
||||
*/
|
||||
complete(): void {
|
||||
if (this.position == 'top') {
|
||||
// Wait a bit before allowing loading more, otherwise it could be re-triggered automatically when it shouldn't.
|
||||
setTimeout(this.completeLoadMore.bind(this), 400);
|
||||
} else {
|
||||
this.completeLoadMore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete loading.
|
||||
*/
|
||||
protected async completeLoadMore(): Promise<void> {
|
||||
this.loadingMore = false;
|
||||
await this.infiniteScroll?.complete();
|
||||
|
||||
// More items loaded. If the list doesn't fill the full height, infinite scroll isn't triggered automatically.
|
||||
this.checkScrollDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of the element.
|
||||
*
|
||||
* @return Height.
|
||||
* @todo erase is not needed: I'm depreacating it because if not needed or getBoundingClientRect has the same result, it should
|
||||
* be erased, also with getElementHeight
|
||||
* @deprecated
|
||||
*/
|
||||
getHeight(): number {
|
||||
// return this.element.nativeElement.getBoundingClientRect().height;
|
||||
|
||||
return (this.position == 'top' ? this.getElementHeight(this.topButton): this.getElementHeight(this.bottomButton)) +
|
||||
this.getElementHeight(this.infiniteEl) +
|
||||
this.getElementHeight(this.spinnerContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the height of an element.
|
||||
*
|
||||
* @param element Element ref.
|
||||
* @return Height.
|
||||
*/
|
||||
protected getElementHeight(element?: ElementRef): number {
|
||||
if (element && element.nativeElement) {
|
||||
return CoreDomUtils.instance.getElementHeight(element.nativeElement, true, true, true);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
<div class="core-loading-container" *ngIf="!hideUntil" role="status"> <!-- @todo [@coreShowHideAnimation] -->
|
||||
<div class="core-loading-container" *ngIf="!hideUntil" role="status" [@coreShowHideAnimation]>
|
||||
<span class="core-loading-spinner">
|
||||
<ion-spinner color="primary"></ion-spinner>
|
||||
<p class="core-loading-message" *ngIf="message" role="status">{{message}}</p>
|
||||
</span>
|
||||
</div>
|
||||
<div #content class="core-loading-content" [id]="uniqueId" [attr.aria-busy]="hideUntil">
|
||||
<div #content class="core-loading-content" [id]="uniqueId" [attr.aria-busy]="hideUntil" [@coreShowHideAnimation]>
|
||||
<ng-content *ngIf="hideUntil">
|
||||
</ng-content> <!-- @todo [@coreShowHideAnimation] -->
|
||||
</ng-content>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,7 @@ import { Component, Input, OnInit, OnChanges, SimpleChange, ViewChild, ElementRe
|
|||
import { CoreEventLoadingChangedData, CoreEvents } from '@singletons/events';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { coreShowHideAnimation } from '@classes/animations';
|
||||
|
||||
/**
|
||||
* Component to show a loading spinner and message while data is being loaded.
|
||||
|
@ -42,7 +43,7 @@ import { Translate } from '@singletons/core.singletons';
|
|||
selector: 'core-loading',
|
||||
templateUrl: 'core-loading.html',
|
||||
styleUrls: ['loading.scss'],
|
||||
// @todo animations: [coreShowHideAnimation],
|
||||
animations: [coreShowHideAnimation],
|
||||
})
|
||||
export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
:host {
|
||||
display: none !important;
|
||||
}
|
|
@ -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, Input, OnInit, OnDestroy, ElementRef } from '@angular/core';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreContextMenuComponent } from '../context-menu/context-menu';
|
||||
|
||||
const BUTTON_HIDDEN_CLASS = 'core-navbar-button-hidden';
|
||||
|
||||
/**
|
||||
* Component to add buttons to the app's header without having to place them inside the header itself. This is meant for
|
||||
* pages that are loaded inside a sub ion-nav, so they don't have a header.
|
||||
*
|
||||
* If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
|
||||
* position. If no start/end is specified, then the buttons will be added to the first <ion-buttons> found in the header.
|
||||
*
|
||||
* If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header.
|
||||
*
|
||||
* You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
|
||||
*
|
||||
* IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-navbar-buttons slot="end">
|
||||
* <ion-button [hidden]="!buttonShown" [attr.aria-label]="Do something" (click)="action()">
|
||||
* <ion-icon name="funnel" slot="icon-only"></ion-icon>
|
||||
* </ion-button>
|
||||
* </core-navbar-buttons>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-navbar-buttons',
|
||||
template: '<ng-content></ng-content>',
|
||||
styleUrls: ['navbar-buttons.scss'],
|
||||
})
|
||||
export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
|
||||
|
||||
// If the hidden input is true, hide all buttons.
|
||||
// eslint-disable-next-line @angular-eslint/no-input-rename
|
||||
@Input('hidden') set hidden(value: boolean) {
|
||||
if (typeof value == 'string' && value == '') {
|
||||
value = true;
|
||||
}
|
||||
this.allButtonsHidden = value;
|
||||
this.showHideAllElements();
|
||||
}
|
||||
|
||||
protected element: HTMLElement;
|
||||
protected allButtonsHidden = false;
|
||||
protected forceHidden = false;
|
||||
protected logger: CoreLogger;
|
||||
protected movedChildren?: Node[];
|
||||
protected instanceId: string;
|
||||
protected mergedContextMenu?: CoreContextMenuComponent;
|
||||
|
||||
constructor(element: ElementRef) {
|
||||
this.element = element.nativeElement;
|
||||
this.logger = CoreLogger.getInstance('CoreNavBarButtonsComponent');
|
||||
this.instanceId = CoreDomUtils.instance.storeInstanceByElement(this.element, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
const header = await this.searchHeader();
|
||||
if (header) {
|
||||
// Search the right buttons container (start, end or any).
|
||||
let selector = 'ion-buttons';
|
||||
|
||||
let slot = this.element.getAttribute('slot');
|
||||
// Take the slot from the parent if it has.
|
||||
if (!slot && this.element.parentElement) {
|
||||
slot = this.element.parentElement.getAttribute('slot');
|
||||
}
|
||||
if (slot) {
|
||||
selector += '[slot="' + slot + '"]';
|
||||
}
|
||||
|
||||
const buttonsContainer = <HTMLElement> header.querySelector(selector);
|
||||
if (buttonsContainer) {
|
||||
this.mergeContextMenus(buttonsContainer);
|
||||
|
||||
const prepend = this.element.hasAttribute('prepend');
|
||||
|
||||
this.movedChildren = CoreDomUtils.instance.moveChildren(this.element, buttonsContainer, prepend);
|
||||
this.showHideAllElements();
|
||||
|
||||
} else {
|
||||
this.logger.warn('The header was found, but it didn\'t have the right ion-buttons.', selector);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Header not found.
|
||||
this.logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force or unforce hiding all buttons. If this is true, it will override the "hidden" input.
|
||||
*
|
||||
* @param value The value to set.
|
||||
*/
|
||||
forceHide(value: boolean): void {
|
||||
this.forceHidden = value;
|
||||
|
||||
this.showHideAllElements();
|
||||
}
|
||||
|
||||
/**
|
||||
* If both button containers have a context menu, merge them into a single one.
|
||||
*
|
||||
* @param buttonsContainer The container where the buttons will be moved.
|
||||
* @todo
|
||||
*/
|
||||
protected mergeContextMenus(buttonsContainer: HTMLElement): void {
|
||||
// Check if both button containers have a context menu.
|
||||
const mainContextMenu = buttonsContainer.querySelector('core-context-menu');
|
||||
if (!mainContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secondaryContextMenu = this.element.querySelector('core-context-menu');
|
||||
if (!secondaryContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Both containers have a context menu. Merge them to prevent having 2 menus at the same time.
|
||||
const mainContextMenuInstance: CoreContextMenuComponent = CoreDomUtils.instance.getInstanceByElement(mainContextMenu);
|
||||
const secondaryContextMenuInstance: CoreContextMenuComponent =
|
||||
CoreDomUtils.instance.getInstanceByElement(secondaryContextMenu);
|
||||
|
||||
// Check that both context menus belong to the same core-tab. We shouldn't merge menus from different tabs.
|
||||
if (mainContextMenuInstance && secondaryContextMenuInstance) {
|
||||
this.mergedContextMenu = secondaryContextMenuInstance;
|
||||
|
||||
this.mergedContextMenu.mergeContextMenus(mainContextMenuInstance);
|
||||
|
||||
// Remove the empty context menu from the DOM.
|
||||
secondaryContextMenu.parentElement?.removeChild(secondaryContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the ion-header where the buttons should be added.
|
||||
*
|
||||
* @param retries Number of retries so far.
|
||||
* @return Promise resolved with the header element.
|
||||
*/
|
||||
protected async searchHeader(retries: number = 0): Promise<HTMLElement> {
|
||||
let parentPage: HTMLElement = this.element;
|
||||
|
||||
while (parentPage) {
|
||||
if (!parentPage.parentElement) {
|
||||
// No parent, stop.
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the next parent page.
|
||||
parentPage = <HTMLElement> CoreDomUtils.instance.closest(parentPage.parentElement, '.ion-page');
|
||||
if (parentPage) {
|
||||
// Check if the page has a header. If it doesn't, search the next parent page.
|
||||
const header = this.searchHeaderInPage(parentPage);
|
||||
if (header && getComputedStyle(header, null).display != 'none') {
|
||||
return header;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header not found.
|
||||
if (retries < 5) {
|
||||
// If the component or any of its parent is inside a ng-content or similar it can be detached when it's initialized.
|
||||
// Try again after a while.
|
||||
return new Promise((resolve, reject): void => {
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
this.searchHeader(retries + 1).then(resolve, reject);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
// We've waited enough time, reject.
|
||||
throw Error('Header not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search ion-header inside a page. The header should be a direct child.
|
||||
*
|
||||
* @param page Page to search in.
|
||||
* @return Header element. Undefined if not found.
|
||||
*/
|
||||
protected searchHeaderInPage(page: HTMLElement): HTMLElement | undefined {
|
||||
for (let i = 0; i < page.children.length; i++) {
|
||||
const child = page.children[i];
|
||||
if (child.tagName == 'ION-HEADER') {
|
||||
return <HTMLElement> child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide all the elements.
|
||||
*/
|
||||
protected showHideAllElements(): void {
|
||||
// Show or hide all moved children.
|
||||
if (this.movedChildren) {
|
||||
this.movedChildren.forEach((child: Node) => {
|
||||
this.showHideElement(child);
|
||||
});
|
||||
}
|
||||
|
||||
// Show or hide all the context menu items that were merged to another context menu.
|
||||
if (this.mergedContextMenu) {
|
||||
if (this.forceHidden || this.allButtonsHidden) {
|
||||
this.mergedContextMenu.removeMergedItems();
|
||||
} else {
|
||||
this.mergedContextMenu.restoreMergedItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide an element.
|
||||
*
|
||||
* @param element Element to show or hide.
|
||||
*/
|
||||
protected showHideElement(element: Node): void {
|
||||
// Check if it's an HTML Element
|
||||
if (element instanceof Element) {
|
||||
element.classList.toggle(BUTTON_HIDDEN_CLASS, !!this.forceHidden || !!this.allButtonsHidden);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
CoreDomUtils.instance.removeInstanceById(this.instanceId);
|
||||
|
||||
// This component was destroyed, remove all the buttons that were moved.
|
||||
// The buttons can be moved outside of the current page, that's why we need to manually destroy them.
|
||||
// There's no need to destroy context menu items that were merged because they weren't moved from their DOM position.
|
||||
if (this.movedChildren) {
|
||||
this.movedChildren.forEach((child) => {
|
||||
if (child.parentElement) {
|
||||
child.parentElement.removeChild(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mergedContextMenu) {
|
||||
this.mergedContextMenu.removeMergedItems();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<ng-container *ngIf="progress >= 0">
|
||||
<progress max="100" [value]="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" [attr.aria-valuenow]="progress">
|
||||
</progress>
|
||||
<div class="core-progress-text">{{ 'core.percentagenumber' | translate: {$a: text} }}</div>
|
||||
</ng-container>
|
|
@ -0,0 +1,33 @@
|
|||
:host {
|
||||
display: flex;
|
||||
|
||||
.core-progress-text {
|
||||
line-height: 40px;
|
||||
font-size: 1rem;
|
||||
color: var(--text-color);
|
||||
width: 55px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
progress {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: var(--height);
|
||||
margin: 16px 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
width: calc(100% - 55px);
|
||||
|
||||
&[value]::-webkit-progress-bar {
|
||||
background-color: var(--background);
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&[value]::-webkit-progress-value {
|
||||
background-color: var(--color);
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// (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, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
|
||||
|
||||
/**
|
||||
* Component to show a progress bar and its value.
|
||||
*
|
||||
* Example usage:
|
||||
* <core-progress-bar [progress]="percentage"></core-progress-bar>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-progress-bar',
|
||||
templateUrl: 'core-progress-bar.html',
|
||||
styleUrls: ['progress-bar.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CoreProgressBarComponent implements OnChanges {
|
||||
|
||||
@Input() progress!: number | string; // Percentage from 0 to 100.
|
||||
@Input() text?: string; // Percentage in text to be shown at the right. If not defined, progress will be used.
|
||||
width?: SafeStyle;
|
||||
protected textSupplied = false;
|
||||
|
||||
constructor(private sanitizer: DomSanitizer) { }
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.text && typeof changes.text.currentValue != 'undefined') {
|
||||
// User provided a custom text, don't use default.
|
||||
this.textSupplied = true;
|
||||
}
|
||||
|
||||
if (changes.progress) {
|
||||
// Progress has changed.
|
||||
if (typeof this.progress == 'string') {
|
||||
this.progress = parseInt(this.progress, 10);
|
||||
}
|
||||
|
||||
if (this.progress < 0 || isNaN(this.progress)) {
|
||||
this.progress = -1;
|
||||
}
|
||||
|
||||
if (this.progress != -1) {
|
||||
// Remove decimals.
|
||||
this.progress = Math.floor(this.progress);
|
||||
|
||||
if (!this.textSupplied) {
|
||||
this.text = String(this.progress);
|
||||
}
|
||||
|
||||
this.width = this.sanitizer.bypassSecurityTrustStyle(this.progress + '%');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -4,11 +4,11 @@
|
|||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeModal()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-times"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-times"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-iframe [src]="recaptchaUrl" (loaded)="loaded($event)"></core-iframe>
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<ion-tabs>
|
||||
<ion-tabs class="hide-header">
|
||||
<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">
|
||||
<ion-icon *ngIf="showPrevButton" name="fa-chevron-left"></ion-icon>
|
||||
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left"></ion-icon>
|
||||
</ion-col>
|
||||
<ion-col class="ion-no-padding" size="10">
|
||||
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slideOpts" [dir]="direction" role="tablist"
|
||||
|
@ -18,13 +18,13 @@
|
|||
<ion-icon *ngIf="tab.icon" [name]="tab.icon"></ion-icon>
|
||||
<ion-label>{{ tab.title | translate}}</ion-label>
|
||||
<ion-badge *ngIf="tab.badge">{{ tab.badge }}</ion-badge>
|
||||
</ion-tab-button>
|
||||
</ion-tab-button>
|
||||
</ion-slide>
|
||||
</ng-container>
|
||||
</ion-slides>
|
||||
</ion-col>
|
||||
<ion-col class="col-with-arrow ion-no-padding" (click)="slideNext()" size="1">
|
||||
<ion-icon *ngIf="showNextButton" name="fa-chevron-right"></ion-icon>
|
||||
<ion-icon *ngIf="showNextButton" name="fas-chevron-right"></ion-icon>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-tab-bar>
|
||||
|
|
|
@ -33,6 +33,9 @@ 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 '@/core/services/utils/dom';
|
||||
import { StackEvent } from '@ionic/angular/directives/navigation/stack-utils';
|
||||
|
||||
/**
|
||||
* This component displays some top scrollable tabs that will autohide on vertical scroll.
|
||||
|
@ -105,6 +108,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
protected isInTransition = false; // Weather Slides is in transition.
|
||||
protected slidesSwiper: any;
|
||||
protected slidesSwiperLoaded = false;
|
||||
protected stackEventsSubscription?: Subscription;
|
||||
|
||||
constructor(
|
||||
protected element: ElementRef,
|
||||
|
@ -151,6 +155,21 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
return;
|
||||
}
|
||||
|
||||
this.stackEventsSubscription = this.ionTabs!.outlet.stackEvents.subscribe(async (stackEvent: StackEvent) => {
|
||||
if (this.isCurrentView) {
|
||||
const content = stackEvent.enteringView.element.querySelector('ion-content');
|
||||
|
||||
this.showHideNavBarButtons(stackEvent.enteringView.element.tagName);
|
||||
if (content) {
|
||||
const scroll = await content.getScrollElement();
|
||||
content.scrollEvents = true;
|
||||
content.addEventListener('ionScroll', (e: CustomEvent): void => {
|
||||
this.showHideTabs(e, scroll);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.tabBarElement = this.element.nativeElement.querySelector('ion-tab-bar');
|
||||
this.tabsElement = this.element.nativeElement.querySelector('ion-tabs');
|
||||
|
||||
|
@ -576,19 +595,29 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
this.selectedIndex = index;
|
||||
|
||||
this.ionChange.emit(selectedTab);
|
||||
|
||||
const content = this.ionTabs!.outlet.nativeEl.querySelector('ion-content');
|
||||
|
||||
if (content) {
|
||||
const scroll = await content.getScrollElement();
|
||||
content.scrollEvents = true;
|
||||
content.addEventListener('ionScroll', (e: CustomEvent): void => {
|
||||
this.showHideTabs(e, scroll);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child core-navbar-buttons and show or hide depending on the page state.
|
||||
* We need to use querySelectorAll because ContentChildren doesn't work with ng-template.
|
||||
* https://github.com/angular/angular/issues/14842
|
||||
*
|
||||
* @param activatedPageName Activated page name.
|
||||
*/
|
||||
protected showHideNavBarButtons(activatedPageName: string): void {
|
||||
const elements = this.ionTabs!.outlet.nativeEl.querySelectorAll('core-navbar-buttons');
|
||||
const domUtils = CoreDomUtils.instance;
|
||||
elements.forEach((element) => {
|
||||
const instance: CoreNavBarButtonsComponent = domUtils.getInstanceByElement(element);
|
||||
|
||||
if (instance) {
|
||||
const pagetagName = element.closest('.ion-page')?.tagName;
|
||||
instance.forceHide(activatedPageName != pagetagName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt tabs to a window resize.
|
||||
*/
|
||||
|
@ -607,6 +636,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
|
|||
if (this.resizeFunction) {
|
||||
window.removeEventListener('resize', this.resizeFunction);
|
||||
}
|
||||
this.stackEventsSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -71,6 +71,12 @@ export class CoreConstants {
|
|||
static readonly OUTDATED = 'outdated';
|
||||
static readonly NOT_DOWNLOADABLE = 'notdownloadable';
|
||||
|
||||
static readonly DOWNLOADED_ICON = 'cloud-done';
|
||||
static readonly DOWNLOADING_ICON = 'spinner';
|
||||
static readonly NOT_DOWNLOADED_ICON = 'cloud-download';
|
||||
static readonly OUTDATED_ICON = 'fas-redo-alt';
|
||||
static readonly NOT_DOWNLOADABLE_ICON = '';
|
||||
|
||||
// Constants from Moodle's resourcelib.
|
||||
static readonly RESOURCELIB_DISPLAY_AUTO = 0; // Try the best way.
|
||||
static readonly RESOURCELIB_DISPLAY_EMBED = 1; // Display using object tag.
|
||||
|
|
|
@ -23,7 +23,7 @@ import { CoreConstants } from '@/core/constants';
|
|||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <ion-icon name="fa-icon">
|
||||
* <ion-icon name="fas-icon">
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ion-icon[name]',
|
||||
|
|
|
@ -194,8 +194,8 @@ export class CoreFormatTextDirective implements OnChanges {
|
|||
|
||||
anchor.classList.add('core-image-viewer-icon');
|
||||
anchor.setAttribute('aria-label', label);
|
||||
// Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
|
||||
anchor.innerHTML = '<ion-icon name="search" class="icon icon-md ion-md-search"></ion-icon>';
|
||||
// @todo Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
|
||||
anchor.innerHTML = '<ion-icon name="fas-search" class="icon icon-md ion-md-search"></ion-icon>';
|
||||
|
||||
anchor.addEventListener('click', (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
// (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 { CORE_SITE_SCHEMAS } from '@/core/services/sites';
|
||||
|
||||
import {
|
||||
SITE_SCHEMA as COURSE_SITE_SCHEMA,
|
||||
OFFLINE_SITE_SCHEMA as COURSE_OFFLINE_SITE_SCHEMA,
|
||||
} from './services/course-db';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CORE_SITE_SCHEMAS,
|
||||
useValue: [
|
||||
COURSE_SITE_SCHEMA,
|
||||
COURSE_OFFLINE_SITE_SCHEMA,
|
||||
],
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CoreCourseModule {
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"activitydisabled": "Your organisation has disabled this activity in the mobile app.",
|
||||
"activitynotyetviewableremoteaddon": "Your organisation installed a plugin that is not yet supported.",
|
||||
"activitynotyetviewablesiteupgradeneeded": "Your organisation's Moodle installation needs to be updated.",
|
||||
"allsections": "All sections",
|
||||
"askadmintosupport": "Contact the site administrator and tell them you want to use this activity with the Moodle Mobile app.",
|
||||
"availablespace": " You currently have about {{available}} free space.",
|
||||
"cannotdeletewhiledownloading": "Files cannot be deleted while the activity is being downloaded. Please wait for the download to finish.",
|
||||
"confirmdeletemodulefiles": "Are you sure you want to delete these files?",
|
||||
"confirmdeletestoreddata": "Are you sure you want to delete the stored data?",
|
||||
"confirmdownload": "You are about to download {{size}}.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmdownloadunknownsize": "It was not possible to calculate the size of the download.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmdownloadzerosize": "You are about to start downloading.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmpartialdownloadsize": "You are about to download <strong>at least</strong> {{size}}.{{availableSpace}} Are you sure you want to continue?",
|
||||
"confirmlimiteddownload": "You are not currently connected to Wi-Fi. ",
|
||||
"contents": "Contents",
|
||||
"couldnotloadsectioncontent": "Could not load the section content. Please try again later.",
|
||||
"couldnotloadsections": "Could not load the sections. Please try again later.",
|
||||
"coursesummary": "Course summary",
|
||||
"downloadcourse": "Download course",
|
||||
"errordownloadingcourse": "Error downloading course.",
|
||||
"errordownloadingsection": "Error downloading section.",
|
||||
"errorgetmodule": "Error getting activity data.",
|
||||
"hiddenfromstudents": "Hidden from students",
|
||||
"hiddenoncoursepage": "Available but not shown on course page",
|
||||
"insufficientavailablespace": "You are trying to download {{size}}. This will leave your device with insufficient space to operate normally. Please clear some storage space first.",
|
||||
"insufficientavailablequota": "Your device could not allocate space to save this download. It may be reserving space for app and system updates. Please clear some storage space first.",
|
||||
"manualcompletionnotsynced": "Manual completion not synchronised.",
|
||||
"nocontentavailable": "No content available at the moment.",
|
||||
"overriddennotice": "Your final grade from this activity was manually adjusted.",
|
||||
"refreshcourse": "Refresh course",
|
||||
"sections": "Sections",
|
||||
"useactivityonbrowser": "You can still use it using your device's web browser.",
|
||||
"warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
|
||||
"warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}"
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// (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 COURSE_STATUS_TABLE = 'course_status';
|
||||
export const SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'CoreCourseProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: COURSE_STATUS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'INTEGER',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
type: 'TEXT',
|
||||
notNull: true,
|
||||
},
|
||||
{
|
||||
name: 'previous',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'updated',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'downloadTime',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'previousDownloadTime',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Database variables for CoreCourseOffline service.
|
||||
*/
|
||||
export const MANUAL_COMPLETION_TABLE = 'course_manual_completion';
|
||||
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||
name: 'CoreCourseOfflineProvider',
|
||||
version: 1,
|
||||
tables: [
|
||||
{
|
||||
name: MANUAL_COMPLETION_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'cmid',
|
||||
type: 'INTEGER',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: 'completed',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'courseid',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'coursename',
|
||||
type: 'TEXT',
|
||||
},
|
||||
{
|
||||
name: 'timecompleted',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type CoreCourseStatusDBRecord = {
|
||||
id: number;
|
||||
status: string;
|
||||
previous: string;
|
||||
updated: number;
|
||||
downloadTime: number;
|
||||
previousDownloadTime: number;
|
||||
};
|
||||
|
||||
export type CoreCourseManualCompletionDBRecord = {
|
||||
cmid: number;
|
||||
completed: number;
|
||||
courseid: number;
|
||||
coursename: string;
|
||||
timecompleted: number;
|
||||
};
|
|
@ -0,0 +1,117 @@
|
|||
// (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/core.singletons';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourseManualCompletionDBRecord, MANUAL_COMPLETION_TABLE } from './course-db';
|
||||
import { CoreStatusWithWarningsWSResponse } from '@services/ws';
|
||||
|
||||
/**
|
||||
* Service to handle offline data for courses.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreCourseOfflineProvider {
|
||||
|
||||
/**
|
||||
* Delete a manual completion stored.
|
||||
*
|
||||
* @param cmId The module ID to remove the completion.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async deleteManualCompletion(cmId: number, siteId?: string): Promise<void> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
await site.getDb().deleteRecords(MANUAL_COMPLETION_TABLE, { cmid: cmId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline manual completions for a certain course.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the list of completions.
|
||||
*/
|
||||
async getAllManualCompletions(siteId?: string): Promise<CoreCourseManualCompletionDBRecord[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return await site.getDb().getRecords(MANUAL_COMPLETION_TABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline manual completions for a certain course.
|
||||
*
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the list of completions.
|
||||
*/
|
||||
async getCourseManualCompletions(courseId: number, siteId?: string): Promise<CoreCourseManualCompletionDBRecord[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return await site.getDb().getRecords(MANUAL_COMPLETION_TABLE, { courseid: courseId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the offline manual completion for a certain module.
|
||||
*
|
||||
* @param cmId The module ID to remove the completion.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the completion, rejected if failure or not found.
|
||||
*/
|
||||
async getManualCompletion(cmId: number, siteId?: string): Promise<CoreCourseManualCompletionDBRecord> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return await site.getDb().getRecord(MANUAL_COMPLETION_TABLE, { cmid: cmId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for manually marking a module as completed.
|
||||
*
|
||||
* @param cmId The module ID to store the completion.
|
||||
* @param completed Whether the module is completed or not.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param courseName Course name. Recommended, it is used to display a better warning message.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when completion is successfully stored.
|
||||
*/
|
||||
async markCompletedManually(
|
||||
cmId: number,
|
||||
completed: boolean,
|
||||
courseId: number,
|
||||
courseName?: string,
|
||||
siteId?: string,
|
||||
): Promise<CoreStatusWithWarningsWSResponse> {
|
||||
|
||||
// Store the offline data.
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const entry: CoreCourseManualCompletionDBRecord = {
|
||||
cmid: cmId,
|
||||
completed: completed ? 1 : 0,
|
||||
courseid: courseId,
|
||||
coursename: courseName || '',
|
||||
timecompleted: Date.now(),
|
||||
};
|
||||
await site.getDb().insertRecord(MANUAL_COMPLETION_TABLE, entry);
|
||||
|
||||
return ({
|
||||
status: true,
|
||||
offline: true,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreCourseOffline extends makeSingleton(CoreCourseOfflineProvider) { }
|
|
@ -0,0 +1,990 @@
|
|||
// (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 { Params } from '@angular/router';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourse, CoreCourseSection } from './course';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||
import { CoreFilepool } from '@services/filepool';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import {
|
||||
CoreCourseBasicData,
|
||||
CoreCourseGetCoursesData,
|
||||
CoreCourses,
|
||||
CoreCourseSearchedData,
|
||||
CoreEnrolledCourseBasicData,
|
||||
CoreEnrolledCourseData,
|
||||
} from '@features/courses/services/courses';
|
||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses.helper';
|
||||
import { CoreArray } from '@singletons/array';
|
||||
import { CoreLoginHelper, CoreLoginHelperProvider } from '@features/login/services/login.helper';
|
||||
import { CoreIonLoadingElement } from '@classes/ion-loading';
|
||||
import { CoreCourseOffline } from './course-offline';
|
||||
|
||||
/**
|
||||
* Prefetch info of a module.
|
||||
*/
|
||||
export type CoreCourseModulePrefetchInfo = {
|
||||
/**
|
||||
* Downloaded size.
|
||||
*/
|
||||
size?: number;
|
||||
|
||||
/**
|
||||
* Downloadable size in a readable format.
|
||||
*/
|
||||
sizeReadable?: string;
|
||||
|
||||
/**
|
||||
* Module status.
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* Icon's name of the module status.
|
||||
*/
|
||||
statusIcon?: string;
|
||||
|
||||
/**
|
||||
* Time when the module was last downloaded.
|
||||
*/
|
||||
downloadTime?: number;
|
||||
|
||||
/**
|
||||
* Download time in a readable format.
|
||||
*/
|
||||
downloadTimeReadable?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Progress of downloading a list of courses.
|
||||
*/
|
||||
export type CoreCourseCoursesProgress = {
|
||||
/**
|
||||
* Number of courses downloaded so far.
|
||||
*/
|
||||
count: number;
|
||||
|
||||
/**
|
||||
* Toal of courses to download.
|
||||
*/
|
||||
total: number;
|
||||
|
||||
/**
|
||||
* Whether the download has been successful so far.
|
||||
*/
|
||||
success: boolean;
|
||||
|
||||
/**
|
||||
* Last downloaded course.
|
||||
*/
|
||||
courseId?: number;
|
||||
};
|
||||
|
||||
export type CorePrefetchStatusInfo = {
|
||||
status: string; // Status of the prefetch.
|
||||
statusTranslatable: string; // Status translatable string.
|
||||
icon: string; // Icon based on the status.
|
||||
loading: boolean; // If it's a loading status.
|
||||
badge?: string; // Progress badge string if any.
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to gather some common course functions.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreCourseHelperProvider {
|
||||
|
||||
protected courseDwnPromises: { [s: string]: { [id: number]: Promise<void> } } = {};
|
||||
protected logger: CoreLogger;
|
||||
|
||||
constructor() {
|
||||
|
||||
this.logger = CoreLogger.getInstance('CoreCourseHelperProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* This function treats every module on the sections provided to load the handler data, treat completion
|
||||
* and navigate to a module page if required. It also returns if sections has content.
|
||||
*
|
||||
* @param sections List of sections to treat modules.
|
||||
* @param courseId Course ID of the modules.
|
||||
* @param completionStatus List of completion status.
|
||||
* @param courseName Course name. Recommended if completionStatus is supplied.
|
||||
* @param forCoursePage Whether the data will be used to render the course page.
|
||||
* @return Whether the sections have content.
|
||||
*/
|
||||
addHandlerDataForModules(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of a section.
|
||||
*
|
||||
* @param section Section to calculate its status. It can't be "All sections".
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param refresh True if it shouldn't use module status cache (slower).
|
||||
* @param checkUpdates Whether to use the WS to check updates. Defaults to true.
|
||||
* @return Promise resolved when the status is calculated.
|
||||
*/
|
||||
calculateSectionStatus(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the status of a list of sections, setting attributes to determine the icons/data to be shown.
|
||||
*
|
||||
* @param sections Sections to calculate their status.
|
||||
* @param courseId Course ID the sections belong to.
|
||||
* @param refresh True if it shouldn't use module status cache (slower).
|
||||
* @param checkUpdates Whether to use the WS to check updates. Defaults to true.
|
||||
* @return Promise resolved when the states are calculated.
|
||||
*/
|
||||
calculateSectionsStatus(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a confirm and prefetch a course. It will retrieve the sections and the course options if not provided.
|
||||
* This function will set the icon to "spinner" when starting and it will also set it back to the initial icon if the
|
||||
* user cancels. All the other updates of the icon should be made when CoreEvents.COURSE_STATUS_CHANGED is received.
|
||||
*
|
||||
* @param data An object where to store the course icon and title: "prefetchCourseIcon", "title" and "downloadSucceeded".
|
||||
* @param course Course to prefetch.
|
||||
* @param sections List of course sections.
|
||||
* @param courseHandlers List of course handlers.
|
||||
* @param menuHandlers List of course menu handlers.
|
||||
* @return Promise resolved when the download finishes, rejected if an error occurs or the user cancels.
|
||||
*/
|
||||
confirmAndPrefetchCourse(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and prefetches a list of courses.
|
||||
*
|
||||
* @param courses List of courses to download.
|
||||
* @param onProgress Function to call everytime a course is downloaded.
|
||||
* @return Resolved when downloaded, rejected if error or canceled.
|
||||
*/
|
||||
async confirmAndPrefetchCourses(
|
||||
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[],
|
||||
onProgress?: (data: CoreCourseCoursesProgress) => void,
|
||||
): Promise<void> {
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
// Confirm the download without checking size because it could take a while.
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.areyousure'));
|
||||
|
||||
const total = courses.length;
|
||||
let count = 0;
|
||||
|
||||
const promises = courses.map((course) => {
|
||||
const subPromises: Promise<void>[] = [];
|
||||
let sections: CoreCourseSection[];
|
||||
let handlers: any;
|
||||
let menuHandlers: any;
|
||||
let success = true;
|
||||
|
||||
// Get the sections and the handlers.
|
||||
subPromises.push(CoreCourse.instance.getSections(course.id, false, true).then((courseSections) => {
|
||||
sections = courseSections;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
/**
|
||||
* @todo
|
||||
subPromises.push(this.courseOptionsDelegate.getHandlersToDisplay(this.injector, course).then((cHandlers: any) => {
|
||||
handlers = cHandlers;
|
||||
}));
|
||||
subPromises.push(this.courseOptionsDelegate.getMenuHandlersToDisplay(this.injector, course).then((mHandlers: any) => {
|
||||
menuHandlers = mHandlers;
|
||||
}));
|
||||
*/
|
||||
|
||||
return Promise.all(subPromises).then(() => this.prefetchCourse(course, sections, handlers, menuHandlers, siteId))
|
||||
.catch((error) => {
|
||||
success = false;
|
||||
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
// Course downloaded or failed, notify the progress.
|
||||
count++;
|
||||
if (onProgress) {
|
||||
onProgress({ count: count, total: total, courseId: course.id, success: success });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (onProgress) {
|
||||
// Notify the start of the download.
|
||||
onProgress({ count: 0, total: total, success: true });
|
||||
}
|
||||
|
||||
return CoreUtils.instance.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show confirmation dialog and then remove a module files.
|
||||
*
|
||||
* @param module Module to remove the files.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param done Function to call when done. It will close the context menu.
|
||||
* @return Promise resolved when done.
|
||||
* @todo module type.
|
||||
*/
|
||||
async confirmAndRemoveFiles(module: any, courseId: number, done?: () => void): Promise<void> {
|
||||
let modal: CoreIonLoadingElement | undefined;
|
||||
|
||||
try {
|
||||
|
||||
await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
||||
|
||||
modal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
await this.removeModuleStoredData(module, courseId);
|
||||
|
||||
done && done();
|
||||
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
CoreDomUtils.instance.showErrorModal(error);
|
||||
}
|
||||
} finally {
|
||||
modal?.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size to download a section and show a confirm modal if needed.
|
||||
*
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param section Section. If not provided, all sections.
|
||||
* @param sections List of sections. Used when downloading all the sections.
|
||||
* @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise.
|
||||
* @return Promise resolved if the user confirms or there's no need to confirm.
|
||||
*/
|
||||
confirmDownloadSizeSection(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to prefetch a module, showing a confirmation modal if the size is big.
|
||||
* This function is meant to be called from a context menu option. It will also modify some data like the prefetch icon.
|
||||
*
|
||||
* @param instance The component instance that has the context menu. It should have prefetchStatusIcon and isDestroyed.
|
||||
* @param module Module to be prefetched
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param done Function to call when done. It will close the context menu.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
contextMenuPrefetch(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the status of a list of courses.
|
||||
*
|
||||
* @param courses Courses
|
||||
* @return Promise resolved with the status.
|
||||
*/
|
||||
async determineCoursesStatus(courses: CoreCourseBasicData[]): Promise<string> {
|
||||
// Get the status of each course.
|
||||
const promises: Promise<string>[] = [];
|
||||
const siteId = CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
courses.forEach((course) => {
|
||||
promises.push(CoreCourse.instance.getCourseStatus(course.id, siteId));
|
||||
});
|
||||
|
||||
const statuses = await Promise.all(promises);
|
||||
|
||||
// Now determine the status of the whole list.
|
||||
let status = statuses[0];
|
||||
const filepool = CoreFilepool.instance;
|
||||
for (let i = 1; i < statuses.length; i++) {
|
||||
status = filepool.determinePackagesStatus(status, statuses[i]);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to open a module main file, downloading the package if needed.
|
||||
* This is meant for modules like mod_resource.
|
||||
*
|
||||
* @param module The module to download.
|
||||
* @param courseId The course ID of the module.
|
||||
* @param component The component to link the files to.
|
||||
* @param componentId An ID to use in conjunction with the component.
|
||||
* @param files List of files of the module. If not provided, use module.contents.
|
||||
* @param siteId The site ID. If not defined, current site.
|
||||
* @return Resolved on success.
|
||||
*/
|
||||
downloadModuleAndOpenFile(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to download a module that has a main file and return the local file's path and other info.
|
||||
* This is meant for modules like mod_resource.
|
||||
*
|
||||
* @param module The module to download.
|
||||
* @param courseId The course ID of the module.
|
||||
* @param component The component to link the files to.
|
||||
* @param componentId An ID to use in conjunction with the component.
|
||||
* @param files List of files of the module. If not provided, use module.contents.
|
||||
* @param siteId The site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
downloadModuleWithMainFileIfNeeded(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to download a module that has a main file and return the local file's path and other info.
|
||||
* This is meant for modules like mod_resource.
|
||||
*
|
||||
* @param module The module to download.
|
||||
* @param courseId The course ID of the module.
|
||||
* @param fixedUrl Main file's fixed URL.
|
||||
* @param files List of files of the module.
|
||||
* @param status The package status.
|
||||
* @param component The component to link the files to.
|
||||
* @param componentId An ID to use in conjunction with the component.
|
||||
* @param siteId The site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected downloadModuleWithMainFile(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to download a module.
|
||||
*
|
||||
* @param module The module to download.
|
||||
* @param courseId The course ID of the module.
|
||||
* @param component The component to link the files to.
|
||||
* @param componentId An ID to use in conjunction with the component.
|
||||
* @param files List of files of the module. If not provided, use module.contents.
|
||||
* @param siteId The site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
downloadModule(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the Context Menu for a certain module.
|
||||
*
|
||||
* @param instance The component instance that has the context menu.
|
||||
* @param module Module to be prefetched
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @param invalidateCache Invalidates the cache first.
|
||||
* @param component Component of the module.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
fillContextMenu(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course. It will first check the user courses, and fallback to another WS if not enrolled.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the course.
|
||||
*/
|
||||
async getCourse(
|
||||
courseId: number,
|
||||
siteId?: string,
|
||||
): Promise<{ enrolled: boolean; course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData }> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
let course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData;
|
||||
|
||||
// Try with enrolled courses first.
|
||||
try {
|
||||
course = await CoreCourses.instance.getUserCourse(courseId, false, siteId);
|
||||
|
||||
return ({ enrolled: true, course: course });
|
||||
} catch {
|
||||
// Not enrolled or an error happened. Try to use another WebService.
|
||||
}
|
||||
|
||||
const available = await CoreCourses.instance.isGetCoursesByFieldAvailableInSite(siteId);
|
||||
|
||||
if (available) {
|
||||
course = await CoreCourses.instance.getCourseByField('id', courseId, siteId);
|
||||
} else {
|
||||
course = await CoreCourses.instance.getCourse(courseId, siteId);
|
||||
}
|
||||
|
||||
return ({ enrolled: false, course: course });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course, wait for any course format plugin to load, and open the course page. It basically chains the functions
|
||||
* getCourse and openCourse.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param params Other params to pass to the course page.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
*/
|
||||
async getAndOpenCourse(courseId: number, params?: Params, siteId?: string): Promise<any> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
let course: CoreEnrolledCourseData | CoreCourseSearchedData | CoreCourseGetCoursesData | { id: number };
|
||||
|
||||
try {
|
||||
const data = await this.getCourse(courseId, siteId);
|
||||
|
||||
course = data.course;
|
||||
} catch {
|
||||
// Cannot get course, return a "fake".
|
||||
course = { id: courseId };
|
||||
}
|
||||
|
||||
modal?.dismiss();
|
||||
|
||||
return this.openCourse(course, params, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the course has a block with that name.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param name Block name to search.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with true if the block exists or false otherwise.
|
||||
* @since 3.3
|
||||
*/
|
||||
async hasABlockNamed(courseId: number, name: string, siteId?: string): Promise<boolean> {
|
||||
try {
|
||||
const blocks = await CoreCourse.instance.getCourseBlocks(courseId, siteId);
|
||||
|
||||
return blocks.some((block) => block.name == name);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the prefetch icon for selected courses.
|
||||
*
|
||||
* @param courses Courses array to get info from.
|
||||
* @param prefetch Prefetch information.
|
||||
* @param minCourses Min course to show icon.
|
||||
* @return Resolved with the prefetch information updated when done.
|
||||
*/
|
||||
async initPrefetchCoursesIcons(
|
||||
courses: CoreCourseBasicData[],
|
||||
prefetch: CorePrefetchStatusInfo,
|
||||
minCourses: number = 2,
|
||||
): Promise<CorePrefetchStatusInfo> {
|
||||
if (!courses || courses.length < minCourses) {
|
||||
// Not enough courses.
|
||||
prefetch.icon = '';
|
||||
|
||||
return prefetch;
|
||||
}
|
||||
|
||||
const status = await this.determineCoursesStatus(courses);
|
||||
|
||||
prefetch = this.getCourseStatusIconAndTitleFromStatus(status);
|
||||
|
||||
if (prefetch.loading) {
|
||||
// It seems all courses are being downloaded, show a download button instead.
|
||||
prefetch.icon = CoreConstants.NOT_DOWNLOADED_ICON;
|
||||
}
|
||||
|
||||
return prefetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load offline completion into a list of sections.
|
||||
* This should be used in 3.6 sites or higher, where the course contents already include the completion.
|
||||
*
|
||||
* @param courseId The course to get the completion.
|
||||
* @param sections List of sections of the course.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadOfflineCompletion(courseId: number, sections: any[], siteId?: string): Promise<void> {
|
||||
const offlineCompletions = await CoreCourseOffline.instance.getCourseManualCompletions(courseId, siteId);
|
||||
|
||||
if (!offlineCompletions || !offlineCompletions.length) {
|
||||
// No offline completion.
|
||||
return;
|
||||
}
|
||||
|
||||
const totalOffline = offlineCompletions.length;
|
||||
let loaded = 0;
|
||||
const offlineCompletionsMap = CoreUtils.instance.arrayToObject(offlineCompletions, 'cmid');
|
||||
// Load the offline data in the modules.
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
if (!section.modules || !section.modules.length) {
|
||||
// Section has no modules, ignore it.
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let j = 0; j < section.modules.length; j++) {
|
||||
const module = section.modules[j];
|
||||
const offlineCompletion = offlineCompletionsMap[module.id];
|
||||
|
||||
if (offlineCompletion && typeof module.completiondata != 'undefined' &&
|
||||
offlineCompletion.timecompleted >= module.completiondata.timecompleted * 1000) {
|
||||
// The module has offline completion. Load it.
|
||||
module.completiondata.state = offlineCompletion.completed;
|
||||
module.completiondata.offline = true;
|
||||
|
||||
// If all completions have been loaded, stop.
|
||||
loaded++;
|
||||
if (loaded == totalOffline) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the courses in the array.
|
||||
*
|
||||
* @param courses Courses array to prefetch.
|
||||
* @param prefetch Prefetch information to be updated.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(
|
||||
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[],
|
||||
prefetch: CorePrefetchStatusInfo,
|
||||
): Promise<void> {
|
||||
prefetch.loading = true;
|
||||
prefetch.icon = CoreConstants.DOWNLOADING_ICON;
|
||||
prefetch.badge = '';
|
||||
|
||||
try {
|
||||
await this.confirmAndPrefetchCourses(courses, (progress) => {
|
||||
prefetch.badge = progress.count + ' / ' + progress.total;
|
||||
});
|
||||
prefetch.icon = CoreConstants.OUTDATED_ICON;
|
||||
} finally {
|
||||
prefetch.loading = false;
|
||||
prefetch.badge = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course download promise (if any).
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Download promise, undefined if not found.
|
||||
*/
|
||||
getCourseDownloadPromise(courseId: number, siteId?: string): Promise<void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
return this.courseDwnPromises[siteId] && this.courseDwnPromises[siteId][courseId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course status icon and the langkey to use as a title.
|
||||
*
|
||||
* @param courseId Course ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the icon name and the title key.
|
||||
*/
|
||||
async getCourseStatusIconAndTitle(courseId: number, siteId?: string): Promise<CorePrefetchStatusInfo> {
|
||||
const status = await CoreCourse.instance.getCourseStatus(courseId, siteId);
|
||||
|
||||
return this.getCourseStatusIconAndTitleFromStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a course status icon and the langkey to use as a title from status.
|
||||
*
|
||||
* @param status Course status.
|
||||
* @return Title and icon name.
|
||||
*/
|
||||
getCourseStatusIconAndTitleFromStatus(status: string): CorePrefetchStatusInfo {
|
||||
const prefetchStatus: CorePrefetchStatusInfo = {
|
||||
status: status,
|
||||
icon: this.getPrefetchStatusIcon(status, false),
|
||||
statusTranslatable: '',
|
||||
loading: false,
|
||||
};
|
||||
|
||||
if (status == CoreConstants.DOWNLOADED) {
|
||||
// Always show refresh icon, we cannot know if there's anything new in course options.
|
||||
prefetchStatus.statusTranslatable = 'core.course.refreshcourse';
|
||||
} else if (status == CoreConstants.DOWNLOADING) {
|
||||
prefetchStatus.statusTranslatable = 'core.downloading';
|
||||
prefetchStatus.loading = true;
|
||||
} else {
|
||||
prefetchStatus.statusTranslatable = 'core.course.downloadcourse';
|
||||
}
|
||||
|
||||
return prefetchStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon given the status and if trust the download status.
|
||||
*
|
||||
* @param status Status constant.
|
||||
* @param trustDownload True to show download success, false to show an outdated status when downloaded.
|
||||
* @return Icon name.
|
||||
*/
|
||||
getPrefetchStatusIcon(status: string, trustDownload: boolean = false): string {
|
||||
if (status == CoreConstants.NOT_DOWNLOADED) {
|
||||
return CoreConstants.NOT_DOWNLOADED_ICON;
|
||||
}
|
||||
if (status == CoreConstants.OUTDATED || (status == CoreConstants.DOWNLOADED && !trustDownload)) {
|
||||
return CoreConstants.OUTDATED_ICON;
|
||||
}
|
||||
if (status == CoreConstants.DOWNLOADED && trustDownload) {
|
||||
return CoreConstants.DOWNLOADED_ICON;
|
||||
}
|
||||
if (status == CoreConstants.DOWNLOADING) {
|
||||
return CoreConstants.DOWNLOADING_ICON;
|
||||
}
|
||||
|
||||
return CoreConstants.DOWNLOADING_ICON;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the course ID from a module instance ID, showing an error message if it can't be retrieved.
|
||||
*
|
||||
* @param id Instance ID.
|
||||
* @param module Name of the module. E.g. 'glossary'.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved with the module's course ID.
|
||||
* @todo module type.
|
||||
*/
|
||||
async getModuleCourseIdByInstance(id: number, module: any, siteId?: string): Promise<number> {
|
||||
try {
|
||||
const cm = await CoreCourse.instance.getModuleBasicInfoByInstance(id, module, siteId);
|
||||
|
||||
return cm.course;
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get prefetch info for a module.
|
||||
*
|
||||
* @param module Module to get the info from.
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param invalidateCache Invalidates the cache first.
|
||||
* @param component Component of the module.
|
||||
* @return Promise resolved with the info.
|
||||
*/
|
||||
getModulePrefetchInfo(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download ID of a section. It's used to interact with CoreCourseModulePrefetchDelegate.
|
||||
*
|
||||
* @param section Section.
|
||||
* @return Section download ID.
|
||||
* @todo section type.
|
||||
*/
|
||||
getSectionDownloadId(section: any): string {
|
||||
return 'Section-' + section.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a module using instance ID and module name.
|
||||
*
|
||||
* @param instanceId Activity instance ID.
|
||||
* @param modName Module name of the activity.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param courseId Course ID. If not defined we'll try to retrieve it from the site.
|
||||
* @param sectionId Section the module belongs to. If not defined we'll try to retrieve it from the site.
|
||||
* @param useModNameToGetModule If true, the app will retrieve all modules of this type with a single WS call. This reduces the
|
||||
* number of WS calls, but it isn't recommended for modules that can return a lot of contents.
|
||||
* @param modParams Params to pass to the module
|
||||
* @param navCtrl NavController for adding new pages to the current history. Optional for legacy support, but
|
||||
* generates a warning if omitted.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
navigateToModuleByInstance(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a module.
|
||||
*
|
||||
* @param moduleId Module's ID.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @param courseId Course ID. If not defined we'll try to retrieve it from the site.
|
||||
* @param sectionId Section the module belongs to. If not defined we'll try to retrieve it from the site.
|
||||
* @param modName If set, the app will retrieve all modules of this type with a single WS call. This reduces the
|
||||
* number of WS calls, but it isn't recommended for modules that can return a lot of contents.
|
||||
* @param modParams Params to pass to the module
|
||||
* @param navCtrl NavController for adding new pages to the current history. Optional for legacy support, but
|
||||
* generates a warning if omitted.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
navigateToModule(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a module.
|
||||
*
|
||||
* @param navCtrl The NavController to use.
|
||||
* @param module The module to open.
|
||||
* @param courseId The course ID of the module.
|
||||
* @param sectionId The section ID of the module.
|
||||
* @param modParams Params to pass to the module
|
||||
* @param True if module can be opened, false otherwise.
|
||||
*/
|
||||
openModule(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the activities in a course and also the course addons.
|
||||
*
|
||||
* @param course The course to prefetch.
|
||||
* @param sections List of course sections.
|
||||
* @param courseHandlers List of course options handlers.
|
||||
* @param courseMenuHandlers List of course menu handlers.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when the download finishes.
|
||||
*/
|
||||
async prefetchCourse(
|
||||
course: CoreEnrolledCourseDataWithExtraInfoAndOptions,
|
||||
sections: CoreCourseSection[],
|
||||
courseHandlers: any[], // @todo CoreCourseOptionsHandlerToDisplay[],
|
||||
courseMenuHandlers: any[], // @todo CoreCourseOptionsMenuHandlerToDisplay[],
|
||||
siteId?: string,
|
||||
): Promise<void> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
if (this.courseDwnPromises[siteId] && this.courseDwnPromises[siteId][course.id]) {
|
||||
// There's already a download ongoing for this course, return the promise.
|
||||
return this.courseDwnPromises[siteId][course.id];
|
||||
} else if (!this.courseDwnPromises[siteId]) {
|
||||
this.courseDwnPromises[siteId] = {};
|
||||
}
|
||||
|
||||
// First of all, mark the course as being downloaded.
|
||||
this.courseDwnPromises[siteId][course.id] = CoreCourse.instance.setCourseStatus(
|
||||
course.id,
|
||||
CoreConstants.DOWNLOADING,
|
||||
siteId,
|
||||
).then(async () => {
|
||||
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
// Prefetch all the sections. If the first section is "All sections", use it. Otherwise, use a fake "All sections".
|
||||
/*
|
||||
* @todo
|
||||
let allSectionsSection = sections[0];
|
||||
if (sections[0].id != CoreCourseProvider.ALL_SECTIONS_ID) {
|
||||
allSectionsSection = { id: CoreCourseProvider.ALL_SECTIONS_ID };
|
||||
}
|
||||
promises.push(this.prefetchSection(allSectionsSection, course.id, sections));
|
||||
|
||||
// Prefetch course options.
|
||||
courseHandlers.forEach((handler) => {
|
||||
if (handler.prefetch) {
|
||||
promises.push(handler.prefetch(course));
|
||||
}
|
||||
});
|
||||
courseMenuHandlers.forEach((handler) => {
|
||||
if (handler.prefetch) {
|
||||
promises.push(handler.prefetch(course));
|
||||
}
|
||||
});*/
|
||||
|
||||
// Prefetch other data needed to render the course.
|
||||
if (CoreCourses.instance.isGetCoursesByFieldAvailable()) {
|
||||
promises.push(CoreCourses.instance.getCoursesByField('id', course.id));
|
||||
}
|
||||
|
||||
const sectionWithModules = sections.find((section) => section.modules && section.modules.length > 0);
|
||||
if (!sectionWithModules || typeof sectionWithModules.modules[0].completion == 'undefined') {
|
||||
promises.push(CoreCourse.instance.getActivitiesCompletionStatus(course.id));
|
||||
}
|
||||
|
||||
// @todo promises.push(this.filterHelper.getFilters('course', course.id));
|
||||
|
||||
return CoreUtils.instance.allPromises(promises);
|
||||
}).then(() =>
|
||||
// Download success, mark the course as downloaded.
|
||||
CoreCourse.instance.setCourseStatus(course.id, CoreConstants.DOWNLOADED, siteId)).catch(async (error) => {
|
||||
// Error, restore previous status.
|
||||
await CoreCourse.instance.setCoursePreviousStatus(course.id, siteId);
|
||||
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
delete this.courseDwnPromises[siteId!][course.id];
|
||||
});
|
||||
|
||||
return this.courseDwnPromises[siteId][course.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to prefetch a module, showing a confirmation modal if the size is big
|
||||
* and invalidating contents if refreshing.
|
||||
*
|
||||
* @param handler Prefetch handler to use. Must implement 'prefetch' and 'invalidateContent'.
|
||||
* @param module Module to download.
|
||||
* @param size Object containing size to download (in bytes) and a boolean to indicate if its totally calculated.
|
||||
* @param courseId Course ID of the module.
|
||||
* @param refresh True if refreshing, false otherwise.
|
||||
* @return Promise resolved when downloaded.
|
||||
*/
|
||||
prefetchModule(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch one section or all the sections.
|
||||
* If the section is "All sections" it will prefetch all the sections.
|
||||
*
|
||||
* @param section Section.
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @param sections List of sections. Used when downloading all the sections.
|
||||
* @return Promise resolved when the prefetch is finished.
|
||||
*/
|
||||
async prefetchSection(): Promise<void> {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a certain section if it needs to be prefetched.
|
||||
* If the section is "All sections" it will be ignored.
|
||||
*
|
||||
* @param section Section to prefetch.
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @return Promise resolved when the section is prefetched.
|
||||
*/
|
||||
protected prefetchSingleSectionIfNeeded(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Start or restore the prefetch of a section.
|
||||
* If the section is "All sections" it will be ignored.
|
||||
*
|
||||
* @param section Section to download.
|
||||
* @param result Result of CoreCourseModulePrefetchDelegate.getModulesStatus for this section.
|
||||
* @param courseId Course ID the section belongs to.
|
||||
* @return Promise resolved when the section has been prefetched.
|
||||
*/
|
||||
protected prefetchSingleSection(): void {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a section has content.
|
||||
*
|
||||
* @param section Section to check.
|
||||
* @return Whether the section has content.
|
||||
* @todo section type.
|
||||
*/
|
||||
sectionHasContent(section: any): boolean {
|
||||
if (section.hiddenbynumsections) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (typeof section.availabilityinfo != 'undefined' && section.availabilityinfo != '') ||
|
||||
section.summary != '' || (section.modules && section.modules.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for any course format plugin to load, and open the course page.
|
||||
*
|
||||
* If the plugin's promise is resolved, the course page will be opened. If it is rejected, they will see an error.
|
||||
* If the promise for the plugin is still in progress when the user tries to open the course, a loader
|
||||
* will be displayed until it is complete, before the course page is opened. If the promise is already complete,
|
||||
* they will see the result immediately.
|
||||
*
|
||||
* @param course Course to open
|
||||
* @param params Params to pass to the course page.
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
openCourse(course: CoreEnrolledCourseBasicData | { id: number }, params?: Params, siteId?: string): Promise<void> {
|
||||
if (!siteId || siteId == CoreSites.instance.getCurrentSiteId()) {
|
||||
// Current site, we can open the course.
|
||||
return CoreCourse.instance.openCourse(course, params);
|
||||
} else {
|
||||
// We need to load the site first.
|
||||
params = params || {};
|
||||
Object.assign(params, { course: course });
|
||||
|
||||
return CoreLoginHelper.instance.redirect(CoreLoginHelperProvider.OPEN_COURSE, params, siteId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete course files.
|
||||
*
|
||||
* @param courseId Course id.
|
||||
* @return Promise to be resolved once the course files are deleted.
|
||||
*/
|
||||
async deleteCourseFiles(courseId: number): Promise<void> {
|
||||
const sections = await CoreCourse.instance.getSections(courseId);
|
||||
const modules = CoreArray.flatten(sections.map((section) => section.modules));
|
||||
|
||||
await Promise.all(
|
||||
modules.map((module) => this.removeModuleStoredData(module, courseId)),
|
||||
);
|
||||
|
||||
await CoreCourse.instance.setCourseStatus(courseId, CoreConstants.NOT_DOWNLOADED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove module stored data.
|
||||
*
|
||||
* @param module Module to remove the files.
|
||||
* @param courseId Course ID the module belongs to.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
// @todo remove when done.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async removeModuleStoredData(module: any, courseId: number): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
// @todo
|
||||
// promises.push(this.prefetchDelegate.removeModuleFiles(module, courseId));
|
||||
|
||||
// @todo
|
||||
// const handler = this.prefetchDelegate.getPrefetchHandlerFor(module);
|
||||
// if (handler) {
|
||||
// promises.push(CoreSites.instance.getCurrentSite().deleteComponentFromCache(handler.component, module.id));
|
||||
// }
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreCourseHelper extends makeSingleton(CoreCourseHelperProvider) {}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,56 @@
|
|||
// (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 { FormsModule } from '@angular/forms';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
||||
import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item';
|
||||
import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress';
|
||||
import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu';
|
||||
import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/self-enrol-password';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
CoreCoursesSelfEnrolPasswordComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
FormsModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
],
|
||||
exports: [
|
||||
CoreCoursesCourseListItemComponent,
|
||||
CoreCoursesCourseProgressComponent,
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
CoreCoursesSelfEnrolPasswordComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
CoreCoursesCourseOptionsMenuComponent,
|
||||
],
|
||||
})
|
||||
export class CoreCoursesComponentsModule {}
|
|
@ -0,0 +1,34 @@
|
|||
<ion-item class="ion-text-wrap" (click)="openCourse()" [class.item-disabled]="course.visible == 0"
|
||||
[title]="course.displayname || course.fullname" detail>
|
||||
<ion-icon *ngIf="!course.courseImage" name="fas-graduation-cap" slot="start" class="course-icon"
|
||||
[attr.course-color]="course.color ? null : course.colorNumber" [style.color]="course.color"></ion-icon>
|
||||
<ion-avatar *ngIf="course.courseImage" slot="start">
|
||||
<img [src]="course.courseImage" core-external-content alt=""/>
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||
class="core-course-additional-info">
|
||||
<span *ngIf="course.categoryname" class="core-course-category">
|
||||
<core-format-text [text]="course.categoryname"></core-format-text>
|
||||
</span>
|
||||
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-category"> | </span>
|
||||
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-shortname">
|
||||
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
</p>
|
||||
<h2>
|
||||
<core-format-text [text]="course.displayname || course.fullname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
<p *ngIf="isEnrolled && course.progress != null && course.progress! >= 0 && course.completionusertracked !== false">
|
||||
<core-progress-bar [progress]="course.progress"></core-progress-bar>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ng-container *ngIf="!isEnrolled">
|
||||
<ion-icon *ngFor="let icon of icons" color="dark" size="small"
|
||||
[name]="icon.icon" [attr.aria-label]="icon.label | translate" slot="end"></ion-icon>
|
||||
</ng-container>
|
||||
</ion-item>
|
|
@ -0,0 +1,48 @@
|
|||
:host {
|
||||
.course-icon {
|
||||
color: white;
|
||||
background: var(--gray-light);
|
||||
padding: 8px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
margin-inline-end: 16px;
|
||||
-webkit-transition: all 50ms ease-in-out;
|
||||
transition: all 50ms ease-in-out;
|
||||
}
|
||||
|
||||
ion-icon[course-color="0"] {
|
||||
color: var(--core-course-color-0);
|
||||
}
|
||||
ion-icon[course-color="1"] {
|
||||
color: var(--core-course-color-1);
|
||||
}
|
||||
ion-icon[course-color="2"] {
|
||||
color: var(--core-course-color-2);
|
||||
}
|
||||
ion-icon[course-color="3"] {
|
||||
color: var(--core-course-color-3);
|
||||
}
|
||||
ion-icon[course-color="4"] {
|
||||
color: var(--core-course-color-4);
|
||||
}
|
||||
ion-icon[course-color="5"] {
|
||||
color: var(--core-course-color-5);
|
||||
}
|
||||
ion-icon[course-color="6"] {
|
||||
color: var(--core-course-color-6);
|
||||
}
|
||||
ion-icon[course-color="7"] {
|
||||
color: var(--core-course-color-7);
|
||||
}
|
||||
ion-icon[course-color="8"] {
|
||||
color: var(--core-course-color-8);
|
||||
}
|
||||
ion-icon[course-color="9"] {
|
||||
color: var(--core-course-color-9);
|
||||
}
|
||||
|
||||
ion-avatar {
|
||||
-webkit-transition: all 50ms ease-in-out;
|
||||
transition: all 50ms ease-in-out;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '../../services/courses.helper';
|
||||
|
||||
/**
|
||||
* This directive is meant to display an item for a list of courses.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-courses-course-list-item [course]="course"></core-courses-course-list-item>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-list-item',
|
||||
templateUrl: 'core-courses-course-list-item.html',
|
||||
styleUrls: ['course-list-item.scss'],
|
||||
})
|
||||
export class CoreCoursesCourseListItemComponent implements OnInit {
|
||||
|
||||
@Input() course!: CoreCourseSearchedData & CoreCourseWithImageAndColor & {
|
||||
completionusertracked?: boolean; // If the user is completion tracked.
|
||||
progress?: number; // Progress percentage.
|
||||
}; // The course to render.
|
||||
|
||||
icons: CoreCoursesEnrolmentIcons[] = [];
|
||||
isEnrolled = false;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
CoreCoursesHelper.instance.loadCourseColorAndImage(this.course);
|
||||
|
||||
// Check if the user is enrolled in the course.
|
||||
try {
|
||||
const course = await CoreCourses.instance.getUserCourse(this.course.id);
|
||||
this.course.progress = course.progress;
|
||||
this.course.completionusertracked = course.completionusertracked;
|
||||
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
this.isEnrolled = false;
|
||||
this.icons = [];
|
||||
|
||||
this.course.enrollmentmethods.forEach((instance) => {
|
||||
if (instance === 'self') {
|
||||
this.icons.push({
|
||||
label: 'core.courses.selfenrolment',
|
||||
icon: 'fas-key',
|
||||
});
|
||||
} else if (instance === 'guest') {
|
||||
this.icons.push({
|
||||
label: 'core.courses.allowguests',
|
||||
icon: 'fas-unlock',
|
||||
});
|
||||
} else if (instance === 'paypal') {
|
||||
this.icons.push({
|
||||
label: 'core.courses.paypalaccepted',
|
||||
icon: 'fab-paypal',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.icons.length == 0) {
|
||||
this.icons.push({
|
||||
label: 'core.courses.notenrollable',
|
||||
icon: 'fas-lock',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a course.
|
||||
*
|
||||
* @param course The course to open.
|
||||
*/
|
||||
openCourse(): void {
|
||||
/* if (this.isEnrolled) {
|
||||
CoreCourseHelper.instance.openCourse(this.course);
|
||||
} else {
|
||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
||||
} */
|
||||
// @todo while opencourse function is not completed, open preview page.
|
||||
this.navCtrl.navigateForward('/courses/preview', { queryParams: { course: this.course } });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrolment icons to show on the list with a label.
|
||||
*/
|
||||
export type CoreCoursesEnrolmentIcons = {
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
<ion-item button class="ion-text-wrap" (click)="action('download')" *ngIf="downloadCourseEnabled">
|
||||
<ion-icon *ngIf="!prefetch.loading" [name]="prefetch.icon" slot="start"></ion-icon>
|
||||
<ion-spinner *ngIf="prefetch.loading" slot="start"></ion-spinner>
|
||||
<ion-label><h2>{{ prefetch.statusTranslatable | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('delete')" *ngIf="prefetch.status == 'downloaded' || prefetch.status == 'outdated'">
|
||||
<ion-icon name="fas-trash" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'addon.storagemanager.deletecourse' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('hide')" *ngIf="!course.hidden">
|
||||
<ion-icon name="fas-eye" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.hidecourse' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('show')" *ngIf="course.hidden">
|
||||
<ion-icon name="fas-eye-slash" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.show' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('favourite')" *ngIf="!course.isfavourite">
|
||||
<ion-icon name="fas-star" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.addtofavourites' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="action('unfavourite')" *ngIf="course.isfavourite">
|
||||
<ion-icon name="far-star" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.removefromfavourites' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// (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 { NavParams, PopoverController } from '@ionic/angular';
|
||||
import { CoreCourses } from '../../services/courses';
|
||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CorePrefetchStatusInfo } from '@features/course/services/course.helper';
|
||||
|
||||
/**
|
||||
* This component is meant to display a popover with the course options.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-options-menu',
|
||||
templateUrl: 'core-courses-course-options-menu.html',
|
||||
})
|
||||
export class CoreCoursesCourseOptionsMenuComponent implements OnInit {
|
||||
|
||||
course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course.
|
||||
prefetch!: CorePrefetchStatusInfo; // The prefecth info.
|
||||
|
||||
downloadCourseEnabled = false;
|
||||
|
||||
constructor(
|
||||
navParams: NavParams,
|
||||
protected popoverController: PopoverController,
|
||||
) {
|
||||
this.course = navParams.get('course') || {};
|
||||
this.prefetch = navParams.get('prefetch') || {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an action over the course.
|
||||
*
|
||||
* @param action Action name to take.
|
||||
*/
|
||||
action(action: string): void {
|
||||
this.popoverController.dismiss(action);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<ion-card [attr.course-color]="course.color ? null : course.colorNumber">
|
||||
<div (click)="openCourse()" class="core-course-thumb" [class.core-course-color-img]="course.courseImage"
|
||||
[style.background-color]="course.color">
|
||||
<img *ngIf="course.courseImage" [src]="course.courseImage" core-external-content alt=""/>
|
||||
</div>
|
||||
<ion-item button lines="none" (click)="openCourse()" [title]="course.displayname || course.fullname"
|
||||
class="core-course-header" [class.item-disabled]="course.visible == 0"
|
||||
[class.core-course-more-than-title]="(course.progress != null && course.progress! >= 0)">
|
||||
<ion-label
|
||||
class="ion-text-wrap core-course-title"
|
||||
[class.core-course-with-buttons]="courseOptionMenuEnabled || (downloadCourseEnabled && showDownload)"
|
||||
[class.core-course-with-spinner]="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner">
|
||||
<p *ngIf="course.categoryname || (course.displayname && course.shortname && course.fullname != course.displayname)"
|
||||
class="core-course-additional-info">
|
||||
<span *ngIf="course.categoryname" class="core-course-category">
|
||||
<core-format-text [text]="course.categoryname"></core-format-text>
|
||||
</span>
|
||||
<span *ngIf="course.categoryname && course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-category"> | </span>
|
||||
<span *ngIf="course.displayname && course.shortname && course.fullname != course.displayname"
|
||||
class="core-course-shortname">
|
||||
<core-format-text [text]="course.shortname" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
</p>
|
||||
<h2>
|
||||
<ion-icon name="fas-star" *ngIf="course.isfavourite"></ion-icon>
|
||||
<core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
|
||||
<div class="core-button-spinner" *ngIf="downloadCourseEnabled && !courseOptionMenuEnabled && showDownload" slot="end">
|
||||
<core-download-refresh
|
||||
[status]="prefetchCourseData.status"
|
||||
[enabled]="downloadCourseEnabled"
|
||||
[statusTranslatable]="prefetchCourseData.statusTranslatable"
|
||||
canTrustDownload="false"
|
||||
[loading]="prefetchCourseData.loading"
|
||||
action="prefetchCourse()"></core-download-refresh>
|
||||
</div>
|
||||
|
||||
<div class="core-button-spinner" *ngIf="courseOptionMenuEnabled" slot="end">
|
||||
<!-- Download course spinner. -->
|
||||
<ion-spinner *ngIf="(downloadCourseEnabled && prefetchCourseData.icon == 'spinner') || showSpinner"></ion-spinner>
|
||||
|
||||
<!-- Options menu. -->
|
||||
<ion-button fill="clear" color="dark" (click)="showCourseOptionsMenu($event)" *ngIf="!showSpinner"
|
||||
[attr.aria-label]="('core.displayoptions' | translate)">
|
||||
<ion-icon name="ellipsis-vertical" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</div>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="showAll && course.progress != null && course.progress! >= 0 && course.completionusertracked !== false" lines="none">
|
||||
<ion-label><core-progress-bar [progress]="course.progress"></core-progress-bar></ion-label>
|
||||
</ion-item>
|
||||
<ng-content></ng-content>
|
||||
</ion-card>
|
|
@ -0,0 +1,163 @@
|
|||
:host {
|
||||
ion-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
height: calc(100% - 20px);
|
||||
|
||||
&[course-color="0"] .core-course-thumb {
|
||||
background: var(--core-course-color-0);
|
||||
}
|
||||
&[course-color="1"] .core-course-thumb {
|
||||
background: var(--core-course-color-1);
|
||||
}
|
||||
&[course-color="2"] .core-course-thumb {
|
||||
background: var(--core-course-color-2);
|
||||
}
|
||||
&[course-color="3"] .core-course-thumb {
|
||||
background: var(--core-course-color-3);
|
||||
}
|
||||
&[course-color="4"] .core-course-thumb {
|
||||
background: var(--core-course-color-4);
|
||||
}
|
||||
&[course-color="5"] .core-course-thumb {
|
||||
background: var(--core-course-color-5);
|
||||
}
|
||||
&[course-color="6"] .core-course-thumb {
|
||||
background: var(--core-course-color-6);
|
||||
}
|
||||
&[course-color="7"] .core-course-thumb {
|
||||
background: var(--core-course-color-7);
|
||||
}
|
||||
&[course-color="8"] .core-course-thumb {
|
||||
background: var(--core-course-color-8);
|
||||
}
|
||||
&[course-color="9"] .core-course-thumb {
|
||||
background: var(--core-course-color-9);
|
||||
}
|
||||
|
||||
.core-course-thumb {
|
||||
padding-top: 40%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
-webkit-transition: all 50ms ease-in-out;
|
||||
transition: all 50ms ease-in-out;
|
||||
|
||||
&.core-course-color-img {
|
||||
background: white;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.core-course-additional-info {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.core-course-header {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
.core-course-title {
|
||||
margin: 5px 0;
|
||||
flex-grow: 1;
|
||||
|
||||
h2 ion-icon {
|
||||
margin-right: 4px;
|
||||
color: var(--core-star-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.core-course-more-than-title {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.core-button-spinner {
|
||||
margin: 0;
|
||||
}
|
||||
.core-button-spinner ion-spinner {
|
||||
vertical-align: top; // the better option for most scenarios
|
||||
vertical-align: -webkit-baseline-middle; // the best for those that support it
|
||||
}
|
||||
|
||||
.core-button-spinner .core-icon-downloaded {
|
||||
font-size: 28.8px;
|
||||
margin-top: 8px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.item-button[icon-only] {
|
||||
min-width: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// @todo
|
||||
:host-context(.core-horizontal-scroll) {
|
||||
/*@include horizontal_scroll_item(80%, 250px, 300px);*/
|
||||
|
||||
ion-card {
|
||||
.core-course-thumb {
|
||||
padding-top: 30%;
|
||||
}
|
||||
|
||||
.core-course-link {
|
||||
/*@include padding(4px, 0px, 4px, 8px);*/
|
||||
.core-course-additional-info {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.core-course-title {
|
||||
margin: 3px 0;
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
ion-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&.core-course-with-buttons {
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
.core-button-spinner {
|
||||
min-height: 40px;
|
||||
min-width: 40px;
|
||||
|
||||
ion-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.item-button[icon-only] {
|
||||
min-width: 40px;
|
||||
width: 40px;
|
||||
font-size: 1.5rem;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(body.version-3-1) {
|
||||
.core-course-thumb{
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
// (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, OnDestroy } from '@angular/core';
|
||||
import { PopoverController } from '@ionic/angular';
|
||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { CoreCourses } from '@features/courses/services/courses';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course.helper';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu';
|
||||
|
||||
/**
|
||||
* This component is meant to display a course for a list of courses with progress.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* <core-courses-course-progress [course]="course">
|
||||
* </core-courses-course-progress>
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-courses-course-progress',
|
||||
templateUrl: 'core-courses-course-progress.html',
|
||||
styleUrls: ['course-progress.scss'],
|
||||
})
|
||||
export class CoreCoursesCourseProgressComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() course!: CoreEnrolledCourseDataWithExtraInfoAndOptions; // The course to render.
|
||||
@Input() showAll = false; // If true, will show all actions, options, star and progress.
|
||||
@Input() showDownload = true; // If true, will show download button. Only works if the options menu is not shown.
|
||||
|
||||
courseStatus = CoreConstants.NOT_DOWNLOADED;
|
||||
isDownloading = false;
|
||||
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
status: '',
|
||||
loading: true,
|
||||
};
|
||||
|
||||
showSpinner = false;
|
||||
downloadCourseEnabled = false;
|
||||
courseOptionMenuEnabled = false;
|
||||
|
||||
protected isDestroyed = false;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
protected siteUpdatedObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected popoverCtrl: PopoverController,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
|
||||
if (this.downloadCourseEnabled) {
|
||||
this.initPrefetchCourse();
|
||||
}
|
||||
|
||||
// This field is only available from 3.6 onwards.
|
||||
this.courseOptionMenuEnabled = this.showAll && typeof this.course.isfavourite != 'undefined';
|
||||
|
||||
// Refresh the enabled flag if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
const wasEnabled = this.downloadCourseEnabled;
|
||||
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
|
||||
if (!wasEnabled && this.downloadCourseEnabled) {
|
||||
// Download course is enabled now, initialize it.
|
||||
this.initPrefetchCourse();
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize prefetch course.
|
||||
*/
|
||||
async initPrefetchCourse(): Promise<void> {
|
||||
if (typeof this.courseStatusObserver != 'undefined') {
|
||||
// Already initialized.
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for status change in course.
|
||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => {
|
||||
if (data.courseId == this.course.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
this.updateCourseStatus(data.status);
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
// Determine course prefetch icon.
|
||||
const status = await CoreCourse.instance.getCourseStatus(this.course.id);
|
||||
|
||||
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||
this.courseStatus = status;
|
||||
|
||||
if (this.prefetchCourseData.loading) {
|
||||
// Course is being downloaded. Get the download promise.
|
||||
const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course.id);
|
||||
if (promise) {
|
||||
// There is a download promise. If it fails, show an error.
|
||||
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.
|
||||
CoreCourse.instance.setCoursePreviousStatus(this.course.id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a course.
|
||||
*/
|
||||
openCourse(): void {
|
||||
CoreCourseHelper.instance.openCourse(this.course);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch the course.
|
||||
*
|
||||
* @param e Click event.
|
||||
*/
|
||||
prefetchCourse(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
/* @ todo try {
|
||||
CoreCourseHelper.instance.confirmAndPrefetchCourse(this.prefetchCourseData, this.course);
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the course.
|
||||
*/
|
||||
async deleteCourse(): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.instance.showDeleteConfirm('core.course.confirmdeletestoreddata');
|
||||
} catch (error) {
|
||||
if (CoreDomUtils.instance.isCanceledError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading();
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.instance.deleteCourseFiles(this.course.id);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, Translate.instance.instant('core.errordeletefile'));
|
||||
} finally {
|
||||
modal.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course status icon and title.
|
||||
*
|
||||
* @param status Status to show.
|
||||
*/
|
||||
protected updateCourseStatus(status: string): void {
|
||||
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||
|
||||
this.courseStatus = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the context menu.
|
||||
*
|
||||
* @param e Click Event.
|
||||
* @todo
|
||||
*/
|
||||
async showCourseOptionsMenu(e: Event): Promise<void> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const popover = await this.popoverCtrl.create({
|
||||
component: CoreCoursesCourseOptionsMenuComponent,
|
||||
componentProps: {
|
||||
course: this.course,
|
||||
courseStatus: this.courseStatus,
|
||||
prefetch: this.prefetchCourseData,
|
||||
},
|
||||
event: e,
|
||||
});
|
||||
popover.present();
|
||||
|
||||
const action = await popover.onDidDismiss<string>();
|
||||
|
||||
if (action.data) {
|
||||
switch (action.data) {
|
||||
case 'download':
|
||||
if (!this.prefetchCourseData.loading) {
|
||||
this.prefetchCourse(e);
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if (this.courseStatus == 'downloaded' || this.courseStatus == 'outdated') {
|
||||
this.deleteCourse();
|
||||
}
|
||||
break;
|
||||
case 'hide':
|
||||
this.setCourseHidden(true);
|
||||
break;
|
||||
case 'show':
|
||||
this.setCourseHidden(false);
|
||||
break;
|
||||
case 'favourite':
|
||||
this.setCourseFavourite(true);
|
||||
break;
|
||||
case 'unfavourite':
|
||||
this.setCourseFavourite(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide/Unhide the course from the course list.
|
||||
*
|
||||
* @param hide True to hide and false to show.
|
||||
* @todo
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected setCourseHidden(hide: boolean): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Favourite/Unfavourite the course from the course list.
|
||||
*
|
||||
* @param favourite True to favourite and false to unfavourite.
|
||||
* @todo
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
protected setCourseFavourite(favourite: boolean): void {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
|
||||
this.siteUpdatedObserver?.off();
|
||||
this.courseStatusObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.selfenrolment' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="close()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon name="fas-times" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding">
|
||||
<form (ngSubmit)="submitPassword($event)" #enrolPasswordForm>
|
||||
<ion-item>
|
||||
<core-show-password [name]="'password'">
|
||||
<ion-input
|
||||
class="ion-text-wrap core-ioninput-password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="{{ 'core.courses.password' | translate }}"
|
||||
[(ngModel)]="password"
|
||||
[core-auto-focus]
|
||||
[clearOnEdit]="false">
|
||||
</ion-input>
|
||||
</core-show-password>
|
||||
</ion-item>
|
||||
<div class="ion-padding">
|
||||
<ion-button expand="block" [disabled]="!password" type="submit">{{ 'core.courses.enrolme' | translate }}</ion-button>
|
||||
</div>
|
||||
</form>
|
||||
</ion-content>
|
|
@ -0,0 +1,63 @@
|
|||
// (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, ElementRef } from '@angular/core';
|
||||
import { ModalController, NavParams } from '@ionic/angular';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
|
||||
/**
|
||||
* Modal that displays a form to enter a password to self enrol in a course.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-self-enrol-password',
|
||||
templateUrl: 'self-enrol-password.html',
|
||||
})
|
||||
export class CoreCoursesSelfEnrolPasswordComponent {
|
||||
|
||||
@ViewChild('enrolPasswordForm') formElement!: ElementRef;
|
||||
password = '';
|
||||
|
||||
constructor(
|
||||
protected modalCtrl: ModalController,
|
||||
navParams: NavParams,
|
||||
) {
|
||||
this.password = navParams.get('password') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Close help modal.
|
||||
*/
|
||||
close(): void {
|
||||
CoreDomUtils.instance.triggerFormCancelledEvent(this.formElement, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
this.modalCtrl.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit password.
|
||||
*
|
||||
* @param e Event.
|
||||
* @param password Password to submit.
|
||||
*/
|
||||
submitPassword(e: Event): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, false, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
this.modalCtrl.dismiss(this.password);
|
||||
}
|
||||
|
||||
}
|
|
@ -13,24 +13,86 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes } from '@angular/router';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { CoreHomeRoutingModule } from '../mainmenu/pages/home/home-routing.module';
|
||||
import { CoreHomeDelegate } from '../mainmenu/services/home.delegate';
|
||||
import { CoreDashboardHomeHandler } from './services/handlers/dashboard.home';
|
||||
import { CoreCoursesMyCoursesHomeHandler } from './services/handlers/my-courses.home';
|
||||
|
||||
const routes: Routes = [
|
||||
const homeRoutes: Routes = [
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/dashboard/dashboard.page.module').then(m => m.CoreCoursesDashboardPageModule),
|
||||
},
|
||||
{
|
||||
path: 'courses/my',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/my-courses/my-courses.page.module')
|
||||
.then(m => m.CoreCoursesMyCoursesPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'courses',
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'my',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'categories',
|
||||
redirectTo: 'categories/root', // Fake "id".
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'categories/:id',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/categories/categories.page.module')
|
||||
.then(m => m.CoreCoursesCategoriesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'all',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/available-courses/available-courses.page.module')
|
||||
.then(m => m.CoreCoursesAvailableCoursesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/search/search.page.module')
|
||||
.then(m => m.CoreCoursesSearchPageModule),
|
||||
},
|
||||
{
|
||||
path: 'my',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/my-courses/my-courses.page.module')
|
||||
.then(m => m.CoreCoursesMyCoursesPageModule),
|
||||
},
|
||||
{
|
||||
path: 'preview',
|
||||
loadChildren: () =>
|
||||
import('@features/courses/pages/course-preview/course-preview.page.module')
|
||||
.then(m => m.CoreCoursesCoursePreviewPageModule),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreHomeRoutingModule.forChild(routes)],
|
||||
exports: [CoreHomeRoutingModule],
|
||||
imports: [
|
||||
CoreHomeRoutingModule.forChild(homeRoutes),
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
exports: [
|
||||
CoreHomeRoutingModule,
|
||||
RouterModule,
|
||||
],
|
||||
providers: [
|
||||
CoreDashboardHomeHandler,
|
||||
CoreCoursesMyCoursesHomeHandler,
|
||||
],
|
||||
})
|
||||
export class CoreCoursesModule {
|
||||
|
@ -38,8 +100,10 @@ export class CoreCoursesModule {
|
|||
constructor(
|
||||
homeDelegate: CoreHomeDelegate,
|
||||
coursesDashboardHandler: CoreDashboardHomeHandler,
|
||||
coursesMyCoursesHandler: CoreCoursesMyCoursesHomeHandler,
|
||||
) {
|
||||
homeDelegate.registerHandler(coursesDashboardHandler);
|
||||
homeDelegate.registerHandler(coursesMyCoursesHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.availablecourses' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="coursesLoaded">
|
||||
<ng-container *ngIf="courses.length > 0">
|
||||
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
|
||||
</ng-container>
|
||||
<core-empty-box *ngIf="!courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocourses' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
import { CoreCoursesAvailableCoursesPage } from './available-courses.page';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesAvailableCoursesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesAvailableCoursesPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesAvailableCoursesPageModule { }
|
|
@ -0,0 +1,78 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { IonRefresher } from '@ionic/angular';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourses, CoreCourseSearchedData } from '../../services/courses';
|
||||
|
||||
/**
|
||||
* Page that displays available courses in current site.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-available-courses',
|
||||
templateUrl: 'available-courses.html',
|
||||
})
|
||||
export class CoreCoursesAvailableCoursesPage implements OnInit {
|
||||
|
||||
courses: CoreCourseSearchedData[] = [];
|
||||
coursesLoaded = false;
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.loadCourses().finally(() => {
|
||||
this.coursesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadCourses(): Promise<void> {
|
||||
const frontpageCourseId = CoreSites.instance.getCurrentSite()!.getSiteHomeId();
|
||||
|
||||
try {
|
||||
const courses = await CoreCourses.instance.getCoursesByField();
|
||||
|
||||
this.courses = courses.filter((course) => course.id != frontpageCourseId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the courses.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshCourses(refresher: CustomEvent<IonRefresher>): void {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||
promises.push(CoreCourses.instance.invalidateCoursesByField());
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.loadCourses().finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>
|
||||
<core-format-text [text]="title" contextLevel="coursecat" [contextInstanceId]="currentCategory && currentCategory!.id">
|
||||
</core-format-text>
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!categoriesLoaded" (ionRefresh)="refreshCategories($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="categoriesLoaded">
|
||||
<ion-item *ngIf="currentCategory" class="ion-text-wrap">
|
||||
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="currentCategory!.name" contextLevel="coursecat"
|
||||
[contextInstanceId]="currentCategory!.id"></core-format-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item class="ion-text-wrap" *ngIf="currentCategory && currentCategory!.description">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="currentCategory!.description" maxHeight="60" contextLevel="coursecat"
|
||||
[contextInstanceId]="currentCategory!.id"></core-format-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div *ngIf="categories.length > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.categories' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<section *ngFor="let category of categories">
|
||||
<ion-item class="ion-text-wrap" router-direction="forward" [routerLink]="['/courses/categories', category.id]"
|
||||
[title]="category.name" detail>
|
||||
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<core-format-text [text]="category.name" contextLevel="coursecat" [contextInstanceId]="category.id">
|
||||
</core-format-text>
|
||||
</h2>
|
||||
</ion-label>
|
||||
<ion-badge slot="end" *ngIf="category.coursecount > 0" color="light">{{category.coursecount}}</ion-badge>
|
||||
</ion-item>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div *ngIf="courses.length > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.courses' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
|
||||
</div>
|
||||
<core-empty-box *ngIf="!categories.length && !courses.length" icon="fas-graduation-cap" [message]="'core.courses.nocoursesyet' | translate">
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,50 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
import { CoreCoursesCategoriesPage } from './categories.page';
|
||||
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesCategoriesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesCategoriesPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesCategoriesPageModule { }
|
|
@ -0,0 +1,123 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreCategoryData, CoreCourses, CoreCourseSearchedData } from '../../services/courses';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Page that displays a list of categories and the courses in the current category if any.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-categories',
|
||||
templateUrl: 'categories.html',
|
||||
})
|
||||
export class CoreCoursesCategoriesPage implements OnInit {
|
||||
|
||||
title: string;
|
||||
currentCategory?: CoreCategoryData;
|
||||
categories: CoreCategoryData[] = [];
|
||||
courses: CoreCourseSearchedData[] = [];
|
||||
categoriesLoaded = false;
|
||||
|
||||
protected categoryId = 0;
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
protected route: ActivatedRoute,
|
||||
) {
|
||||
this.title = Translate.instance.instant('core.courses.categories');
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.categoryId = parseInt(this.route.snapshot.params['id'], 0) || 0;
|
||||
|
||||
this.fetchCategories().finally(() => {
|
||||
this.categoriesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the categories.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchCategories(): Promise<void> {
|
||||
try{
|
||||
const categories: CoreCategoryData[] = await CoreCourses.instance.getCategories(this.categoryId, true);
|
||||
|
||||
this.currentCategory = undefined;
|
||||
|
||||
const index = categories.findIndex((category) => category.id == this.categoryId);
|
||||
|
||||
if (index >= 0) {
|
||||
this.currentCategory = categories[index];
|
||||
// Delete current Category to avoid problems with the formatTree.
|
||||
delete categories[index];
|
||||
}
|
||||
|
||||
// Sort by depth and sortorder to avoid problems formatting Tree.
|
||||
categories.sort((a, b) => {
|
||||
if (a.depth == b.depth) {
|
||||
return (a.sortorder > b.sortorder) ? 1 : ((b.sortorder > a.sortorder) ? -1 : 0);
|
||||
}
|
||||
|
||||
return a.depth > b.depth ? 1 : -1;
|
||||
});
|
||||
|
||||
this.categories = CoreUtils.instance.formatTree(categories, 'parent', 'id', this.categoryId);
|
||||
|
||||
if (this.currentCategory) {
|
||||
this.title = this.currentCategory.name;
|
||||
|
||||
try {
|
||||
this.courses = await CoreCourses.instance.getCoursesByField('category', this.categoryId);
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcategories', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the categories.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshCategories(refresher?: CustomEvent<IonRefresher>): void {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||
promises.push(CoreCourses.instance.invalidateCategories(this.categoryId, true));
|
||||
promises.push(CoreCourses.instance.invalidateCoursesByField('category', this.categoryId));
|
||||
promises.push(CoreSites.instance.getCurrentSite()!.invalidateConfig());
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchCategories().finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
<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]="course?.fullname" contextLevel="course" [contextInstanceId]="course?.id"></core-format-text></ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!dataLoaded" (ionRefresh)="refreshData($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<div class="core-course-thumb-parallax">
|
||||
<div *ngIf="courseImageUrl" (click)="openCourse()" class="core-course-thumb">
|
||||
<img [src]="courseImageUrl" core-external-content alt=""/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="core-course-thumb-parallax-content">
|
||||
<ion-item class="ion-text-wrap" (click)="openCourse()" [title]="course.fullname" [attr.details]="!avoidOpenCourse && canAccessCourse">
|
||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2><core-format-text [text]="course.fullname" contextLevel="course" [contextInstanceId]="course.id"></core-format-text></h2>
|
||||
<p *ngIf="course.categoryname"><core-format-text [text]="course.categoryname"
|
||||
contextLevel="coursecat" [contextInstanceId]="course.categoryid"></core-format-text></p>
|
||||
<p *ngIf="course.startdate">
|
||||
{{course.startdate * 1000 | coreFormatDate:"strftimedatefullshort" }}
|
||||
<span *ngIf="course.enddate"> - {{course.enddate * 1000 | coreFormatDate:"strftimedatefullshort" }}</span>
|
||||
</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="course.summary" detail="false">
|
||||
<ion-label>
|
||||
<core-format-text [text]="course.summary" maxHeight="120" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<ng-container class="ion-text-wrap" *ngIf="course.contacts && course.contacts.length">
|
||||
<ion-item-divider>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.teachers' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item-divider>
|
||||
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
|
||||
[userId]="contact.id"
|
||||
[courseId]="isEnrolled ? course.id : null"
|
||||
[attr.aria-label]="'core.viewprofile' | translate">
|
||||
<ion-avatar core-user-avatar
|
||||
[user]="contact" slot="start"
|
||||
[userId]="contact.id"
|
||||
[courseId]="isEnrolled ? course.id : null">
|
||||
</ion-avatar>
|
||||
<ion-label>
|
||||
<h2>{{contact.fullname}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item-divider></ion-item-divider>
|
||||
</ng-container>
|
||||
|
||||
<ion-item class="ion-text-wrap" *ngIf="course.customfields">
|
||||
<ion-label>
|
||||
<ng-container *ngFor="let field of course.customfields">
|
||||
<div *ngIf="field.value"
|
||||
class="core-customfield core-customfield_{{field.type}} core-customfield_{{field.shortname}}">
|
||||
<span class="core-customfieldname">
|
||||
<core-format-text [text]="field.name" contextLevel="course" [contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</span><span class="core-customfieldseparator">: </span>
|
||||
<span class="core-customfieldvalue">
|
||||
<core-format-text [text]="field.value" maxHeight="120" contextLevel="course"
|
||||
[contextInstanceId]="course.id">
|
||||
</core-format-text>
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
|
||||
<div *ngIf="!isEnrolled" detail="false">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let instance of selfEnrolInstances">
|
||||
<ion-label>
|
||||
<h2>{{ instance.name }}</h2>
|
||||
<ion-button expand="block" class="ion-margin-top" (click)="selfEnrolClicked(instance.id)">
|
||||
{{ 'core.courses.enrolme' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
<ion-item class="ion-text-wrap" *ngIf="!isEnrolled && paypalEnabled">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.paypalaccepted' | translate }}</h2>
|
||||
<p>{{ 'core.paymentinstant' | translate }}</p>
|
||||
<ion-button expand="block" class="ion-margin-top" (click)="paypalEnrol()" *ngIf="isMobile">
|
||||
{{ 'core.courses.sendpaymentbutton' | translate }}
|
||||
</ion-button>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="!isEnrolled && !selfEnrolInstances.length && !paypalEnabled">
|
||||
<ion-label><p>{{ 'core.courses.notenrollable' | translate }}</p></ion-label>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canAccessCourse && downloadCourseEnabled" (click)="prefetchCourse()" detail="false"
|
||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate">
|
||||
<ion-icon *ngIf="!prefetchCourseData.status != statusDownloaded && !prefetchCourseData.loading"
|
||||
[name]="prefetchCourseData.icon" slot="start">
|
||||
</ion-icon>
|
||||
<ion-icon *ngIf="prefetchCourseData.status == statusDownloaded && !prefetchCourseData.loading"
|
||||
slot="start" [name]="prefetchCourseData.icon" color="success"
|
||||
[attr.aria-label]="prefetchCourseData.statusTranslatable | translate" role="status">
|
||||
</ion-icon>
|
||||
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
|
||||
<ion-label><h2>{{ 'core.course.downloadcourse' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item (click)="openCourse()" [title]="course.fullname" *ngIf="!avoidOpenCourse && canAccessCourse">
|
||||
<ion-icon name="fas-briefcase" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.course.contents' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
<ion-item [href]="courseUrl" core-link [title]="course.fullname">
|
||||
<ion-icon name="fas-external-link-alt" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.openinbrowser' | translate }}</h2></ion-label>
|
||||
</ion-item>
|
||||
</div>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,51 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
||||
import { CoreCoursesCoursePreviewPage } from './course-preview.page';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesCoursePreviewPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesCoursePreviewPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesCoursePreviewPageModule { }
|
|
@ -0,0 +1,475 @@
|
|||
// (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, OnDestroy, NgZone, OnInit } from '@angular/core';
|
||||
import { ModalController, IonRefresher, NavController } from '@ionic/angular';
|
||||
import { CoreApp } from '@services/app';
|
||||
import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreTextUtils } from '@services/utils/text';
|
||||
import {
|
||||
CoreCourseEnrolmentMethod,
|
||||
CoreCourseGetCoursesData,
|
||||
CoreCourses,
|
||||
CoreCourseSearchedData,
|
||||
CoreCoursesProvider,
|
||||
CoreEnrolledCourseData,
|
||||
} from '@features/courses/services/courses';
|
||||
// import { CoreCourseOptionsDelegate } from '@features/course/services/options-delegate';
|
||||
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
|
||||
import { CoreCourseHelper, CorePrefetchStatusInfo } from '@features/course/services/course.helper';
|
||||
import { Translate } from '@singletons/core.singletons';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
import { CoreCoursesSelfEnrolPasswordComponent } from '../../components/self-enrol-password/self-enrol-password';
|
||||
|
||||
/**
|
||||
* Page that allows "previewing" a course and enrolling in it if enabled and not enrolled.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-course-preview',
|
||||
templateUrl: 'course-preview.html',
|
||||
styleUrls: ['course-preview.scss'],
|
||||
})
|
||||
export class CoreCoursesCoursePreviewPage implements OnInit, OnDestroy {
|
||||
|
||||
course?: CoreCourseSearchedData;
|
||||
isEnrolled = false;
|
||||
canAccessCourse = true;
|
||||
selfEnrolInstances: CoreCourseEnrolmentMethod[] = [];
|
||||
paypalEnabled = false;
|
||||
dataLoaded = false;
|
||||
avoidOpenCourse = false;
|
||||
prefetchCourseData: CorePrefetchStatusInfo = {
|
||||
icon: '',
|
||||
statusTranslatable: 'core.loading',
|
||||
status: '',
|
||||
loading: true,
|
||||
};
|
||||
|
||||
statusDownloaded = CoreConstants.DOWNLOADED;
|
||||
|
||||
downloadCourseEnabled: boolean;
|
||||
courseUrl = '';
|
||||
courseImageUrl?: string;
|
||||
|
||||
protected isGuestEnabled = false;
|
||||
protected guestInstanceId?: number;
|
||||
protected enrolmentMethods: CoreCourseEnrolmentMethod[] = [];
|
||||
protected waitStart = 0;
|
||||
protected enrolUrl = '';
|
||||
protected paypalReturnUrl = '';
|
||||
protected isMobile: boolean;
|
||||
protected pageDestroyed = false;
|
||||
protected courseStatusObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected modalCtrl: ModalController,
|
||||
// protected courseOptionsDelegate: CoreCourseOptionsDelegate,
|
||||
protected zone: NgZone,
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
this.isMobile = CoreApp.instance.isMobile();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
|
||||
if (this.downloadCourseEnabled) {
|
||||
// Listen for status change in course.
|
||||
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data: CoreEventCourseStatusChanged) => {
|
||||
if (data.courseId == this.course!.id || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
|
||||
this.updateCourseStatus(data.status);
|
||||
}
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
const navParams = this.route.snapshot.queryParams;
|
||||
this.course = navParams['course'];
|
||||
this.avoidOpenCourse = !!navParams['avoidOpenCourse'];
|
||||
|
||||
if (!this.course) {
|
||||
this.navCtrl.back();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSite = CoreSites.instance.getCurrentSite();
|
||||
const currentSiteUrl = currentSite && currentSite.getURL();
|
||||
|
||||
this.paypalEnabled = this.course!.enrollmentmethods?.indexOf('paypal') > -1;
|
||||
this.enrolUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'enrol/index.php?id=' + this.course!.id);
|
||||
this.courseUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'course/view.php?id=' + this.course!.id);
|
||||
this.paypalReturnUrl = CoreTextUtils.instance.concatenatePaths(currentSiteUrl!, 'enrol/paypal/return.php');
|
||||
if (this.course.overviewfiles.length > 0) {
|
||||
this.courseImageUrl = this.course.overviewfiles[0].fileurl;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.getCourse();
|
||||
} finally {
|
||||
if (this.downloadCourseEnabled) {
|
||||
|
||||
// Determine course prefetch icon.
|
||||
this.prefetchCourseData = await CoreCourseHelper.instance.getCourseStatusIconAndTitle(this.course!.id);
|
||||
|
||||
if (this.prefetchCourseData.loading) {
|
||||
// Course is being downloaded. Get the download promise.
|
||||
const promise = CoreCourseHelper.instance.getCourseDownloadPromise(this.course!.id);
|
||||
if (promise) {
|
||||
// There is a download promise. If it fails, show an error.
|
||||
promise.catch((error) => {
|
||||
if (!this.pageDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No download, this probably means that the app was closed while downloading. Set previous status.
|
||||
CoreCourse.instance.setCoursePreviousStatus(this.course!.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user can access as guest.
|
||||
*
|
||||
* @return Promise resolved if can access as guest, rejected otherwise. Resolve param indicates if
|
||||
* password is required for guest access.
|
||||
*/
|
||||
protected async canAccessAsGuest(): Promise<boolean> {
|
||||
if (!this.isGuestEnabled) {
|
||||
throw Error('Guest access is not enabled.');
|
||||
}
|
||||
|
||||
// Search instance ID of guest enrolment method.
|
||||
const method = this.enrolmentMethods.find((method) => method.type == 'guest');
|
||||
this.guestInstanceId = method?.id;
|
||||
|
||||
if (this.guestInstanceId) {
|
||||
const info = await CoreCourses.instance.getCourseGuestEnrolmentInfo(this.guestInstanceId);
|
||||
if (!info.status) {
|
||||
// Not active, reject.
|
||||
throw Error('Guest access is not enabled.');
|
||||
}
|
||||
|
||||
return info.passwordrequired;
|
||||
}
|
||||
|
||||
throw Error('Guest enrollment method not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get course. We use this to determine if a user can see the course or not.
|
||||
*/
|
||||
protected async getCourse(): Promise<void> {
|
||||
// Get course enrolment methods.
|
||||
this.selfEnrolInstances = [];
|
||||
|
||||
try {
|
||||
this.enrolmentMethods = await CoreCourses.instance.getCourseEnrolmentMethods(this.course!.id);
|
||||
|
||||
this.enrolmentMethods.forEach((method) => {
|
||||
if (method.type === 'self') {
|
||||
this.selfEnrolInstances.push(method);
|
||||
} else if (method.type === 'guest') {
|
||||
this.isGuestEnabled = true;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'Error getting enrolment data');
|
||||
}
|
||||
|
||||
try {
|
||||
let course: CoreEnrolledCourseData | CoreCourseGetCoursesData;
|
||||
|
||||
// Check if user is enrolled in the course.
|
||||
try {
|
||||
course = await CoreCourses.instance.getUserCourse(this.course!.id);
|
||||
this.isEnrolled = true;
|
||||
} catch {
|
||||
// The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
|
||||
this.isEnrolled = false;
|
||||
|
||||
course = await CoreCourses.instance.getCourse(this.course!.id);
|
||||
}
|
||||
|
||||
// Success retrieving the course, we can assume the user has permissions to view it.
|
||||
this.course!.fullname = course.fullname || this.course!.fullname;
|
||||
this.course!.summary = course.summary || this.course!.summary;
|
||||
this.canAccessCourse = true;
|
||||
} catch {
|
||||
// The user is not an admin/manager. Check if we can provide guest access to the course.
|
||||
try {
|
||||
this.canAccessCourse = !(await this.canAccessAsGuest());
|
||||
} catch {
|
||||
this.canAccessCourse = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CoreSites.instance.getCurrentSite()?.isVersionGreaterEqualThan('3.7')) {
|
||||
try {
|
||||
const available = await CoreCourses.instance.isGetCoursesByFieldAvailableInSite();
|
||||
if (available) {
|
||||
const course = await CoreCourses.instance.getCourseByField('id', this.course!.id);
|
||||
|
||||
this.course!.customfields = course.customfields;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
this.dataLoaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the course.
|
||||
*/
|
||||
openCourse(): void {
|
||||
if (!this.canAccessCourse || this.avoidOpenCourse) {
|
||||
// Course cannot be opened or we are avoiding opening because we accessed from inside a course.
|
||||
return;
|
||||
}
|
||||
|
||||
CoreCourseHelper.instance.openCourse(this.course!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrol using PayPal.
|
||||
*/
|
||||
async paypalEnrol(): Promise<void> {
|
||||
// We cannot control browser in browser.
|
||||
if (!this.isMobile || !CoreSites.instance.getCurrentSite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasReturnedFromPaypal = false;
|
||||
|
||||
const urlLoaded = (event: InAppBrowserEvent): void => {
|
||||
if (event.url.indexOf(this.paypalReturnUrl) != -1) {
|
||||
hasReturnedFromPaypal = true;
|
||||
} else if (event.url.indexOf(this.courseUrl) != -1 && hasReturnedFromPaypal) {
|
||||
// User reached the course index page after returning from PayPal, close the InAppBrowser.
|
||||
inAppClosed();
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
const inAppClosed = (): void => {
|
||||
// InAppBrowser closed, refresh data.
|
||||
unsubscribeAll();
|
||||
|
||||
if (!this.dataLoaded) {
|
||||
return;
|
||||
}
|
||||
this.dataLoaded = false;
|
||||
this.refreshData();
|
||||
};
|
||||
const unsubscribeAll = (): void => {
|
||||
inAppLoadSubscription?.unsubscribe();
|
||||
inAppExitSubscription?.unsubscribe();
|
||||
};
|
||||
|
||||
// Open the enrolment page in InAppBrowser.
|
||||
const window = await CoreSites.instance.getCurrentSite()!.openInAppWithAutoLogin(this.enrolUrl);
|
||||
|
||||
// Observe loaded pages in the InAppBrowser to check if the enrol process has ended.
|
||||
const inAppLoadSubscription = window.on('loadstart').subscribe((event) => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
this.zone.run(() => urlLoaded(event));
|
||||
});
|
||||
// Observe window closed.
|
||||
const inAppExitSubscription = window.on('exit').subscribe(() => {
|
||||
// Execute the callback in the Angular zone, so change detection doesn't stop working.
|
||||
this.zone.run(inAppClosed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicked in a self enrol button.
|
||||
*
|
||||
* @param instanceId The instance ID of the enrolment method.
|
||||
*/
|
||||
async selfEnrolClicked(instanceId: number): Promise<void> {
|
||||
try {
|
||||
await CoreDomUtils.instance.showConfirm(Translate.instance.instant('core.courses.confirmselfenrol'));
|
||||
|
||||
this.selfEnrolInCourse('', instanceId);
|
||||
} catch {
|
||||
// User cancelled.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Self enrol in a course.
|
||||
*
|
||||
* @param password Password to use.
|
||||
* @param instanceId The instance ID.
|
||||
* @return Promise resolved when self enrolled.
|
||||
*/
|
||||
async selfEnrolInCourse(password: string, instanceId: number): Promise<void> {
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.loading', true);
|
||||
|
||||
try {
|
||||
await CoreCourses.instance.selfEnrol(this.course!.id, password, instanceId);
|
||||
|
||||
// Close modal and refresh data.
|
||||
this.isEnrolled = true;
|
||||
this.dataLoaded = false;
|
||||
|
||||
// Sometimes the list of enrolled courses takes a while to be updated. Wait for it.
|
||||
await this.waitForEnrolled(true);
|
||||
|
||||
this.refreshData().finally(() => {
|
||||
// My courses have been updated, trigger event.
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, {
|
||||
courseId: this.course!.id,
|
||||
course: this.course,
|
||||
action: CoreCoursesProvider.ACTION_ENROL,
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
});
|
||||
|
||||
modal?.dismiss();
|
||||
} catch (error) {
|
||||
modal?.dismiss();
|
||||
|
||||
if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) {
|
||||
// Initialize the self enrol modal.
|
||||
const selfEnrolModal = await this.modalCtrl.create(
|
||||
{
|
||||
component: CoreCoursesSelfEnrolPasswordComponent,
|
||||
componentProps: { password },
|
||||
},
|
||||
);
|
||||
|
||||
// Invalid password, show the modal to enter the password.
|
||||
await selfEnrolModal.present();
|
||||
|
||||
const data = await selfEnrolModal.onDidDismiss<string>();
|
||||
if (typeof data?.data != 'undefined') {
|
||||
this.selfEnrolInCourse(data.data, instanceId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
// No password entered, don't show error.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorselfenrol', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher The refresher if this was triggered by a Pull To Refresh.
|
||||
*/
|
||||
async refreshData(refresher?: CustomEvent<IonRefresher>): Promise<void> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||
promises.push(CoreCourses.instance.invalidateCourse(this.course!.id));
|
||||
promises.push(CoreCourses.instance.invalidateCourseEnrolmentMethods(this.course!.id));
|
||||
// @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions(this.course!.id));
|
||||
if (CoreSites.instance.getCurrentSite() && !CoreSites.instance.getCurrentSite()!.isVersionGreaterEqualThan('3.7')) {
|
||||
promises.push(CoreCourses.instance.invalidateCoursesByField('id', this.course!.id));
|
||||
}
|
||||
if (this.guestInstanceId) {
|
||||
promises.push(CoreCourses.instance.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId));
|
||||
}
|
||||
|
||||
await Promise.all(promises).finally(() => this.getCourse()).finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the course status icon and title.
|
||||
*
|
||||
* @param status Status to show.
|
||||
*/
|
||||
protected updateCourseStatus(status: string): void {
|
||||
this.prefetchCourseData = CoreCourseHelper.instance.getCourseStatusIconAndTitleFromStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the user to be enrolled in the course.
|
||||
*
|
||||
* @param first If it's the first call (true) or it's a recursive call (false).
|
||||
* @return Promise resolved when enrolled or timeout.
|
||||
*/
|
||||
protected async waitForEnrolled(first?: boolean): Promise<void> {
|
||||
if (first) {
|
||||
this.waitStart = Date.now();
|
||||
}
|
||||
|
||||
// Check if user is enrolled in the course.
|
||||
try {
|
||||
CoreCourses.instance.invalidateUserCourses();
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
try {
|
||||
CoreCourses.instance.getUserCourse(this.course!.id);
|
||||
} catch {
|
||||
// Not enrolled, wait a bit and try again.
|
||||
if (this.pageDestroyed || (Date.now() - this.waitStart > 60000)) {
|
||||
// Max time reached or the user left the view, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve): void => {
|
||||
setTimeout(async () => {
|
||||
if (!this.pageDestroyed) {
|
||||
// Wait again.
|
||||
await this.waitForEnrolled();
|
||||
}
|
||||
resolve();
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch the course.
|
||||
*/
|
||||
prefetchCourse(): void {
|
||||
/* @todo CoreCourseHelper.instance.confirmAndPrefetchCourse(this.prefetchCourseData, this.course).catch((error) => {
|
||||
if (!this.pageDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.pageDestroyed = true;
|
||||
|
||||
if (this.courseStatusObserver) {
|
||||
this.courseStatusObserver.off();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
:host {
|
||||
--scroll-factor: 0.5;
|
||||
--translate-z: calc(-2 * var(--scroll-factor))px;
|
||||
--scale: calc(1 + var(--scroll-factor) * 2);
|
||||
|
||||
perspective: 1px;
|
||||
perspective-origin: center top;
|
||||
transform-style: preserve-3d;
|
||||
|
||||
.core-course-thumb-parallax-content {
|
||||
transform: translateZ(0);
|
||||
-webkit-filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
filter: drop-shadow(0px -3px 3px rgba(var(--drop-shadow)));
|
||||
}
|
||||
.core-course-thumb-parallax {
|
||||
height: 40vw;
|
||||
max-height: 35vh;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.core-course-thumb {
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
transform-origin: center top;
|
||||
|
||||
/**
|
||||
* Calculated with scroll-factor: 0.5;
|
||||
* translate-z: -2 * $scroll-factor px;
|
||||
* scale: 1 + $scroll-factor * 2;
|
||||
*/
|
||||
transform: translateZ(-1px) scale(2);
|
||||
}
|
||||
|
||||
|
||||
.core-customfieldvalue core-format-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
<core-navbar-buttons slot="end">
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000"
|
||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
|
||||
[iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="500"
|
||||
[content]="'addon.storagemanager.managestorage' | translate"
|
||||
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<ion-content>
|
||||
<!-- @todo -->
|
||||
<core-empty-box icon="fa-home" [message]="'core.courses.nocourses' | translate">
|
||||
|
|
|
@ -12,25 +12,93 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreCourses, CoreCoursesProvider } from '../../services/courses';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
|
||||
/**
|
||||
* Page that displays the Home.
|
||||
* Page that displays the dashboard page.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-dashboard',
|
||||
templateUrl: 'dashboard.html',
|
||||
styleUrls: ['dashboard.scss'],
|
||||
})
|
||||
export class CoreCoursesDashboardPage implements OnInit {
|
||||
export class CoreCoursesDashboardPage implements OnInit, OnDestroy {
|
||||
|
||||
searchEnabled = false;
|
||||
downloadEnabled = false;
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
downloadEnabledIcon = 'far-square';
|
||||
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
|
||||
siteName = 'Hello world';
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Initialize the component.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
// @todo
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled);
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle download enabled.
|
||||
*/
|
||||
toggleDownload(): void {
|
||||
this.switchDownload(!this.downloadEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to switch download enabled.
|
||||
*
|
||||
* @param enable If enable or disable.
|
||||
*/
|
||||
protected switchDownload(enable: boolean): void {
|
||||
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && enable;
|
||||
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, { enabled: this.downloadEnabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open page to manage courses storage.
|
||||
*/
|
||||
manageCoursesStorage(): void {
|
||||
// @todo this.navCtrl.navigateForward(['/courses/storage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch(): void {
|
||||
this.navCtrl.navigateForward(['/courses/search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.mycourses' | translate }}</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<core-navbar-buttons>
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || downloadAllCoursesLoading"
|
||||
(click)="prefetchCourses()" [attr.aria-label]="'core.courses.downloadcourses' | translate">
|
||||
<ion-icon [name]="downloadAllCoursesIcon" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-spinner [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 ||
|
||||
downloadAllCoursesBadge != '' || !downloadAllCoursesLoading"
|
||||
[attr.aria-label]="'core.loading' | translate"></ion-spinner>
|
||||
<ion-badge [hidden]="!downloadAllCoursesEnabled || !courses || courses.length < 2 || !downloadAllCoursesLoading ||
|
||||
downloadAllCoursesBadge == '' || !downloadAllCoursesLoading"
|
||||
[attr.aria-label]="'core.downloading' | translate">{{downloadAllCoursesBadge}}</ion-badge>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!coursesLoaded" (ionRefresh)="refreshCourses($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
|
||||
<core-loading [hideUntil]="coursesLoaded">
|
||||
<ion-searchbar #searchbar *ngIf="courses && courses.length > 5" [(ngModel)]="filter" (ionInput)="filterChanged($event)"
|
||||
(ionCancel)="filterChanged()" [placeholder]="'core.courses.filtermycourses' | translate">
|
||||
</ion-searchbar>
|
||||
<ion-grid class="ion-no-padding safe-area-page">
|
||||
<ion-row class="ion-no-padding">
|
||||
<ion-col *ngFor="let course of filteredCourses" class="ion-no-padding" size="12" size-sm="6" size-md="6"
|
||||
size-lg="4" size-xl="4" align-self-stretch>
|
||||
<core-courses-course-progress [course]="course" class="core-courseoverview" showAll="true">
|
||||
</core-courses-course-progress>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<core-empty-box *ngIf="!courses || !courses.length" icon="fas-graduation-cap"
|
||||
[message]="'core.courses.nocourses' | translate">
|
||||
<p *ngIf="searchEnabled">{{ 'core.courses.searchcoursesadvice' | translate }}</p>
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -0,0 +1,51 @@
|
|||
// (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 { FormsModule } from '@angular/forms';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
|
||||
import { CoreCoursesMyCoursesPage } from './my-courses.page';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesMyCoursesPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesMyCoursesPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesMyCoursesPageModule { }
|
|
@ -0,0 +1,215 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { NavController, IonSearchbar, IonRefresher } from '@ionic/angular';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import {
|
||||
CoreCoursesProvider,
|
||||
CoreCoursesMyCoursesUpdatedEventData,
|
||||
CoreCourses,
|
||||
} from '../../services/courses';
|
||||
import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses.helper';
|
||||
import { CoreCourseHelper } from '@features/course/services/course.helper';
|
||||
import { CoreConstants } from '@/core/constants';
|
||||
// import { CoreCourseOptionsDelegate } from '@core/course/services/options-delegate';
|
||||
|
||||
/**
|
||||
* Page that displays the list of courses the user is enrolled in.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-my-courses',
|
||||
templateUrl: 'my-courses.html',
|
||||
})
|
||||
export class CoreCoursesMyCoursesPage implements OnInit, OnDestroy {
|
||||
|
||||
@ViewChild(IonSearchbar) searchbar!: IonSearchbar;
|
||||
|
||||
courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
filteredCourses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = [];
|
||||
searchEnabled = false;
|
||||
filter = '';
|
||||
showFilter = false;
|
||||
coursesLoaded = false;
|
||||
downloadAllCoursesIcon = CoreConstants.NOT_DOWNLOADED_ICON;
|
||||
downloadAllCoursesLoading = false;
|
||||
downloadAllCoursesBadge = '';
|
||||
downloadAllCoursesEnabled = false;
|
||||
|
||||
protected myCoursesObserver: CoreEventObserver;
|
||||
protected siteUpdatedObserver: CoreEventObserver;
|
||||
protected isDestroyed = false;
|
||||
protected courseIds = '';
|
||||
|
||||
constructor(
|
||||
protected navCtrl: NavController,
|
||||
) {
|
||||
// Update list if user enrols in a course.
|
||||
this.myCoursesObserver = CoreEvents.on(
|
||||
CoreCoursesProvider.EVENT_MY_COURSES_UPDATED,
|
||||
(data: CoreCoursesMyCoursesUpdatedEventData) => {
|
||||
|
||||
if (data.action == CoreCoursesProvider.ACTION_ENROL) {
|
||||
this.fetchCourses();
|
||||
}
|
||||
},
|
||||
|
||||
CoreSites.instance.getCurrentSiteId(),
|
||||
);
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadAllCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.fetchCourses().finally(() => {
|
||||
this.coursesLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async fetchCourses(): Promise<void> {
|
||||
try {
|
||||
const courses: CoreEnrolledCourseDataWithExtraInfoAndOptions[] = await CoreCourses.instance.getUserCourses();
|
||||
const courseIds = courses.map((course) => course.id);
|
||||
|
||||
this.courseIds = courseIds.join(',');
|
||||
|
||||
await CoreCoursesHelper.instance.loadCoursesExtraInfo(courses);
|
||||
|
||||
if (CoreCourses.instance.canGetAdminAndNavOptions()) {
|
||||
const options = await CoreCourses.instance.getCoursesAdminAndNavOptions(courseIds);
|
||||
courses.forEach((course) => {
|
||||
course.navOptions = options.navOptions[course.id];
|
||||
course.admOptions = options.admOptions[course.id];
|
||||
});
|
||||
}
|
||||
|
||||
this.courses = courses;
|
||||
this.filteredCourses = this.courses;
|
||||
this.filter = '';
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorloadcourses', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the courses.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
refreshCourses(refresher: CustomEvent<IonRefresher>): void {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
promises.push(CoreCourses.instance.invalidateUserCourses());
|
||||
// @todo promises.push(this.courseOptionsDelegate.clearAndInvalidateCoursesOptions());
|
||||
if (this.courseIds) {
|
||||
promises.push(CoreCourses.instance.invalidateCoursesByField('ids', this.courseIds));
|
||||
}
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchCourses().finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the filter.
|
||||
*/
|
||||
switchFilter(): void {
|
||||
this.filter = '';
|
||||
this.showFilter = !this.showFilter;
|
||||
this.filteredCourses = this.courses;
|
||||
if (this.showFilter) {
|
||||
setTimeout(() => {
|
||||
this.searchbar.setFocus();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter has changed.
|
||||
*
|
||||
* @param Received Event.
|
||||
*/
|
||||
filterChanged(event?: Event): void {
|
||||
const target = <HTMLInputElement>event?.target || null;
|
||||
const newValue = target ? String(target.value).trim().toLowerCase() : null;
|
||||
if (!newValue || !this.courses) {
|
||||
this.filteredCourses = this.courses;
|
||||
} else {
|
||||
// Use displayname if avalaible, or fullname if not.
|
||||
if (this.courses.length > 0 && typeof this.courses[0].displayname != 'undefined') {
|
||||
this.filteredCourses = this.courses.filter((course) => course.displayname!.toLowerCase().indexOf(newValue) > -1);
|
||||
} else {
|
||||
this.filteredCourses = this.courses.filter((course) => course.fullname.toLowerCase().indexOf(newValue) > -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch all the courses.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async prefetchCourses(): Promise<void> {
|
||||
this.downloadAllCoursesLoading = true;
|
||||
|
||||
try {
|
||||
await CoreCourseHelper.instance.confirmAndPrefetchCourses(this.courses, (progress) => {
|
||||
this.downloadAllCoursesBadge = progress.count + ' / ' + progress.total;
|
||||
});
|
||||
} catch (error) {
|
||||
if (!this.isDestroyed) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
|
||||
}
|
||||
}
|
||||
|
||||
this.downloadAllCoursesBadge = '';
|
||||
this.downloadAllCoursesLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch(): void {
|
||||
this.navCtrl.navigateForward(['/courses/search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.isDestroyed = true;
|
||||
this.myCoursesObserver?.off();
|
||||
this.siteUpdatedObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
|
||||
</ion-buttons>
|
||||
<ion-title>{{ 'core.courses.searchcourses' | translate }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<core-search-box (onSubmit)="search($event)" (onClear)="clearSearch($event)"
|
||||
[placeholder]="'core.courses.search' | translate" [searchLabel]="'core.courses.search' | translate" autoFocus="true"
|
||||
searchArea="CoreCoursesSearch"></core-search-box>
|
||||
|
||||
<ng-container *ngIf="total > 0">
|
||||
<ion-item-divider>
|
||||
<ion-label><h2>{{ 'core.courses.totalcoursesearchresults' | translate:{$a: total} }}</h2></ion-label>
|
||||
</ion-item-divider>
|
||||
<core-courses-course-list-item *ngFor="let course of courses" [course]="course"></core-courses-course-list-item>
|
||||
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMoreResults($event)" [error]="loadMoreError">
|
||||
</core-infinite-loading>
|
||||
</ng-container>
|
||||
<core-empty-box *ngIf="total == 0" icon="search" [message]="'core.courses.nosearchresults' | translate"></core-empty-box>
|
||||
</ion-content>
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreCoursesComponentsModule } from '../../components/components.module';
|
||||
import { CoreSearchComponentsModule } from '@features/search/components/components.module';
|
||||
|
||||
import { CoreCoursesSearchPage } from './search.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreCoursesSearchPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCoursesComponentsModule,
|
||||
CoreSearchComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreCoursesSearchPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreCoursesSearchPageModule { }
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreCourseBasicSearchedData, CoreCourses } from '../../services/courses';
|
||||
|
||||
/**
|
||||
* Page that allows searching for courses.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-courses-search',
|
||||
templateUrl: 'search.html',
|
||||
})
|
||||
export class CoreCoursesSearchPage {
|
||||
|
||||
total = 0;
|
||||
courses: CoreCourseBasicSearchedData[] = [];
|
||||
canLoadMore = false;
|
||||
loadMoreError = false;
|
||||
|
||||
protected page = 0;
|
||||
protected currentSearch = '';
|
||||
|
||||
/**
|
||||
* Search a new text.
|
||||
*
|
||||
* @param text The text to search.
|
||||
*/
|
||||
async search(text: string): Promise<void> {
|
||||
this.currentSearch = text;
|
||||
this.courses = [];
|
||||
this.page = 0;
|
||||
this.total = 0;
|
||||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.searching', true);
|
||||
this.searchCourses().finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search box.
|
||||
*/
|
||||
clearSearch(): void {
|
||||
this.currentSearch = '';
|
||||
this.courses = [];
|
||||
this.page = 0;
|
||||
this.total = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more results.
|
||||
*
|
||||
* @param infiniteComplete Infinite scroll complete function. Only used from core-infinite-loading.
|
||||
*/
|
||||
loadMoreResults(infiniteComplete?: () => void ): void {
|
||||
this.searchCourses().finally(() => {
|
||||
infiniteComplete && infiniteComplete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Search courses or load the next page of current search.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async searchCourses(): Promise<void> {
|
||||
this.loadMoreError = false;
|
||||
|
||||
try {
|
||||
const response = await CoreCourses.instance.search(this.currentSearch, this.page);
|
||||
|
||||
if (this.page === 0) {
|
||||
this.courses = response.courses;
|
||||
} else {
|
||||
this.courses = this.courses.concat(response.courses);
|
||||
}
|
||||
this.total = response.total;
|
||||
|
||||
this.page++;
|
||||
this.canLoadMore = this.courses.length < this.total;
|
||||
} catch (error) {
|
||||
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.courses.errorsearching', true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
// import { PopoverController } from '@ionic/angular';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreCourses, CoreCourseSearchedData, CoreCourseUserAdminOrNavOptionIndexed, CoreEnrolledCourseData } from './courses';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
import { CoreWSExternalFile } from '@services/ws';
|
||||
// import { AddonCourseCompletionProvider } from '@addon/coursecompletion/providers/coursecompletion';
|
||||
// import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
|
||||
|
||||
/**
|
||||
* Helper to gather some common courses functions.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreCoursesHelperProvider {
|
||||
|
||||
/**
|
||||
* Get the courses to display the course picker popover. If a courseId is specified, it will also return its categoryId.
|
||||
*
|
||||
* @param courseId Course ID to get the category.
|
||||
* @return Promise resolved with the list of courses and the category.
|
||||
*/
|
||||
async getCoursesForPopover(): Promise<void> {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a course object returned by core_enrol_get_users_courses and another one returned by core_course_get_courses_by_field,
|
||||
* load some extra data to the first one.
|
||||
*
|
||||
* @param course Course returned by core_enrol_get_users_courses.
|
||||
* @param courseByField Course returned by core_course_get_courses_by_field.
|
||||
* @param addCategoryName Whether add category name or not.
|
||||
*/
|
||||
loadCourseExtraInfo(
|
||||
course: CoreEnrolledCourseDataWithExtraInfo,
|
||||
courseByField: CoreCourseSearchedData,
|
||||
addCategoryName: boolean = false,
|
||||
colors?: (string | undefined)[],
|
||||
): void {
|
||||
if (courseByField) {
|
||||
course.displayname = courseByField.displayname;
|
||||
course.categoryname = addCategoryName ? courseByField.categoryname : undefined;
|
||||
course.overviewfiles = course.overviewfiles || courseByField.overviewfiles;
|
||||
} else {
|
||||
delete course.displayname;
|
||||
}
|
||||
|
||||
this.loadCourseColorAndImage(course, colors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of courses returned by core_enrol_get_users_courses, load some extra data using the WebService
|
||||
* core_course_get_courses_by_field if available.
|
||||
*
|
||||
* @param courses List of courses.
|
||||
* @param loadCategoryNames Whether load category names or not.
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
async loadCoursesExtraInfo(courses: CoreEnrolledCourseDataWithExtraInfo[], loadCategoryNames: boolean = false): Promise<void> {
|
||||
if (!courses.length ) {
|
||||
// No courses or cannot get the data, stop.
|
||||
return;
|
||||
}
|
||||
|
||||
let coursesInfo = {};
|
||||
let courseInfoAvailable = false;
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
let colors: (string | undefined)[] = [];
|
||||
|
||||
promises.push(this.loadCourseSiteColors().then((loadedColors) => {
|
||||
colors = loadedColors;
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
if (CoreCourses.instance.isGetCoursesByFieldAvailable() && (loadCategoryNames ||
|
||||
(typeof courses[0].overviewfiles == 'undefined' && typeof courses[0].displayname == 'undefined'))) {
|
||||
const courseIds = courses.map((course) => course.id).join(',');
|
||||
|
||||
courseInfoAvailable = true;
|
||||
|
||||
// Get the extra data for the courses.
|
||||
promises.push(CoreCourses.instance.getCoursesByField('ids', courseIds).then((coursesInfos) => {
|
||||
coursesInfo = CoreUtils.instance.arrayToObject(coursesInfos, 'id');
|
||||
|
||||
return;
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
courses.forEach((course) => {
|
||||
this.loadCourseExtraInfo(course, courseInfoAvailable ? coursesInfo[course.id] : course, loadCategoryNames, colors);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load course colors from site config.
|
||||
*
|
||||
* @return course colors RGB.
|
||||
*/
|
||||
protected async loadCourseSiteColors(): Promise<(string | undefined)[]> {
|
||||
const site = CoreSites.instance.getCurrentSite();
|
||||
const colors: (string | undefined)[] = [];
|
||||
|
||||
if (site?.isVersionGreaterEqualThan('3.8')) {
|
||||
try {
|
||||
const configs = await site.getConfig();
|
||||
for (let x = 0; x < 10; x++) {
|
||||
colors[x] = configs['core_admin_coursecolor' + (x + 1)] || undefined;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the color of the course or the thumb image.
|
||||
*
|
||||
* @param course Course data.
|
||||
* @param colors Colors loaded.
|
||||
*/
|
||||
async loadCourseColorAndImage(course: CoreCourseWithImageAndColor, colors?: (string | undefined)[]): Promise<void> {
|
||||
if (!colors) {
|
||||
colors = await this.loadCourseSiteColors();
|
||||
}
|
||||
|
||||
if (course.overviewfiles && course.overviewfiles[0]) {
|
||||
course.courseImage = course.overviewfiles[0].fileurl;
|
||||
} else {
|
||||
course.colorNumber = course.id % 10;
|
||||
course.color = colors.length ? colors[course.colorNumber] : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user courses with admin and nav options.
|
||||
*
|
||||
* @param sort Sort courses after get them. If sort is not defined it won't be sorted.
|
||||
* @param slice Slice results to get the X first one. If slice > 0 it will be done after sorting.
|
||||
* @param filter Filter using some field.
|
||||
* @param loadCategoryNames Whether load category names or not.
|
||||
* @return Courses filled with options.
|
||||
*/
|
||||
async getUserCoursesWithOptions(): Promise<void> {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a context menu to select a course, and return the courseId and categoryId of the selected course (-1 for all courses).
|
||||
* Returns an empty object if popover closed without picking a course.
|
||||
*
|
||||
* @param event Click event.
|
||||
* @param courses List of courses, from CoreCoursesHelperProvider.getCoursesForPopover.
|
||||
* @param courseId The course to select at start.
|
||||
* @return Promise resolved with the course ID and category ID.
|
||||
*/
|
||||
async selectCourse(): Promise<void> {
|
||||
// @todo params and logic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreCoursesHelper extends makeSingleton(CoreCoursesHelperProvider) { }
|
||||
|
||||
/**
|
||||
* Course with colors info and course image.
|
||||
*/
|
||||
export type CoreCourseWithImageAndColor = {
|
||||
id: number; // Course id.
|
||||
overviewfiles?: CoreWSExternalFile[];
|
||||
colorNumber?: number; // Color index number.
|
||||
color?: string; // Color RGB.
|
||||
courseImage?: string; // Course thumbnail.
|
||||
};
|
||||
|
||||
/**
|
||||
* Enrolled course data with extra rendering info.
|
||||
*/
|
||||
export type CoreEnrolledCourseDataWithExtraInfo = CoreCourseWithImageAndColor & CoreEnrolledCourseData & {
|
||||
categoryname?: string; // Category name,
|
||||
};
|
||||
|
||||
/**
|
||||
* Enrolled course data with admin and navigation option availability.
|
||||
*/
|
||||
export type CoreEnrolledCourseDataWithOptions = CoreEnrolledCourseData & {
|
||||
navOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
admOptions?: CoreCourseUserAdminOrNavOptionIndexed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Enrolled course data with admin and navigation option availability and extra rendering info.
|
||||
*/
|
||||
export type CoreEnrolledCourseDataWithExtraInfoAndOptions = CoreEnrolledCourseDataWithExtraInfo & CoreEnrolledCourseDataWithOptions;
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
|
|||
import { CoreHomeHandler, CoreHomeHandlerToDisplay } from '@features/mainmenu/services/home.delegate';
|
||||
|
||||
/**
|
||||
* Handler to add Home into main menu.
|
||||
* Handler to add dashboard into home page.
|
||||
*/
|
||||
Injectable();
|
||||
export class CoreDashboardHomeHandler implements CoreHomeHandler {
|
||||
|
@ -41,7 +41,7 @@ export class CoreDashboardHomeHandler implements CoreHomeHandler {
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async isEnabledForSite(siteId?: string): Promise<boolean> {
|
||||
// @todo
|
||||
// @todo return this.blockDelegate.hasSupportedBlock(this.blocks);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
// (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 { CoreHomeHandler, CoreHomeHandlerToDisplay } from '@features/mainmenu/services/home.delegate';
|
||||
|
||||
/**
|
||||
* Handler to add my courses into home page.
|
||||
*/
|
||||
Injectable();
|
||||
export class CoreCoursesMyCoursesHomeHandler implements CoreHomeHandler {
|
||||
|
||||
name = 'CoreCoursesMyCourses';
|
||||
priority = 900;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return this.isEnabledForSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async isEnabledForSite(siteId?: string): Promise<boolean> {
|
||||
// @todo return !this.blockDelegate.hasSupportedBlock(this.blocks) && !CoreSiteHome.instance.isAvailable(siteId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreHomeHandlerToDisplay {
|
||||
return {
|
||||
title: 'core.courses.mycourses',
|
||||
page: 'courses/my',
|
||||
class: 'core-courses-my-courses-handler',
|
||||
icon: 'fas-graduation-cap',
|
||||
selectPriority: 900,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -30,8 +30,8 @@
|
|||
<!-- Button to start/stop in mobile devices. -->
|
||||
<ion-button fill="clear" *ngIf="!hasCaptured && isCordovaAudioCapture" (click)="actionClicked()"
|
||||
[attr.aria-label]="title">
|
||||
<ion-icon *ngIf="!isCapturing" name="fa-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="!isCapturing" name="fas-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fas-square" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
|
||||
<!-- Audio player to listen to the result. -->
|
||||
|
@ -47,13 +47,13 @@
|
|||
<ion-col class="ion-text-center">
|
||||
<ion-button fill="clear" *ngIf="!hasCaptured && !isCordovaAudioCapture" (click)="actionClicked()"
|
||||
[attr.aria-label]="title">
|
||||
<ion-icon *ngIf="!isCapturing && isAudio" name="fa-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="!isCapturing && isVideo" name="fa-video" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isImage" name="fa-camera" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fa-square" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="!isCapturing && isAudio" name="fas-microphone" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="!isCapturing && isVideo" name="fas-video" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isImage" name="fas-camera" slot="icon-only"></ion-icon>
|
||||
<ion-icon *ngIf="isCapturing" name="fas-square" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button fill="clear" *ngIf="hasCaptured" (click)="discard()" [attr.aria-label]="'core.discard' | translate">
|
||||
<ion-icon color="danger" name="fa-trash" slot="icon-only"></ion-icon>
|
||||
<ion-icon color="danger" name="fas-trash" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
<ion-col class="ion-padding ion-text-end chrono-container">
|
||||
|
|
|
@ -14,19 +14,23 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { CoreCourseModule } from './course/course.module';
|
||||
import { CoreCoursesModule } from './courses/courses.module';
|
||||
import { CoreEmulatorModule } from './emulator/emulator.module';
|
||||
import { CoreFileUploaderInitModule } from './fileuploader/fileuploader-init.module';
|
||||
import { CoreLoginModule } from './login/login.module';
|
||||
import { CoreSettingsInitModule } from './settings/settings-init.module';
|
||||
import { CoreSiteHomeInitModule } from './sitehome/sitehome-init.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CoreEmulatorModule,
|
||||
CoreLoginModule,
|
||||
CoreCourseModule,
|
||||
CoreCoursesModule,
|
||||
CoreSettingsInitModule,
|
||||
CoreFileUploaderInitModule,
|
||||
CoreSiteHomeInitModule,
|
||||
],
|
||||
})
|
||||
export class CoreFeaturesModule {}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="closeHelp()" [attr.aria-label]="'core.close' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-times"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-times"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
@ -73,4 +73,4 @@
|
|||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-content>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<ion-toolbar>
|
||||
<ion-buttons slot="start">
|
||||
<ion-button (click)="previous($event)" [attr.aria-label]="'core.back' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-arrow-left"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-arrow-left"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<ion-buttons slot="end">
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-cog"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
@ -53,7 +53,7 @@
|
|||
<ng-container *ngIf="showScanQR">
|
||||
<div class="ion-text-center ion-padding">{{ 'core.login.or' | translate }}</div>
|
||||
<ion-button expand="block" color="light" class="ion-margin" lines="none" (click)="showInstructionsAndScanQR()">
|
||||
<ion-icon slot="start" name="fa-qrcode" aria-hidden="true"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.scanqr' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<ion-buttons slot="end">
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-cog"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
@ -50,7 +50,7 @@
|
|||
<ion-item button *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)"
|
||||
[attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site">
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-icon name="fa-pencil-alt"></ion-icon>
|
||||
<ion-icon name="fas-pencil-alt"></ion-icon>
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 text-wrap>{{ 'core.login.yourenteredsite' | translate }}</h2>
|
||||
|
@ -98,7 +98,7 @@
|
|||
<ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl">
|
||||
<div class="ion-text-center ion-padding ion-margin-top">{{ 'core.login.or' | translate }}</div>
|
||||
<ion-button expand="block" color="light" class="ion-margin" lines="none" (click)="showInstructionsAndScanQR()">
|
||||
<ion-icon slot="start" name="fa-qrcode" aria-hidden="true"></ion-icon>
|
||||
<ion-icon slot="start" name="fas-qrcode" aria-hidden="true"></ion-icon>
|
||||
<ion-label>{{ 'core.scanqr' | translate }}</ion-label>
|
||||
</ion-button>
|
||||
</ng-container>
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
|
||||
<ion-buttons slot="end">
|
||||
<ion-button *ngIf="sites && sites.length > 0" (click)="toggleDelete()" [attr.aria-label]="'core.delete' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-pencil-alt"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-pencil-alt"></ion-icon>
|
||||
</ion-button>
|
||||
<ion-button router-direction="forward" routerLink="/settings"
|
||||
[attr.aria-label]="'core.settings.appsettings' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-cog"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-cog"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
@ -32,13 +32,13 @@
|
|||
<ion-badge slot="end" *ngIf="!showDelete && site.badge">{{site.badge}}</ion-badge>
|
||||
<ion-button *ngIf="showDelete" slot="end" fill="clear" color="danger" (click)="deleteSite($event, site)"
|
||||
[attr.aria-label]="'core.delete' | translate">
|
||||
<ion-icon name="fa-trash" slot="icon-only"></ion-icon>
|
||||
<ion-icon name="fas-trash" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end">
|
||||
<ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate">
|
||||
<ion-icon name="fa-plus"></ion-icon>
|
||||
<ion-icon name="fas-plus"></ion-icon>
|
||||
</ion-fab-button>
|
||||
</ion-fab>
|
||||
</ion-content>
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
<core-format-text [text]="siteName" contextLevel="system" [contextInstanceId]="0"></core-format-text>
|
||||
<img src="assets/img/login_logo.png" class="core-header-logo" [alt]="siteName">
|
||||
</ion-title>
|
||||
|
||||
<ion-buttons slot="end">
|
||||
<!-- @todo -->
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -12,10 +12,13 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.delegate';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreTabsComponent } from '@components/tabs/tabs';
|
||||
|
||||
/**
|
||||
* Page that displays the Home.
|
||||
|
@ -27,12 +30,16 @@ import { CoreHomeDelegate, CoreHomeHandlerToDisplay } from '../../services/home.
|
|||
})
|
||||
export class CoreHomePage implements OnInit {
|
||||
|
||||
@ViewChild(CoreTabsComponent) tabsComponent?: CoreTabsComponent;
|
||||
|
||||
|
||||
siteName!: string;
|
||||
tabs: CoreHomeHandlerToDisplay[] = [];
|
||||
loaded = false;
|
||||
selectedTab?: number;
|
||||
|
||||
protected subscription?: Subscription;
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected homeDelegate: CoreHomeDelegate,
|
||||
|
@ -47,6 +54,11 @@ export class CoreHomePage implements OnInit {
|
|||
this.subscription = this.homeDelegate.getHandlersObservable().subscribe((handlers) => {
|
||||
handlers && this.initHandlers(handlers);
|
||||
});
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.loadSiteName();
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,4 +103,18 @@ export class CoreHomePage implements OnInit {
|
|||
this.siteName = CoreSites.instance.getCurrentSite()!.getSiteName();
|
||||
}
|
||||
|
||||
/**
|
||||
* User entered the page.
|
||||
*/
|
||||
ionViewDidEnter(): void {
|
||||
this.tabsComponent?.ionViewDidEnter();
|
||||
}
|
||||
|
||||
/**
|
||||
* User left the page.
|
||||
*/
|
||||
ionViewDidLeave(): void {
|
||||
this.tabsComponent?.ionViewDidLeave();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button (ionTabButtonClick)="tabClicked($event, 'more')" [hidden]="!loaded" tab="more" layout="label-hide">
|
||||
<ion-icon name="fa-bars"></ion-icon>
|
||||
<ion-icon name="fas-bars"></ion-icon>
|
||||
<ion-label>{{ 'core.more' | translate }}</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
|
|
@ -51,34 +51,34 @@
|
|||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-item button *ngIf="showScanQR" (click)="scanQR()" detail>
|
||||
<ion-icon name="fa-qrcode" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon name="fas-qrcode" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.scanqr' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="showWeb && siteInfo" [href]="siteInfo.siteurl" core-link autoLogin="yes"
|
||||
title="{{ 'core.mainmenu.website' | translate }}" detail>
|
||||
<ion-icon name="globe" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon name="fas-globe" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.mainmenu.website' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="showHelp" [href]="docsUrl" core-link autoLogin="no"
|
||||
title="{{ 'core.mainmenu.help' | translate }}" detail>
|
||||
<ion-icon name="help-buoy" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon name="far-life-ring" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.mainmenu.help' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button router-direction="forward" routerLink="preferences"
|
||||
title="{{ 'core.settings.preferences' | translate }}" detail>
|
||||
<ion-icon name="fa-wrench" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-wrench" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.preferences' | translate }}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="logout()" title="{{ logoutLabel | translate }}" detail>
|
||||
<ion-icon name="log-out" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-icon name="fas-sign-out-alt" slot="start" aria-hidden="true"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ logoutLabel | translate }}</h2>
|
||||
</ion-label>
|
||||
|
@ -86,7 +86,7 @@
|
|||
<ion-item-divider></ion-item-divider>
|
||||
<ion-item button router-direction="forward" routerLink="settings"
|
||||
title="{{ 'core.settings.appsettings' | translate }}" detail>
|
||||
<ion-icon name="fa-cogs" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-cogs" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.settings.appsettings' | translate }}</h2>
|
||||
</ion-label>
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
</ion-button>
|
||||
</ion-item>
|
||||
<ion-list class="core-search-history" [hidden]="!historyShown">
|
||||
<ion-item class="ion-text-wrap" *ngFor="let item of history"
|
||||
(click)="historyClicked($event, item.searchedtext)" class="core-clickable" tabindex="1">
|
||||
<ion-icon name="fa-history" slot="start">
|
||||
<ion-item button class="ion-text-wrap" *ngFor="let item of history"
|
||||
(click)="historyClicked($event, item.searchedtext)" tabindex="1" detail>
|
||||
<ion-icon name="fas-history" slot="start">
|
||||
</ion-icon>
|
||||
{{item.searchedtext}}
|
||||
</ion-item>
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
<ion-label>{{ 'core.settings.opensourcelicenses' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" *ngIf="privacyPolicy" [href]="privacyPolicy" core-link auto-login="no" detail>
|
||||
<ion-icon name="fa-user-shield" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-user-shield" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.privacypolicy' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button class="ion-text-wrap" (click)="openPage('deviceinfo')" detail>
|
||||
<ion-icon name="fa-mobile" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-mobile" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.deviceinfo' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
|
|
@ -10,25 +10,25 @@
|
|||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-item button (click)="openSettings('general')" [class.core-split-item-selected]="'general' == selectedPage" detail>
|
||||
<ion-icon name="fa-wrench" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-wrench" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.general' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="openSettings('spaceusage')" [class.core-split-item-selected]="'spaceusage' == selectedPage"
|
||||
detail>
|
||||
<ion-icon name="fa-tasks" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-tasks" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.spaceusage' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="openSettings('sync')" [class.core-split-item-selected]="'sync' == selectedPage" detail>
|
||||
<ion-icon name="fa-sync-alt" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-sync-alt" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.synchronization' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button *ngIf="isIOS" (click)="openSettings('sharedfiles', {manage: true})"
|
||||
[class.core-split-item-selected]="'sharedfiles' == selectedPage" detail>
|
||||
<ion-icon name="fa-folder" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.sharedfiles.sharedfiles' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
<ion-item button (click)="openSettings('about')" [class.core-split-item-selected]="'about' == selectedPage" detail>
|
||||
<ion-icon name="fa-id-card" slot="start"></ion-icon>
|
||||
<ion-icon name="fas-id-card" slot="start"></ion-icon>
|
||||
<ion-label>{{ 'core.settings.about' | translate }}</ion-label>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="copyInfo()" [attr.aria-label]="'core.settings.copyinfo' | translate">
|
||||
<ion-icon slot="icon-only" name="fa-clipboard" color="light"></ion-icon>
|
||||
<ion-icon slot="icon-only" name="fas-clipboard" color="light"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<ion-item *ngIf="isIOS"
|
||||
(click)="openHandler('CoreSharedFilesListPage', {manage: true, siteId: siteId, hideSitePicker: true})"
|
||||
[title]="'core.sharedfiles.sharedfiles' | translate"
|
||||
[class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage" details>
|
||||
[class.core-split-item-selected]="'CoreSharedFilesListPage' == selectedPage" detail>
|
||||
<ion-icon name="fas-folder" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.sharedfiles.sharedfiles' | translate }}</h2>
|
||||
|
@ -37,7 +37,7 @@
|
|||
</ion-item>
|
||||
|
||||
<ion-item *ngFor="let handler of handlers" [ngClass]="['core-settings-handler', handler.class]"
|
||||
(click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" details
|
||||
(click)="openHandler(handler.page, handler.params)" [title]="handler.title | translate" detail
|
||||
[class.core-split-item-selected]="handler.page == selectedPage">
|
||||
<ion-icon [name]="handler.icon" slot="start" *ngIf="handler.icon">
|
||||
</ion-icon>
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
</ion-buttons>
|
||||
<ion-title>{{ 'core.settings.spaceusage' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- @todo <core-navbar-buttons></core-navbar-buttons>-->
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-navbar-buttons>
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -5,10 +5,11 @@
|
|||
</ion-buttons>
|
||||
<ion-title>{{ 'core.settings.synchronization' | translate }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<!-- @todo <core-navbar-buttons></core-navbar-buttons>-->
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-navbar-buttons>
|
||||
<ion-button (click)="showInfo()" [attr.aria-label]="'core.info' | translate">
|
||||
<ion-icon name="fas-info-circle" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
</core-navbar-buttons>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
|
|
@ -24,7 +24,7 @@ import { CoreConstants } from '@/core/constants';
|
|||
import { CoreConfig } from '@services/config';
|
||||
// import { CoreFilterProvider } from '@features/filter/providers/filter';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
// import { CoreCourseProvider } from '@features/course/providers/course';
|
||||
import { CoreCourse } from '@features/course/services/course';
|
||||
import { makeSingleton, Translate } from '@singletons/core.singletons';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
|
||||
|
@ -58,7 +58,6 @@ export class CoreSettingsHelperProvider {
|
|||
|
||||
constructor() {
|
||||
// protected filterProvider: CoreFilterProvider,
|
||||
// protected courseProvider: CoreCourseProvider,
|
||||
|
||||
if (!CoreConstants.CONFIG.forceColorScheme) {
|
||||
// Update color scheme when a user enters or leaves a site, or when the site info is updated.
|
||||
|
@ -116,7 +115,7 @@ export class CoreSettingsHelperProvider {
|
|||
promises.push(site.deleteFolder().then(() => {
|
||||
filepoolService.clearAllPackagesStatus(siteId);
|
||||
filepoolService.clearFilepool(siteId);
|
||||
// this.courseProvider.clearAllCoursesStatus(siteId);
|
||||
CoreCourse.instance.clearAllCoursesStatus(siteId);
|
||||
|
||||
siteInfo.spaceUsage = 0;
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"sitehome": "Site home",
|
||||
"sitenews": "Site announcements"
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<core-navbar-buttons slot="end">
|
||||
<ion-button *ngIf="searchEnabled" (click)="openSearch()" [attr.aria-label]="'core.courses.searchcourses' | translate">
|
||||
<ion-icon name="fas-search" slot="icon-only"></ion-icon>
|
||||
</ion-button>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="1000"
|
||||
[content]="'core.settings.showdownloadoptions' | translate" (action)="toggleDownload()"
|
||||
[iconAction]="downloadEnabledIcon"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="(downloadCourseEnabled || downloadCoursesEnabled)" [priority]="500"
|
||||
[content]="'addon.storagemanager.managestorage' | translate"
|
||||
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
|
||||
</core-context-menu>
|
||||
</core-navbar-buttons>
|
||||
<ion-content>
|
||||
<ion-refresher slot="fixed" [disabled]="!dataLoaded"
|
||||
(ionRefresh)="doRefresh($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<!-- @todo <core-block-course-blocks [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled">-->
|
||||
<core-loading [hideUntil]="dataLoaded">
|
||||
<ion-list>
|
||||
<!-- Site home main contents. -->
|
||||
<!-- @todo <ng-container *ngIf="section && section.hasContent">
|
||||
<ion-item class="ion-text-wrap" *ngIf="section.summary">
|
||||
<core-format-text [text]="section.summary" contextLevel="course" [contextInstanceId]="siteHomeId"></core-format-text>
|
||||
</ion-item>
|
||||
|
||||
<core-course-module *ngFor="let module of section.modules" [module]="module" [courseId]="siteHomeId" [downloadEnabled]="downloadEnabled" [section]="section"></core-course-module>
|
||||
</ng-container> -->
|
||||
|
||||
<!-- Site home items: news, categories, courses, etc. -->
|
||||
<ng-container *ngIf="items.length > 0">
|
||||
<ion-item-divider *ngIf="section && section!.hasContent"></ion-item-divider>
|
||||
<ng-container *ngFor="let item of items">
|
||||
<ng-container [ngSwitch]="item">
|
||||
<ng-container *ngSwitchCase="'LIST_OF_COURSE'">
|
||||
<ng-template *ngTemplateOutlet="allCourseList"></ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'LIST_OF_CATEGORIES'">
|
||||
<ng-template *ngTemplateOutlet="categories"></ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'COURSE_SEARCH_BOX'">
|
||||
<ng-template *ngTemplateOutlet="courseSearch"></ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'ENROLLED_COURSES'">
|
||||
<ng-template *ngTemplateOutlet="enrolledCourseList"></ng-template>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'NEWS_ITEMS'">
|
||||
<ng-template *ngTemplateOutlet="news"></ng-template>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ion-list>
|
||||
<core-empty-box *ngIf="!hasContent" icon="qr-scanner" [message]="'core.course.nocontentavailable' | translate">
|
||||
|
||||
</core-empty-box>
|
||||
</core-loading>
|
||||
<!-- @todo </core-block-course-blocks> -->
|
||||
</ion-content>
|
||||
|
||||
<ng-template #allCourseList>
|
||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/all" detail>
|
||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.availablecourses' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #news>
|
||||
<ion-item>
|
||||
<ion-label>News (TODO)</ion-label>
|
||||
</ion-item>
|
||||
<!-- @todo <core-course-module class="core-sitehome-news" *ngIf="show" [module]="module" [courseId]="siteHomeId"></core-course-module> -->
|
||||
</ng-template>
|
||||
|
||||
<ng-template #categories>
|
||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/categories" detail>
|
||||
<ion-icon name="folder" slot="start"></ion-icon>
|
||||
<ion-label>
|
||||
<h2>{{ 'core.courses.categories' | translate}}</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #enrolledCourseList>
|
||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/my" detail>
|
||||
<ion-icon name="fas-graduation-cap" fixed-width slot="start">
|
||||
</ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.mycourses' | translate}}</h2></ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #courseSearch>
|
||||
<ion-item button class="ion-text-wrap" router-direction="forward" routerLink="/courses/search" detail>
|
||||
<ion-icon name="fas-search" slot="start"></ion-icon>
|
||||
<ion-label><h2>{{ 'core.courses.searchcourses' | translate}}</h2></ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
|
@ -0,0 +1,47 @@
|
|||
// (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 { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
|
||||
import { CoreSiteHomeIndexPage } from './index.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: CoreSiteHomeIndexPage,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild(routes),
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreDirectivesModule,
|
||||
CoreComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreSiteHomeIndexPage,
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class CoreSiteHomeIndexPageModule {}
|
|
@ -0,0 +1,214 @@
|
|||
// (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, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { IonRefresher, NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreSite, CoreSiteConfig } from '@classes/site';
|
||||
import { CoreCourse, CoreCourseSection } from '@features/course/services/course';
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
|
||||
import { CoreCourses, CoreCoursesProvider } from '@features//courses/services/courses';
|
||||
import { CoreEventObserver, CoreEvents } from '@singletons/events';
|
||||
import { CoreCourseHelper } from '@features/course/services/course.helper';
|
||||
|
||||
/**
|
||||
* Page that displays site home index.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'page-core-sitehome-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
|
||||
|
||||
// @todo @ViewChild(CoreBlockCourseBlocksComponent) courseBlocksComponent: CoreBlockCourseBlocksComponent;
|
||||
|
||||
dataLoaded = false;
|
||||
section?: CoreCourseSection & {
|
||||
hasContent?: boolean;
|
||||
};
|
||||
|
||||
hasContent = false;
|
||||
items: string[] = [];
|
||||
siteHomeId?: number;
|
||||
currentSite?: CoreSite;
|
||||
searchEnabled = false;
|
||||
downloadEnabled = false;
|
||||
downloadCourseEnabled = false;
|
||||
downloadCoursesEnabled = false;
|
||||
downloadEnabledIcon = 'far-square';
|
||||
|
||||
protected updateSiteObserver?: CoreEventObserver;
|
||||
|
||||
constructor(
|
||||
protected route: ActivatedRoute,
|
||||
protected navCtrl: NavController,
|
||||
// @todo private prefetchDelegate: CoreCourseModulePrefetchDelegate,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Page being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
const navParams = this.route.snapshot.queryParams;
|
||||
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
// Refresh the enabled flags if site is updated.
|
||||
this.updateSiteObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
|
||||
this.searchEnabled = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
this.downloadCourseEnabled = !CoreCourses.instance.isDownloadCourseDisabledInSite();
|
||||
this.downloadCoursesEnabled = !CoreCourses.instance.isDownloadCoursesDisabledInSite();
|
||||
|
||||
this.switchDownload(this.downloadEnabled && this.downloadCourseEnabled && this.downloadCoursesEnabled);
|
||||
}, CoreSites.instance.getCurrentSiteId());
|
||||
|
||||
this.currentSite = CoreSites.instance.getCurrentSite()!;
|
||||
this.siteHomeId = this.currentSite.getSiteHomeId();
|
||||
|
||||
const module = navParams['module'];
|
||||
if (module) {
|
||||
// @todo const modParams = navParams.get('modParams');
|
||||
// CoreCourseHelper.instance.openModule(module, this.siteHomeId, undefined, modParams);
|
||||
}
|
||||
|
||||
this.loadContent().finally(() => {
|
||||
this.dataLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to fetch the data.
|
||||
*
|
||||
* @return Promise resolved when done.
|
||||
*/
|
||||
protected async loadContent(): Promise<void> {
|
||||
this.hasContent = false;
|
||||
|
||||
const config = this.currentSite!.getStoredConfig() || { numsections: 1, frontpageloggedin: undefined };
|
||||
|
||||
this.items = await CoreSiteHome.instance.getFrontPageItems(config.frontpageloggedin);
|
||||
this.hasContent = this.items.length > 0;
|
||||
|
||||
try {
|
||||
const sections = await CoreCourse.instance.getSections(this.siteHomeId!, false, true);
|
||||
|
||||
// Check "Include a topic section" setting from numsections.
|
||||
this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
|
||||
if (this.section) {
|
||||
this.section.hasContent = false;
|
||||
this.section.hasContent = CoreCourseHelper.instance.sectionHasContent(this.section);
|
||||
/* @todo this.hasContent = CoreCourseHelper.instance.addHandlerDataForModules(
|
||||
[this.section],
|
||||
this.siteHomeId,
|
||||
undefined,
|
||||
undefined,
|
||||
true,
|
||||
) || this.hasContent;*/
|
||||
}
|
||||
|
||||
// Add log in Moodle.
|
||||
CoreCourse.instance.logView(
|
||||
this.siteHomeId!,
|
||||
undefined,
|
||||
undefined,
|
||||
this.currentSite!.getInfo()?.sitename,
|
||||
).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
} catch (error) {
|
||||
CoreDomUtils.instance.showErrorModalDefault(error, 'core.course.couldnotloadsectioncontent', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the data.
|
||||
*
|
||||
* @param refresher Refresher.
|
||||
*/
|
||||
doRefresh(refresher?: CustomEvent<IonRefresher>): void {
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
promises.push(CoreCourse.instance.invalidateSections(this.siteHomeId!));
|
||||
promises.push(this.currentSite!.invalidateConfig().then(async () => {
|
||||
// Config invalidated, fetch it again.
|
||||
const config: CoreSiteConfig = await this.currentSite!.getConfig();
|
||||
this.currentSite!.setConfig(config);
|
||||
|
||||
return;
|
||||
}));
|
||||
|
||||
if (this.section && this.section.modules) {
|
||||
// Invalidate modules prefetch data.
|
||||
// @todo promises.push(this.prefetchDelegate.invalidateModules(this.section.modules, this.siteHomeId));
|
||||
}
|
||||
|
||||
// @todo promises.push(this.courseBlocksComponent.invalidateBlocks());
|
||||
|
||||
Promise.all(promises).finally(async () => {
|
||||
const p2: Promise<unknown>[] = [];
|
||||
|
||||
p2.push(this.loadContent());
|
||||
// @todo p2.push(this.courseBlocksComponent.loadContent());
|
||||
|
||||
await Promise.all(p2).finally(() => {
|
||||
refresher?.detail.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle download enabled.
|
||||
*/
|
||||
toggleDownload(): void {
|
||||
this.switchDownload(!this.downloadEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to switch download enabled.
|
||||
*
|
||||
* @param enable If enable or disable.
|
||||
*/
|
||||
protected switchDownload(enable: boolean): void {
|
||||
this.downloadEnabled = (this.downloadCourseEnabled || this.downloadCoursesEnabled) && enable;
|
||||
this.downloadEnabledIcon = this.downloadEnabled ? 'far-check-square' : 'far-square';
|
||||
CoreEvents.trigger(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED, { enabled: this.downloadEnabled });
|
||||
}
|
||||
|
||||
/**
|
||||
* Open page to manage courses storage.
|
||||
*/
|
||||
manageCoursesStorage(): void {
|
||||
// @todo this.navCtrl.navigateForward(['/courses/storage']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Go to search courses.
|
||||
*/
|
||||
openSearch(): void {
|
||||
this.navCtrl.navigateForward(['/courses/search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being destroyed.
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.updateSiteObserver?.off();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
// (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 { Params } from '@angular/router';
|
||||
import { CoreSites } from '@services/sites';
|
||||
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
|
||||
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks.helper';
|
||||
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks.delegate';
|
||||
import { CoreSiteHome } from '../sitehome';
|
||||
|
||||
/**
|
||||
* Handler to treat links to site home index.
|
||||
*/
|
||||
Injectable();
|
||||
export class CoreSiteHomeIndexLinkHandler extends CoreContentLinksHandlerBase {
|
||||
|
||||
name = 'CoreSiteHomeIndexLinkHandler';
|
||||
featureName = 'CoreMainMenuDelegate_CoreSiteHome';
|
||||
pattern = /\/course\/view\.php.*([?&]id=\d+)/;
|
||||
|
||||
/**
|
||||
* Get the list of actions for a link (url).
|
||||
*
|
||||
* @param siteIds List of sites the URL belongs to.
|
||||
* @param url The URL to treat.
|
||||
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param courseId Course ID related to the URL. Optional but recommended.
|
||||
* @return List of (or promise resolved with list of) actions.
|
||||
*/
|
||||
getActions(): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
|
||||
return [{
|
||||
action: (siteId: string): void => {
|
||||
CoreContentLinksHelper.instance.goInSite('sitehome', [], siteId);
|
||||
},
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled for a certain site (site + user) and a URL.
|
||||
* If not defined, defaults to true.
|
||||
*
|
||||
* @param siteId The site ID.
|
||||
* @param url The URL to treat.
|
||||
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
|
||||
* @param courseId Course ID related to the URL. Optional but recommended.
|
||||
* @return Whether the handler is enabled for the URL and site.
|
||||
*/
|
||||
async isEnabled(siteId: string, url: string, params: Params, courseId?: number): Promise<boolean> {
|
||||
courseId = parseInt(params.id, 10);
|
||||
if (!courseId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
if (courseId != site.getSiteHomeId()) {
|
||||
// The course is not site home.
|
||||
return false;
|
||||
}
|
||||
|
||||
return CoreSiteHome.instance.isAvailable(siteId).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// (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 { CoreSites } from '@services/sites';
|
||||
import { CoreHomeHandler, CoreHomeHandlerToDisplay } from '@features/mainmenu/services/home.delegate';
|
||||
import { CoreSiteHome } from '../sitehome';
|
||||
|
||||
/**
|
||||
* Handler to add site home into home page.
|
||||
*/
|
||||
Injectable();
|
||||
export class CoreSiteHomeHomeHandler implements CoreHomeHandler {
|
||||
|
||||
name = 'CoreSiteHomeDashboard';
|
||||
priority = 1200;
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): Promise<boolean> {
|
||||
return this.isEnabledForSite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a certain site.
|
||||
*
|
||||
* @param siteId Site ID. If not defined, current site.
|
||||
* @return Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
async isEnabledForSite(siteId?: string): Promise<boolean> {
|
||||
return CoreSiteHome.instance.isAvailable(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data needed to render the handler.
|
||||
*
|
||||
* @return Data needed to render the handler.
|
||||
*/
|
||||
getDisplayData(): CoreHomeHandlerToDisplay {
|
||||
const site = CoreSites.instance.getCurrentSite();
|
||||
const displaySiteHome = site?.getInfo() && site?.getInfo()?.userhomepage === 0;
|
||||
|
||||
return {
|
||||
title: 'core.sitehome.sitehome',
|
||||
page: 'sitehome',
|
||||
class: 'core-sitehome-dashboard-handler',
|
||||
icon: 'fas-home',
|
||||
selectPriority: displaySiteHome ? 1100 : 900,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
// (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 { CoreSites } from '@services/sites';
|
||||
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||
import { makeSingleton } from '@singletons/core.singletons';
|
||||
import { CoreCourse, CoreCourseSection } from '../../course/services/course';
|
||||
import { CoreCourses } from '../../courses/services/courses';
|
||||
|
||||
/**
|
||||
* Items with index 1 and 3 were removed on 2.5 and not being supported in the app.
|
||||
*/
|
||||
export enum FrontPageItemNames {
|
||||
NEWS_ITEMS = 0,
|
||||
LIST_OF_CATEGORIES = 2,
|
||||
COMBO_LIST = 3,
|
||||
ENROLLED_COURSES = 5,
|
||||
LIST_OF_COURSE = 6,
|
||||
COURSE_SEARCH_BOX = 7,
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding site home.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreSiteHomeProvider {
|
||||
|
||||
/**
|
||||
* Get the news forum for the Site Home.
|
||||
*
|
||||
* @param siteHomeId Site Home ID.
|
||||
* @return Promise resolved with the forum if found, rejected otherwise.
|
||||
*/
|
||||
getNewsForum(): void {
|
||||
// @todo params and logic.
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the WS call to get the news forum for the Site Home.
|
||||
*
|
||||
* @param siteHomeId Site Home ID.
|
||||
* @return Promise resolved when invalidated.
|
||||
*/
|
||||
invalidateNewsForum(): void {
|
||||
// @todo params and logic.
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the frontpage is available for the current site.
|
||||
*
|
||||
* @param siteId The site ID. If not defined, current site.
|
||||
* @return Promise resolved with boolean: whether it's available.
|
||||
*/
|
||||
async isAvailable(siteId?: string): Promise<boolean> {
|
||||
try {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
// First check if it's disabled.
|
||||
if (this.isDisabledInSite(site)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use a WS call to check if there's content in the site home.
|
||||
const siteHomeId = site.getSiteHomeId();
|
||||
const preSets: CoreSiteWSPreSets = { emergencyCache: false };
|
||||
|
||||
try {
|
||||
const sections: CoreCourseSection[] =
|
||||
await CoreCourse.instance.getSections(siteHomeId, false, true, preSets, site.id);
|
||||
|
||||
if (!sections || !sections.length) {
|
||||
throw Error('No sections found');
|
||||
}
|
||||
|
||||
const hasContent = sections.some((section) => section.summary || (section.modules && section.modules.length));
|
||||
|
||||
if (hasContent) {
|
||||
// There's a section with content.
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
const config = site.getStoredConfig();
|
||||
if (config && config.frontpageloggedin) {
|
||||
const items = await this.getFrontPageItems(config.frontpageloggedin);
|
||||
|
||||
// There are items to show.
|
||||
return items.length > 0;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Site Home is disabled in a certain site.
|
||||
*
|
||||
* @param siteId Site Id. If not defined, use current site.
|
||||
* @return Promise resolved with true if disabled, rejected or resolved with false otherwise.
|
||||
*/
|
||||
async isDisabled(siteId?: string): Promise<boolean> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
return this.isDisabledInSite(site);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Site Home is disabled in a certain site.
|
||||
*
|
||||
* @param site Site. If not defined, use current site.
|
||||
* @return Whether it's disabled.
|
||||
*/
|
||||
isDisabledInSite(site: CoreSite): boolean {
|
||||
site = site || CoreSites.instance.getCurrentSite();
|
||||
|
||||
return site.isFeatureDisabled('CoreMainMenuDelegate_CoreSiteHome');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the nams of the valid frontpage items.
|
||||
*
|
||||
* @param frontpageItemIds CSV string with indexes of site home components.
|
||||
* @return Valid names for each item.
|
||||
*/
|
||||
async getFrontPageItems(frontpageItemIds?: string): Promise<string[]> {
|
||||
if (!frontpageItemIds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items = frontpageItemIds.split(',');
|
||||
|
||||
const filteredItems: string[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
let itemNumber = parseInt(item, 10);
|
||||
|
||||
let add = false;
|
||||
switch (itemNumber) {
|
||||
case FrontPageItemNames['NEWS_ITEMS']:
|
||||
// @todo
|
||||
add = true;
|
||||
break;
|
||||
case FrontPageItemNames['LIST_OF_CATEGORIES']:
|
||||
case FrontPageItemNames['COMBO_LIST']:
|
||||
case FrontPageItemNames['LIST_OF_COURSE']:
|
||||
add = CoreCourses.instance.isGetCoursesByFieldAvailable();
|
||||
if (add && itemNumber == FrontPageItemNames['COMBO_LIST']) {
|
||||
itemNumber = FrontPageItemNames['LIST_OF_CATEGORIES'];
|
||||
}
|
||||
break;
|
||||
case FrontPageItemNames['ENROLLED_COURSES']:
|
||||
if (!CoreCourses.instance.isMyCoursesDisabledInSite()) {
|
||||
const courses = await CoreCourses.instance.getUserCourses();
|
||||
|
||||
add = courses.length > 0;
|
||||
}
|
||||
break;
|
||||
case FrontPageItemNames['COURSE_SEARCH_BOX']:
|
||||
add = !CoreCourses.instance.isSearchCoursesDisabledInSite();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Do not add an item twice.
|
||||
if (add && filteredItems.indexOf(FrontPageItemNames[itemNumber]) < 0) {
|
||||
filteredItems.push(FrontPageItemNames[itemNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CoreSiteHome extends makeSingleton(CoreSiteHomeProvider) {}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
// (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 { Routes } from '@angular/router';
|
||||
|
||||
import { CoreSiteHomeIndexLinkHandler } from './services/handlers/index.link';
|
||||
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks.delegate';
|
||||
import { CoreSiteHomeHomeHandler } from './services/handlers/sitehome.home';
|
||||
import { CoreHomeDelegate } from '@features/mainmenu/services/home.delegate';
|
||||
import { CoreHomeRoutingModule } from '@features/mainmenu/pages/home/home-routing.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'sitehome',
|
||||
loadChildren: () =>
|
||||
import('@features/sitehome/pages/index/index.page.module').then(m => m.CoreSiteHomeIndexPageModule),
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [CoreHomeRoutingModule.forChild(routes)],
|
||||
exports: [CoreHomeRoutingModule],
|
||||
providers: [
|
||||
CoreSiteHomeIndexLinkHandler,
|
||||
CoreSiteHomeHomeHandler,
|
||||
],
|
||||
})
|
||||
export class CoreSiteHomeInitModule {
|
||||
|
||||
constructor(
|
||||
contentLinksDelegate: CoreContentLinksDelegate,
|
||||
homeDelegate: CoreHomeDelegate,
|
||||
siteHomeIndexLinkHandler: CoreSiteHomeIndexLinkHandler,
|
||||
siteHomeDashboardHandler: CoreSiteHomeHomeHandler,
|
||||
) {
|
||||
contentLinksDelegate.registerHandler(siteHomeIndexLinkHandler);
|
||||
homeDelegate.registerHandler(siteHomeDashboardHandler);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,8 @@ import { makeSingleton, Translate } from '@singletons/core.singletons';
|
|||
import { CoreWSExternalWarning } from '@services/ws';
|
||||
import { CoreCourseBase } from '@/types/global';
|
||||
|
||||
const ROOT_CACHE_KEY = 'mmGroups:';
|
||||
|
||||
/*
|
||||
* Service to handle groups.
|
||||
*/
|
||||
|
@ -31,7 +33,6 @@ export class CoreGroupsProvider {
|
|||
static readonly NOGROUPS = 0;
|
||||
static readonly SEPARATEGROUPS = 1;
|
||||
static readonly VISIBLEGROUPS = 2;
|
||||
protected readonly ROOT_CACHE_KEY = 'mmGroups:';
|
||||
|
||||
/**
|
||||
* Check if group mode of an activity is enabled.
|
||||
|
@ -65,12 +66,12 @@ export class CoreGroupsProvider {
|
|||
userId?: number,
|
||||
siteId?: string,
|
||||
ignoreCache?: boolean,
|
||||
): Promise<CoreGroupGetActivityAllowedGroupsResponse> {
|
||||
): Promise<CoreGroupGetActivityAllowedGroupsWSResponse> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
const params = {
|
||||
const params: CoreGroupGetActivityAllowedGroupsWSParams = {
|
||||
cmid: cmId,
|
||||
userid: userId,
|
||||
};
|
||||
|
@ -84,7 +85,7 @@ export class CoreGroupsProvider {
|
|||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
const response: CoreGroupGetActivityAllowedGroupsResponse =
|
||||
const response: CoreGroupGetActivityAllowedGroupsWSResponse =
|
||||
await site.read('core_group_get_activity_allowed_groups', params, preSets);
|
||||
|
||||
if (!response || !response.groups) {
|
||||
|
@ -102,7 +103,7 @@ export class CoreGroupsProvider {
|
|||
* @return Cache key.
|
||||
*/
|
||||
protected getActivityAllowedGroupsCacheKey(cmId: number, userId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId;
|
||||
return ROOT_CACHE_KEY + 'allowedgroups:' + cmId + ':' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +116,7 @@ export class CoreGroupsProvider {
|
|||
* @return Promise resolved when the groups are retrieved. If not allowed, empty array will be returned.
|
||||
*/
|
||||
async getActivityAllowedGroupsIfEnabled(cmId: number, userId?: number, siteId?: string, ignoreCache?: boolean):
|
||||
Promise<CoreGroupGetActivityAllowedGroupsResponse> {
|
||||
Promise<CoreGroupGetActivityAllowedGroupsWSResponse> {
|
||||
siteId = siteId || CoreSites.instance.getCurrentSiteId();
|
||||
|
||||
// Get real groupmode, in case it's forced by the course.
|
||||
|
@ -157,7 +158,7 @@ export class CoreGroupsProvider {
|
|||
groupInfo.separateGroups = groupMode === CoreGroupsProvider.SEPARATEGROUPS;
|
||||
groupInfo.visibleGroups = groupMode === CoreGroupsProvider.VISIBLEGROUPS;
|
||||
|
||||
let result: CoreGroupGetActivityAllowedGroupsResponse;
|
||||
let result: CoreGroupGetActivityAllowedGroupsWSResponse;
|
||||
if (groupInfo.separateGroups || groupInfo.visibleGroups) {
|
||||
result = await this.getActivityAllowedGroups(cmId, userId, siteId, ignoreCache);
|
||||
} else {
|
||||
|
@ -195,7 +196,7 @@ export class CoreGroupsProvider {
|
|||
*/
|
||||
async getActivityGroupMode(cmId: number, siteId?: string, ignoreCache?: boolean): Promise<number> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
const params = {
|
||||
const params: CoreGroupGetActivityGroupmodeWSParams = {
|
||||
cmid: cmId,
|
||||
};
|
||||
const preSets: CoreSiteWSPreSets = {
|
||||
|
@ -208,7 +209,7 @@ export class CoreGroupsProvider {
|
|||
preSets.emergencyCache = false;
|
||||
}
|
||||
|
||||
const response: CoreGroupGetActivityGroupModeResponse =
|
||||
const response: CoreGroupGetActivityGroupModeWSResponse =
|
||||
await site.read('core_group_get_activity_groupmode', params, preSets);
|
||||
|
||||
if (!response || typeof response.groupmode == 'undefined') {
|
||||
|
@ -225,7 +226,7 @@ export class CoreGroupsProvider {
|
|||
* @return Cache key.
|
||||
*/
|
||||
protected getActivityGroupModeCacheKey(cmId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'groupmode:' + cmId;
|
||||
return ROOT_CACHE_KEY + 'groupmode:' + cmId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -274,7 +275,7 @@ export class CoreGroupsProvider {
|
|||
async getUserGroupsInCourse(courseId: number, siteId?: string, userId?: number): Promise<CoreGroup[]> {
|
||||
const site = await CoreSites.instance.getSite(siteId);
|
||||
userId = userId || site.getUserId();
|
||||
const data = {
|
||||
const data: CoreGroupGetCourseUserGroupsWSParams = {
|
||||
userid: userId,
|
||||
courseid: courseId,
|
||||
};
|
||||
|
@ -283,7 +284,7 @@ export class CoreGroupsProvider {
|
|||
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||
};
|
||||
|
||||
const response: CoreGroupGetCourseUserGroupsResponse =
|
||||
const response: CoreGroupGetCourseUserGroupsWSResponse =
|
||||
await site.read('core_group_get_course_user_groups', data, preSets);
|
||||
|
||||
if (!response || !response.groups) {
|
||||
|
@ -299,7 +300,7 @@ export class CoreGroupsProvider {
|
|||
* @return Prefix Cache key.
|
||||
*/
|
||||
protected getUserGroupsInCoursePrefixCacheKey(): string {
|
||||
return this.ROOT_CACHE_KEY + 'courseGroups:';
|
||||
return ROOT_CACHE_KEY + 'courseGroups:';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -474,24 +475,48 @@ export type CoreGroupInfo = {
|
|||
/**
|
||||
* WS core_group_get_activity_allowed_groups response type.
|
||||
*/
|
||||
export type CoreGroupGetActivityAllowedGroupsResponse = {
|
||||
export type CoreGroupGetActivityAllowedGroupsWSResponse = {
|
||||
groups: CoreGroup[]; // List of groups.
|
||||
canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_group_get_activity_groupmode WS.
|
||||
*/
|
||||
type CoreGroupGetActivityGroupmodeWSParams = {
|
||||
cmid: number; // Course module id.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of WS core_group_get_activity_groupmode.
|
||||
*/
|
||||
export type CoreGroupGetActivityGroupModeResponse = {
|
||||
export type CoreGroupGetActivityGroupModeWSResponse = {
|
||||
groupmode: number; // Group mode: 0 for no groups, 1 for separate groups, 2 for visible groups.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_group_get_activity_allowed_groups WS.
|
||||
*/
|
||||
type CoreGroupGetActivityAllowedGroupsWSParams = {
|
||||
cmid: number; // Course module id.
|
||||
userid?: number; // Id of user, empty for current user.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params of core_group_get_course_user_groups WS.
|
||||
*/
|
||||
type CoreGroupGetCourseUserGroupsWSParams = {
|
||||
courseid?: number; // Id of course (empty or 0 for all the courses where the user is enrolled).
|
||||
userid?: number; // Id of user (empty or 0 for current user).
|
||||
groupingid?: number; // Returns only groups in the specified grouping.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of WS core_group_get_course_user_groups.
|
||||
*/
|
||||
export type CoreGroupGetCourseUserGroupsResponse = {
|
||||
export type CoreGroupGetCourseUserGroupsWSResponse = {
|
||||
groups: {
|
||||
id: number; // Group record id.
|
||||
name: string; // Multilang compatible name, course unique.
|
||||
|
|
|
@ -990,6 +990,13 @@ export type CoreStatusWithWarningsWSResponse = {
|
|||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Special response structure of many webservices that contains only warnings.
|
||||
*/
|
||||
export type CoreWarningsWSResponse = {
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Structure of files returned by WS.
|
||||
*/
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue