MOBILE-3931 module: Adapt module site plugins code

main
Pau Ferrer Ocaña 2022-02-16 15:32:09 +01:00
parent 16cee9df14
commit 6c782e3b3e
7 changed files with 145 additions and 214 deletions

View File

@ -58,7 +58,6 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
hasOffline = false; // Resources don't have any data to sync.
description?: string; // Module description.
isDestroyed = false; // Whether the component is destroyed.
protected fetchContentDefaultError = 'core.course.errorgetmodule'; // Default error to show when loading contents.
protected isCurrentView = false; // Whether the component is in the current view.
@ -72,6 +71,7 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
protected debouncedUpdateModule?: () => void; // Update the module after a certain time.
protected showCompletion = false; // Whether to show completion inside the activity.
protected displayDescription = true; // Wether to show Module description on module page, and not on summary or the contrary.
protected isDestroyed = false; // Whether the component is destroyed.
constructor(
@Optional() @Inject('') loggerName: string = 'CoreCourseModuleMainResourceComponent',
@ -111,11 +111,10 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
* Refresh the data.
*
* @param refresher Refresher.
* @param done Function to call when done. Never used.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: IonRefresher | null, done?: () => void, showErrors: boolean = false): Promise<void> {
async doRefresh(refresher?: IonRefresher | null, showErrors = false): Promise<void> {
if (!this.loaded || !this.module) {
// Module can be undefined if course format changes from single activity to weekly/topics.
return;
@ -405,10 +404,14 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
componentProps: {
moduleId: this.module.id,
module: this.module,
description: !this.displayDescription ? this.description : '',
description: this.description,
component: this.component,
courseId: this.courseId,
hasOffline: this.hasOffline,
displayOptions: {
// Show description on summary if not shown on the page.
displayDescription: !this.displayDescription,
},
},
});
@ -421,11 +424,11 @@ export class CoreCourseModuleMainResourceComponent implements OnInit, OnDestroy,
} finally {
modal.dismiss();
}
} else if(data.action == 'sync') {
} else if (data.action == 'sync') {
const modal = await CoreDomUtils.showModalLoading();
try {
await this.doRefresh( undefined, undefined, true);
await this.doRefresh( undefined, true);
} finally {
modal.dismiss();
}

View File

@ -19,12 +19,12 @@
</core-format-text>
</h1>
</ion-label>
<ion-button fill="clear" [href]="externalUrl" core-link [showBrowserWarning]="false" color="dark"
[attr.aria-label]="'core.openinbrowser' | translate" slot="end">
<ion-button fill="clear" *ngIf="displayOptions.displayOpenInBrowser" [href]="externalUrl" core-link [showBrowserWarning]="false"
color="dark" [attr.aria-label]="'core.openinbrowser' | translate" slot="end">
<ion-icon name="fas-external-link-alt" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="module && description">
<ion-item class="ion-text-wrap" *ngIf="module && description && displayOptions.displayDescription">
<ion-label>
<core-format-text [text]="description" [component]="component" [componentId]="componentId" contextLevel="module"
[contextInstanceId]="module.id" [courseId]="courseId" [maxHeight]="120">
@ -32,7 +32,7 @@
</ion-label>
</ion-item>
<ion-item button class="ion-margin" *ngIf="prefetchText" class="ion-text-wrap">
<ion-item button class="ion-margin" *ngIf="prefetchText && displayOptions.displayPrefetch" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ prefetchText }}</p>
<p *ngIf="downloadTimeReadable">{{ downloadTimeReadable }}</p>
@ -45,7 +45,7 @@
<ion-spinner *ngIf="prefetchStatusIcon == 'spinner'" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-item button class="ion-margin" *ngIf="sizeReadable" class="ion-text-wrap">
<ion-item button class="ion-margin" *ngIf="sizeReadable && displayOptions.displaySize" class="ion-text-wrap">
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.totalspaceusage' | translate }}</p>
<ion-badge color="light">{{ sizeReadable | coreBytesToSize }}</ion-badge>
@ -57,7 +57,7 @@
<ion-spinner *ngIf="removeFilesLoading" slot="end" aria-hidden="true"></ion-spinner>
</ion-item>
<ion-button *ngIf="blog" class="ion-margin" (click)="gotoBlog()" expand="block" fill="outline">
<ion-button *ngIf="blog && displayOptions.displayBlog" class="ion-margin" (click)="gotoBlog()" expand="block" fill="outline">
<ion-icon name="far-newspaper" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
{{ 'addon.blog.blog' | translate }}
@ -65,7 +65,7 @@
</ion-button>
</core-loading>
</ion-content>
<ion-footer *ngIf="loaded && isOnline">
<ion-footer *ngIf="loaded && isOnline && displayOptions.displayRefresh">
<ion-button class="ion-margin" *ngIf="!hasOffline" (click)="refresh()" expand="block">
<ion-icon name="fas-redo-alt" slot="start" aria-hidden="true"></ion-icon>
<ion-label>

View File

@ -46,6 +46,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
@Input() component = ''; // Component name.
@Input() description = ''; // Module description.
@Input() hasOffline = false; // If it has offline data to be synced.
@Input() displayOptions: CoreCourseModuleSummaryDisplayOptions = {};
loaded = false; // If the component has been loaded.
componentId?: number; // Component ID.
@ -95,6 +96,15 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
return;
}
this.displayOptions = Object.assign({
displayOpenInBrowser: true,
displayDescription: true,
displayRefresh: true,
displayPrefetch: true,
displaySize: true,
displayBlog: true,
}, this.displayOptions);
this.fetchContent();
if (this.component) {
@ -306,6 +316,15 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy {
}
export type CoreCourseModuleSummaryResult = {
export type CoreCourseModuleSummaryResult = {
action: 'sync'|'refresh';
};
export type CoreCourseModuleSummaryDisplayOptions = {
displayOpenInBrowser?: boolean;
displayDescription?: boolean;
displayRefresh?: boolean;
displayPrefetch?: boolean;
displaySize?: boolean;
displayBlog?: boolean;
};

View File

@ -64,7 +64,6 @@ import { CoreFile } from '@services/file';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
import { CoreNetworkError } from '@classes/errors/network-error';
import { CoreSiteHome } from '@features/sitehome/services/sitehome';
@ -474,23 +473,18 @@ export class CoreCourseHelperProvider {
*
* @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.
* @deprecated since 4.0
*/
async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number, done?: () => void): Promise<void> {
async confirmAndRemoveFiles(module: CoreCourseModuleData, courseId: number): Promise<void> {
let modal: CoreIonLoadingElement | undefined;
try {
await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: module.name });
modal = await CoreDomUtils.showModalLoading();
await this.removeModuleStoredData(module, courseId);
done && done();
} catch (error) {
if (error) {
CoreDomUtils.showErrorModal(error);
@ -555,45 +549,6 @@ export class CoreCourseHelperProvider {
await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm);
}
/**
* 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.
* @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.
* @deprecated since 4.0
*/
async contextMenuPrefetch(
instance: ComponentWithContextMenu,
module: CoreCourseModuleData,
courseId: number,
done?: () => void,
): Promise<void> {
const initialIcon = instance.prefetchStatusIcon;
instance.prefetchStatusIcon = CoreConstants.ICON_DOWNLOADING; // Show spinner since this operation might take a while.
try {
// We need to call getDownloadSize, the package might have been updated.
const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(module, courseId, true);
await CoreDomUtils.confirmDownloadSize(size);
await CoreCourseModulePrefetchDelegate.prefetchModule(module, courseId, true);
// Success, close menu.
done && done();
} catch (error) {
instance.prefetchStatusIcon = initialIcon;
if (!instance.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}
}
}
/**
* Check whether a course is accessed using guest access.
*
@ -1030,87 +985,6 @@ export class CoreCourseHelperProvider {
await CoreFilepool.downloadOrPrefetchFiles(siteId, files, false, false, component, componentId);
}
/**
* 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.
*/
async fillContextMenu(
instance: ComponentWithContextMenu,
module: CoreCourseModuleData,
courseId: number,
invalidateCache?: boolean,
component?: string,
): Promise<void> {
const siteId = CoreSites.getCurrentSiteId();
const moduleInfo = await this.getModulePrefetchInfo(module, courseId, invalidateCache, component);
instance.size = moduleInfo.sizeReadable;
instance.prefetchStatusIcon = moduleInfo.statusIcon;
instance.prefetchStatus = moduleInfo.status;
instance.downloadTimeReadable = CoreTextUtils.ucFirst(moduleInfo.downloadTimeReadable);
if (moduleInfo.status != CoreConstants.NOT_DOWNLOADABLE) {
// Module is downloadable, get the text to display to prefetch.
if (moduleInfo.downloadTime && moduleInfo.downloadTime > 0) {
instance.prefetchText = Translate.instant('core.lastdownloaded') + ': ' + moduleInfo.downloadTimeReadable;
} else {
// Module not downloaded, show a default text.
instance.prefetchText = Translate.instant('core.download');
}
}
if (moduleInfo.status == CoreConstants.DOWNLOADING) {
// Set this to empty to prevent "remove file" option showing up while downloading.
instance.size = '';
}
if (!instance.contextMenuStatusObserver && component) {
instance.contextMenuStatusObserver = CoreEvents.on(
CoreEvents.PACKAGE_STATUS_CHANGED,
(data) => {
if (data.componentId == module.id && data.component == component) {
this.fillContextMenu(instance, module, courseId, false, component);
}
},
siteId,
);
}
if (!instance.contextFileStatusObserver && component) {
// Debounce the update size function to prevent too many calls when downloading or deleting a whole activity.
const debouncedUpdateSize = CoreUtils.debounce(async () => {
const moduleSize = await CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, courseId);
instance.size = moduleSize > 0 ? CoreTextUtils.bytesToSize(moduleSize, 2) : '';
}, 1000);
instance.contextFileStatusObserver = CoreEvents.on(
CoreEvents.COMPONENT_FILE_ACTION,
(data) => {
if (data.component != component || data.componentId != module.id) {
// The event doesn't belong to this component, ignore.
return;
}
if (!CoreFilepool.isFileEventDownloadedOrDeleted(data)) {
return;
}
// Update the module size.
debouncedUpdateSize();
},
siteId,
);
}
}
/**
* Get a course. It will first check the user courses, and fallback to another WS if not enrolled.
*
@ -2225,14 +2099,3 @@ export type CoreCourseOpenModuleOptions = {
sectionId?: number; // Section the module belongs to.
modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module.
};
type ComponentWithContextMenu = {
prefetchStatusIcon?: string;
isDestroyed?: boolean;
size?: string;
prefetchStatus?: string;
prefetchText?: string;
downloadTimeReadable?: string;
contextMenuStatusObserver?: CoreEventObserver;
contextFileStatusObserver?: CoreEventObserver;
};

View File

@ -202,10 +202,10 @@ export interface CoreCourseModuleMainComponent {
* Refresh the data.
*
* @param refresher Refresher.
* @param done Function to call when done.
* @param showErrors If show errors to the user of hide them.
* @return Promise resolved when done.
*/
doRefresh(refresher?: IonRefresher, done?: () => void): Promise<void>;
doRefresh(refresher?: IonRefresher | null, showErrors?: boolean): Promise<void>;
}
/**

View File

@ -1,28 +1,8 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item [hidden]="!displayOpenInBrowser || !externalUrl || (
content?.compileComponent?.componentInstance?.displayOpenInBrowser === false)" [priority]="900"
[content]="'core.openinbrowser' | translate" [href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item [hidden]="!displayDescription || !description || (
content?.compileComponent?.componentInstance?.displayDescription === false)" [priority]="800"
[content]="'core.moduleintro' | translate" (action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item [hidden]="!displayRefresh || (
content?.compileComponent?.componentInstance?.displayRefresh === false)" [priority]="700"
[content]="'core.refresh' | translate" (action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item [hidden]="!displayPrefetch || !prefetchStatusIcon || (
content?.compileComponent?.componentInstance?.displayPrefetch === false)" [priority]="600" [content]="prefetchText"
(action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item [hidden]="!displaySize || !size || (
content?.compileComponent?.componentInstance?.displaySize === false)" [priority]="500"
[content]="'core.clearstoreddata' | translate:{$a: size}" [iconDescription]="'fas-archive'" (action)="removeFiles()"
iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
<ion-button fill="clear" (click)="openModuleSummary()" [attr.aria-label]="'core.info' | translate">
<ion-icon name="fas-info-circle" slot="icon-only" aria-hidden="true"></ion-icon>
</ion-button>
</core-navbar-buttons>
<core-site-plugins-plugin-content *ngIf="component && method" [component]="component" [method]="method" [args]="args"

View File

@ -14,8 +14,13 @@
import { CoreConstants } from '@/core/constants';
import { Component, OnInit, OnDestroy, Input, ViewChild } from '@angular/core';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreSiteWSPreSets } from '@classes/site';
import {
CoreCourseModuleSummaryResult,
CoreCourseModuleSummaryComponent,
} from '@features/course/components/module-summary/module-summary';
import { CoreCourseHelper, CoreCourseModuleData } from '@features/course/services/course-helper';
import {
CoreCourseModuleDelegate,
@ -28,10 +33,8 @@ import {
CoreSitePluginsCourseModuleHandlerData,
} from '@features/siteplugins/services/siteplugins';
import { IonRefresher } from '@ionic/angular';
import { CoreTextUtils } from '@services/utils/text';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver } from '@singletons/events';
import { CoreSitePluginsPluginContentComponent } from '../plugin-content/plugin-content';
/**
@ -55,22 +58,39 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
args?: Record<string, unknown>;
initResult?: CoreSitePluginsContent | null;
preSets?: CoreSiteWSPreSets;
// Data for context menu.
externalUrl?: string;
description?: string;
refreshIcon?: string;
/**
* @deprecated since 4.0, use module.url instead.
*/
externalUrl?: string;
/**
* @deprecated since 4.0. It won't be populated anymore.
*/
refreshIcon = CoreConstants.ICON_REFRESH;
/**
* @deprecated since 4.0.. It won't be populated anymore.
*/
prefetchStatus?: string;
/**
* @deprecated since 4.0. It won't be populated anymore.
*/
prefetchStatusIcon?: string;
/**
* @deprecated since 4.0. It won't be populated anymore.
*/
prefetchText?: string;
/**
* @deprecated since 4.0. It won't be populated anymore.
*/
size?: string;
contextMenuStatusObserver?: CoreEventObserver;
contextFileStatusObserver?: CoreEventObserver;
displayOpenInBrowser = true;
displayDescription = true;
displayRefresh = true;
displayPrefetch = true;
displaySize = true;
ptrEnabled = true;
isDestroyed = false;
@ -80,8 +100,6 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
* Component being initialized.
*/
ngOnInit(): void {
this.refreshIcon = CoreConstants.ICON_LOADING;
if (!this.module) {
return;
}
@ -122,71 +140,119 @@ export class CoreSitePluginsModuleIndexComponent implements OnInit, OnDestroy, C
* Refresh the data.
*
* @param refresher Refresher.
* @param done Function to call when done.
* @return Promise resolved when done.
*/
async doRefresh(refresher?: IonRefresher | null, done?: () => void): Promise<void> {
if (this.content) {
this.refreshIcon = CoreConstants.ICON_LOADING;
}
async doRefresh(refresher?: IonRefresher | null): Promise<void> {
try {
await this.content?.refreshContent(false);
} finally {
refresher?.complete();
done && done();
}
}
/**
* Function called when the data of the site plugin content is loaded.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
contentLoaded(refresh: boolean): void {
this.refreshIcon = CoreConstants.ICON_REFRESH;
// Check if there is a prefetch handler for this type of module.
if (CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(this.module.modname)) {
CoreCourseHelper.fillContextMenu(this, this.module, this.courseId, refresh, this.component);
}
return;
}
/**
* Function called when starting to load the data of the site plugin content.
*/
contentLoading(): void {
this.refreshIcon = CoreConstants.ICON_LOADING;
return;
}
/**
* Expand the description.
*
* @deprecated since 4.0
*/
expandDescription(): void {
if (!this.description) {
this.openModuleSummary();
}
/**
* Opens a module summary page.
*/
async openModuleSummary(): Promise<void> {
if (!this.module) {
return;
}
CoreTextUtils.viewText(Translate.instant('core.description'), this.description, {
component: this.component,
componentId: this.module.id,
filter: true,
contextLevel: 'module',
instanceId: this.module.id,
courseId: this.courseId,
const data = await CoreDomUtils.openSideModal<CoreCourseModuleSummaryResult>({
component: CoreCourseModuleSummaryComponent,
componentProps: {
moduleId: this.module.id,
module: this.module,
description: this.description,
component: this.component,
courseId: this.courseId,
displayOptions: {
displayOpenInBrowser: this.displayOpenInBrowser,
displayDescription: this.displayDescription,
displayRefresh: this.displayRefresh,
displayPrefetch: this.displayPrefetch,
displaySize: this.displaySize,
displayBlog: false,
},
},
});
if (data && data.action == 'refresh') {
const modal = await CoreDomUtils.showModalLoading();
try {
await this.doRefresh();
} finally {
modal.dismiss();
}
}
}
/**
* Prefetch the module.
*
* @deprecated since 4.0
*/
prefetch(): void {
CoreCourseHelper.contextMenuPrefetch(this, this.module, this.courseId);
async prefetch(): Promise<void> {
try {
// We need to call getDownloadSize, the package might have been updated.
const size = await CoreCourseModulePrefetchDelegate.getModuleDownloadSize(this.module, this.courseId, true);
await CoreDomUtils.confirmDownloadSize(size);
await CoreCourseModulePrefetchDelegate.prefetchModule(this.module, this.courseId, true);
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}
}
}
/**
* Confirm and remove downloaded files.
*
* @deprecated since 4.0
*/
removeFiles(): void {
CoreCourseHelper.confirmAndRemoveFiles(this.module, this.courseId);
async removeFiles(): Promise<void> {
let modal: CoreIonLoadingElement | undefined;
try {
await CoreDomUtils.showDeleteConfirm('addon.storagemanager.confirmdeletedatafrom', { name: this.module.name });
modal = await CoreDomUtils.showModalLoading();
await CoreCourseHelper.removeModuleStoredData(this.module, this.courseId);
} catch (error) {
if (error) {
CoreDomUtils.showErrorModal(error);
}
} finally {
modal?.dismiss();
}
}
/**