MOBILE-3954 storage: Add prefetch features on course storage page

main
Pau Ferrer Ocaña 2021-12-23 16:02:42 +01:00
parent e673ffe2df
commit 255e987412
16 changed files with 538 additions and 99 deletions

View File

@ -1106,6 +1106,7 @@
"addon.storagemanager.deletecourses": "local_moodlemobileapp",
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
"addon.storagemanager.info": "local_moodlemobileapp",
"addon.storagemanager.managecoursestorage": "local_moodlemobileapp",
"addon.storagemanager.managestorage": "local_moodlemobileapp",
"addon.storagemanager.storageused": "local_moodlemobileapp",
"assets.countries.AD": "countries",

View File

@ -91,7 +91,7 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
const items = config.frontpageloggedin.split(',');
const hasNewsItem = items.find((item) => parseInt(item, 10) == FrontPageItemNames['NEWS_ITEMS']);
const result = CoreCourseHelper.addHandlerDataForModules(
const result = await CoreCourseHelper.addHandlerDataForModules(
[mainMenuBlock],
this.siteHomeId,
undefined,

View File

@ -4,5 +4,6 @@
"deletedatafrom": "Offload data from {{name}}",
"info": "Files stored on your device make the app work faster and enable the app to be used offline. You can safely offload files if you need to free up storage space.",
"managestorage": "Manage storage",
"managecoursestorage": "Manage course storage",
"storageused": "File storage used:"
}

View File

@ -4,7 +4,7 @@
<ion-back-button [text]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<h1>{{ 'addon.storagemanager.managestorage' | translate }}</h1>
<h1>{{ 'addon.storagemanager.managecoursestorage' | translate }}</h1>
</ion-title>
</ion-toolbar>
</ion-header>
@ -12,54 +12,90 @@
<core-loading [hideUntil]="loaded">
<ion-card class="wholecourse">
<ion-card-header>
<ion-card-title *ngIf="course.displayname">{{ course.displayname }}</ion-card-title>
<ion-card-title *ngIf="!course.displayname">{{ course.fullname }}</ion-card-title>
<ion-card-title>{{ title }}</ion-card-title>
<p class="ion-text-wrap">{{ 'addon.storagemanager.info' | translate }}</p>
<ion-item class="size ion-text-wrap ion-no-padding" lines="none">
<ion-icon name="fas-archive" slot="start" aria-hidden="true"></ion-icon>
<ion-label>
<p class="item-heading ion-text-wrap">{{ 'addon.storagemanager.storageused' | translate }}</p>
<ion-badge color="light">{{ totalSize | coreBytesToSize }}</ion-badge>
</ion-label>
<p slot="end" class="ion-text-end">{{ totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteForCourse()" [disabled]="totalSize == 0">
<ion-icon name="fas-trash" slot="icon-only" [attr.aria-label]="'addon.storagemanager.deletecourse' | translate">
</ion-icon>
</ion-button>
</ion-item>
<ion-button *ngIf="downloadCourseEnabled" (click)="prefetchCourse()" expand="block">
<ion-icon *ngIf="!prefetchCourseData.loading" [name]="prefetchCourseData.icon" slot="start"></ion-icon>
<ion-spinner *ngIf="prefetchCourseData.loading" slot="start"></ion-spinner>
{{ prefetchCourseData.statusTranslatable | translate }}
</ion-button>
</ion-card-header>
</ion-card>
<ng-container *ngFor="let section of sections">
<ion-card *ngIf="section.totalSize! > 0" class="section">
<ion-card class="section" *ngIf="section.modules.length > 0">
<ion-card-header>
<ion-item class="ion-no-padding">
<ion-label>
<p class="item-heading ion-text-wrap">{{ section.name }}</p>
<ion-badge color="light" *ngIf="section.totalSize > 0">
{{ section.totalSize | coreBytesToSize }}
</ion-badge>
<!-- Download progress. -->
<p *ngIf="downloadEnabled && section.isDownloading">
<core-progress-bar [progress]="section.total == 0 ? -1 : section.count / section.total">
</core-progress-bar>
</p>
</ion-label>
<p slot="end" class="ion-text-end">{{ section.totalSize | coreBytesToSize }}</p>
<ion-button slot="end" (click)="deleteForSection(section)">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
</ion-icon>
</ion-button>
<div class="storage-buttons" slot="end" *ngIf="section.totalSize > 0 || downloadEnabled">
<ion-button (click)="deleteForSection(section)" *ngIf="section.totalSize > 0">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: section.name }">
</ion-icon>
</ion-button>
<div *ngIf="downloadEnabled" slot="end" class="core-button-spinner">
<core-download-refresh *ngIf="!section.isDownloading" [status]="section.downloadStatus" [enabled]="true"
(action)="prefecthSection(section)" [loading]="section.isDownloading || section.isCalculating"
[canTrustDownload]="true" size="small">
</core-download-refresh>
<ion-badge class="core-course-download-section-progress"
*ngIf="section.isDownloading && section.count < section.total" role="progressbar"
[attr.aria-valuemax]="section.total" [attr.aria-valuenow]="section.count"
[attr.aria-valuetext]="'core.course.downloadsectionprogressdescription' | translate:section">
{{section.count}} / {{section.total}}
</ion-badge>
</div>
</div>
</ion-item>
</ion-card-header>
<ion-card-content>
<ng-container *ngFor="let module of section.modules">
<ion-item class="ion-no-padding" *ngIf="module.totalSize! > 0">
<core-mod-icon slot="start" *ngIf="module.handlerData!.icon" [modicon]="module.handlerData!.icon"
<ion-item class="ion-no-padding"
*ngIf="(downloadEnabled && module.handlerData?.showDownloadButton) || module.totalSize > 0">
<core-mod-icon slot="start" *ngIf="module.handlerData.icon" [modicon]="module.handlerData.icon"
[modname]="module.modname" [componentId]="module.instance">
</core-mod-icon>
<ion-label class="ion-text-wrap">
<h3 class="{{module.handlerData!.class}} addon-storagemanager-module-size">
{{ module.name }}
</h3>
<ion-badge color="light" *ngIf="module.totalSize > 0">
{{ module.totalSize | coreBytesToSize }}
</ion-badge>
</ion-label>
<p slot="end" class="ion-text-end">{{ module.totalSize | coreBytesToSize }}</p>
<ion-button fill="clear" slot="end" (click)="deleteForModule(module)">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon>
</ion-button>
<div class="storage-buttons" slot="end">
<ion-button fill="clear" (click)="deleteForModule(module, section)" *ngIf="module.totalSize > 0">
<ion-icon name="fas-trash" slot="icon-only"
[attr.aria-label]="'addon.storagemanager.deletedatafrom' | translate: { name: module.name }">
</ion-icon>
</ion-button>
<core-download-refresh *ngIf="downloadEnabled && module.handlerData?.showDownloadButton"
[status]="module.downloadStatus" [enabled]="true" [canTrustDownload]="true" size="small"
[loading]="module.spinner || module.handlerData.spinner" (action)="prefetchModule(module, section)">
</core-download-refresh>
</div>
</ion-item>
</ng-container>
</ion-card-content>

View File

@ -9,3 +9,12 @@
font-size: 1.2rem;
}
}
.storage-buttons {
display: flex;
align-items: center;
}
ion-item {
--inner-padding-end: 0px;
}

View File

@ -13,37 +13,77 @@
// limitations under the License.
import { CoreConstants } from '@/core/constants';
import { Component, OnInit } from '@angular/core';
import { CoreCourse } from '@features/course/services/course';
import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreEnrolledCourseData } from '@features/courses/services/courses';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { CoreCourse, CoreCourseProvider } from '@features/course/services/course';
import {
CoreCourseHelper,
CoreCourseModuleData,
CoreCourseSectionWithStatus,
CorePrefetchStatusInfo,
} from '@features/course/services/course-helper';
import {
CoreCourseModulePrefetchDelegate,
CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate';
import { CoreCourses, CoreEnrolledCourseData } from '@features/courses/services/courses';
import { CoreNavigator } from '@services/navigator';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
/**
* Page that displays the amount of file storage used by each activity on the course, and allows
* the user to delete these files.
* the user to prefecth and delete this data.
*/
@Component({
selector: 'page-addon-storagemanager-course-storage',
templateUrl: 'course-storage.html',
styleUrls: ['course-storage.scss'],
})
export class AddonStorageManagerCourseStoragePage implements OnInit {
export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy {
course!: CoreEnrolledCourseData;
course?: CoreEnrolledCourseData;
courseId!: number;
title = '';
loaded = false;
sections: AddonStorageManagerCourseSection[] = [];
totalSize = 0;
downloadEnabled = false;
downloadCourseEnabled = false;
prefetchCourseData: CorePrefetchStatusInfo = {
icon: CoreConstants.ICON_LOADING,
statusTranslatable: 'core.course.downloadcourse',
status: '',
loading: true,
};
protected siteUpdatedObserver?: CoreEventObserver;
protected courseStatusObserver?: CoreEventObserver;
protected sectionStatusObserver?: CoreEventObserver;
protected moduleStatusObserver?: CoreEventObserver;
protected isDestroyed = false;
protected isGuest = false;
constructor() {
// Refresh the enabled flags if site is updated.
this.siteUpdatedObserver = CoreEvents.on(CoreEvents.SITE_UPDATED, () => {
this.downloadCourseEnabled = !!this.course && !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
this.initCoursePrefetch();
this.initModulePrefetch();
}, CoreSites.getCurrentSiteId());
}
/**
* View loaded.
* @inheritdoc
*/
async ngOnInit(): Promise<void> {
try {
this.course = CoreNavigator.getRequiredRouteParam<CoreEnrolledCourseData>('course');
this.courseId = CoreNavigator.getRequiredRouteParam('courseId');
} catch (error) {
CoreDomUtils.showErrorModal(error);
@ -52,16 +92,160 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
return;
}
const sections = await CoreCourse.getSections(this.course.id, false, true);
this.sections = CoreCourseHelper.addHandlerDataForModules(sections, this.course.id).sections;
this.course = CoreNavigator.getRouteParam<CoreEnrolledCourseData>('course');
this.title = this.course?.displayname ?? this.course?.fullname ?? '';
if (!this.title && this.courseId == CoreSites.getCurrentSiteHomeId()) {
this.title = Translate.instant('core.sitehome.sitehome');
}
this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest');
this.downloadCourseEnabled = !!this.course && !CoreCourses.isDownloadCourseDisabledInSite();
this.downloadEnabled = !CoreSites.getRequiredCurrentSite().isOfflineDisabled();
const sections = await CoreCourse.getSections(this.courseId, false, true);
this.sections = (await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId)).sections
.map((section) => ({ ...section, totalSize: 0 }));
await Promise.all([
this.loadSizes(),
this.initCoursePrefetch(),
this.initModulePrefetch(),
]);
this.loaded = true;
}
/**
* Init course prefetch information.
*
* @return Promise resolved when done.
*/
protected async initCoursePrefetch(): Promise<void> {
if (!this.downloadCourseEnabled || this.courseStatusObserver) {
return;
}
// Listen for changes in course status.
this.courseStatusObserver = CoreEvents.on(CoreEvents.COURSE_STATUS_CHANGED, (data) => {
if (data.courseId == this.courseId || data.courseId == CoreCourseProvider.ALL_COURSES_CLEARED) {
this.updateCourseStatus(data.status);
}
}, CoreSites.getCurrentSiteId());
// Determine the course prefetch status.
await this.determineCoursePrefetchIcon();
if (this.prefetchCourseData.icon != CoreConstants.ICON_LOADING) {
return;
}
// Course is being downloaded. Get the download promise.
const promise = CoreCourseHelper.getCourseDownloadPromise(this.courseId);
if (promise) {
// There is a download promise. Show an error if it fails.
promise.catch((error) => {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
});
} else {
// No download, this probably means that the app was closed while downloading. Set previous status.
const status = await CoreCourse.setCoursePreviousStatus(this.courseId);
this.updateCourseStatus(status);
}
}
/**
* Init module prefetch information.
*
* @return Promise resolved when done.
*/
protected async initModulePrefetch(): Promise<void> {
if (!this.downloadEnabled || this.sectionStatusObserver) {
return;
}
// Listen for section status changes.
this.sectionStatusObserver = CoreEvents.on(
CoreEvents.SECTION_STATUS_CHANGED,
async (data) => {
if (!this.downloadEnabled || !this.sections.length || !data.sectionId || data.courseId != this.courseId) {
return;
}
// Check if the affected section is being downloaded.
// If so, we don't update section status because it'll already be updated when the download finishes.
const downloadId = CoreCourseHelper.getSectionDownloadId({ id: data.sectionId });
if (CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
return;
}
// Get the affected section.
const section = this.sections.find(section => section.id == data.sectionId);
if (!section) {
return;
}
// Recalculate the status.
await CoreCourseHelper.calculateSectionStatus(section, this.courseId, false);
if (section.isDownloading && !CoreCourseModulePrefetchDelegate.isBeingDownloaded(downloadId)) {
// All the modules are now downloading, set a download all promise.
this.prefecthSection(section);
}
},
CoreSites.getCurrentSiteId(),
);
// The download status of a section might have been changed from within a module page.
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
this.sections.forEach((section) => {
section.modules.forEach((module) => {
if (module.handlerData?.showDownloadButton) {
module.spinner = true;
// Listen for changes on this module status, even if download isn't enabled.
module.prefetchHandler = CoreCourseModulePrefetchDelegate.getPrefetchHandlerFor(module.modname);
this.calculateModuleStatus(module);
}
});
});
this.moduleStatusObserver = CoreEvents.on(CoreEvents.PACKAGE_STATUS_CHANGED, (data) => {
let module: AddonStorageManagerModule | undefined;
this.sections.some((section) => {
module = section.modules.find((module) =>
module.id == data.componentId && module.prefetchHandler && data.component == module.prefetchHandler?.component);
return !!module;
});
if (!module) {
return;
}
// Call determineModuleStatus to get the right status to display.
const status = CoreCourseModulePrefetchDelegate.determineModuleStatus(module, data.status);
// Update the status.
this.updateModuleStatus(module, status);
}, CoreSites.getCurrentSiteId());
}
/**
* Init section, course and modules sizes.
*/
protected async loadSizes(): Promise<void> {
this.totalSize = 0;
const promises: Promise<void>[] = [];
this.sections.forEach((section) => {
section.totalSize = 0;
section.modules.forEach((module) => {
module.parentSection = section;
module.totalSize = 0;
// Note: This function only gets the size for modules which are downloadable.
@ -71,11 +255,11 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
// But these aren't necessarily consistent, for example mod_frog vs mmaModFrog.
// There is nothing enforcing correct values.
// Most modules which have large files are downloadable, so I think this is sufficient.
const promise = CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.course.id).then((size) => {
const promise = CoreCourseModulePrefetchDelegate.getModuleStoredSize(module, this.courseId).then((size) => {
// There are some cases where the return from this is not a valid number.
if (!isNaN(size)) {
module.totalSize = Number(size);
section.totalSize! += size;
section.totalSize += size;
this.totalSize += size;
}
@ -86,8 +270,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
});
await Promise.all(promises);
this.loaded = true;
// Mark course as not downloaded if course size is 0.
if (this.totalSize == 0) {
this.markCourseAsNotDownloaded();
}
@ -146,15 +330,16 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
}
});
this.deleteModules(modules);
this.deleteModules(modules, section);
}
/**
* The user has requested a delete for a module's data
*
* @param module Module details
* @param section Section the module belongs to.
*/
async deleteForModule(module: AddonStorageManagerModule): Promise<void> {
async deleteForModule(module: AddonStorageManagerModule, section: AddonStorageManagerCourseSection): Promise<void> {
if (module.totalSize === 0) {
return;
}
@ -169,25 +354,29 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
return;
}
this.deleteModules([module]);
this.deleteModules([module], section);
}
/**
* Deletes the specified modules, showing the loading overlay while it happens.
*
* @param modules Modules to delete
* @param section Section the modules belong to.
* @return Promise<void> Once deleting has finished
*/
protected async deleteModules(modules: AddonStorageManagerModule[]): Promise<void> {
protected async deleteModules(modules: AddonStorageManagerModule[], section?: AddonStorageManagerCourseSection): Promise<void> {
const modal = await CoreDomUtils.showModalLoading();
const promises: Promise<void>[] = [];
modules.forEach((module) => {
// Remove the files.
const promise = CoreCourseHelper.removeModuleStoredData(module, this.course.id).then(() => {
const promise = CoreCourseHelper.removeModuleStoredData(module, this.courseId).then(() => {
const moduleSize = module.totalSize || 0;
// When the files and cache are removed, update the size.
module.parentSection!.totalSize! -= module.totalSize!;
this.totalSize -= module.totalSize!;
if (section) {
section.totalSize -= moduleSize;
}
this.totalSize -= moduleSize;
module.totalSize = 0;
return;
@ -203,13 +392,8 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
} finally {
modal.dismiss();
// @TODO This is a workaround that should be more specific solving MOBILE-3305.
// Also should take into account all modules are not downloaded.
// Mark course as not downloaded if course size is 0.
if (this.totalSize == 0) {
this.markCourseAsNotDownloaded();
}
await this.loadSizes();
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
}
}
@ -221,17 +405,201 @@ export class AddonStorageManagerCourseStoragePage implements OnInit {
// Also should take into account all modules are not downloaded.
// Check after MOBILE-3188 is integrated.
CoreCourse.setCourseStatus(this.course.id, CoreConstants.NOT_DOWNLOADED);
CoreCourse.setCourseStatus(this.courseId, CoreConstants.NOT_DOWNLOADED);
}
/**
* Calculate the status of sections.
*
* @param refresh If refresh or not.
*/
protected calculateSectionsStatus(refresh?: boolean): void {
if (!this.sections) {
return;
}
CoreUtils.ignoreErrors(CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, refresh));
}
/**
* Confirm and prefetch a section. If the section is "all sections", prefetch all the sections.
*
* @param section Section to download.
* @param refresh Refresh clicked (not used).
*/
async prefecthSection(section: AddonStorageManagerCourseSection): Promise<void> {
section.isCalculating = true;
try {
await CoreCourseHelper.confirmDownloadSizeSection(this.courseId, section, this.sections);
try {
await CoreCourseHelper.prefetchSection(section, this.courseId, this.sections);
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingsection', true);
}
} finally {
await this.loadSizes();
}
} catch (error) {
// User cancelled or there was an error calculating the size.
if (!this.isDestroyed && error) {
CoreDomUtils.showErrorModal(error);
return;
}
} finally {
section.isCalculating = false;
}
}
/**
* Download the module.
*
* @param module Module to prefetch.
* @param refresh Whether it's refreshing.
* @return Promise resolved when done.
*/
async prefetchModule(
module: AddonStorageManagerModule,
refresh = false,
): Promise<void> {
if (!module.prefetchHandler) {
return;
}
// Show spinner since this operation might take a while.
module.spinner = true;
try {
// Get download size to ask for confirm if it's high.
const size = await module.prefetchHandler.getDownloadSize(module, module.course, true);
await CoreCourseHelper.prefetchModule(module.prefetchHandler, module, size, module.course, refresh);
CoreCourseHelper.calculateSectionsStatus(this.sections, this.courseId, false, false);
} catch (error) {
if (!this.isDestroyed) {
CoreDomUtils.showErrorModalDefault(error, 'core.errordownloading', true);
}
} finally {
module.spinner = false;
await this.loadSizes();
}
}
/**
* Show download buttons according to module status.
*
* @param module Module to update.
* @param status Module status.
*/
protected updateModuleStatus(module: AddonStorageManagerModule, status: string): void {
if (!status) {
return;
}
module.spinner = false;
module.downloadStatus = status;
module.handlerData?.updateStatus?.(status);
}
/**
* Calculate and show module status.
*
* @param module Module to update.
* @return Promise resolved when done.
*/
protected async calculateModuleStatus(module: AddonStorageManagerModule): Promise<void> {
if (!module) {
return;
}
const status = await CoreCourseModulePrefetchDelegate.getModuleStatus(module, this.courseId);
this.updateModuleStatus(module, status);
}
/**
* Determines the prefetch icon of the course.
*
* @return Promise resolved when done.
*/
protected async determineCoursePrefetchIcon(): Promise<void> {
this.prefetchCourseData = await CoreCourseHelper.getCourseStatusIconAndTitle(this.courseId);
}
/**
* Update the course status icon and title.
*
* @param status Status to show.
*/
protected updateCourseStatus(status: string): void {
const statusData = CoreCourseHelper.getCoursePrefetchStatusInfo(status);
this.prefetchCourseData.status = statusData.status;
this.prefetchCourseData.icon = statusData.icon;
this.prefetchCourseData.statusTranslatable = statusData.statusTranslatable;
this.prefetchCourseData.loading = statusData.loading;
}
/**
* Prefetch the whole course.
*/
async prefetchCourse(): Promise<void> {
if (!this.course) {
return;
}
try {
await CoreCourseHelper.confirmAndPrefetchCourse(
this.prefetchCourseData,
this.course,
{
sections: this.sections,
isGuest: this.isGuest,
},
);
} catch (error) {
if (this.isDestroyed) {
return;
}
CoreDomUtils.showErrorModalDefault(error, 'core.course.errordownloadingcourse', true);
}
}
/**
* @inheritdoc
*/
ngOnDestroy(): void {
this.courseStatusObserver?.off();
this.sectionStatusObserver?.off();
this.moduleStatusObserver?.off();
this.siteUpdatedObserver?.off();
this.sections.forEach((section) => {
section.modules.forEach((module) => {
module.handlerData?.onDestroy?.();
});
});
this.isDestroyed = true;
}
}
type AddonStorageManagerCourseSection = Omit<CoreCourseSection, 'modules'> & {
totalSize?: number;
type AddonStorageManagerCourseSection = Omit<CoreCourseSectionWithStatus, 'modules'> & {
totalSize: number;
modules: AddonStorageManagerModule[];
};
type AddonStorageManagerModule = CoreCourseModuleData & {
parentSection?: AddonStorageManagerCourseSection;
totalSize?: number;
prefetchHandler?: CoreCourseModulePrefetchHandler;
spinner?: boolean;
downloadStatus?: string;
};

View File

@ -49,7 +49,7 @@ export class AddonStorageManagerCourseMenuHandlerService implements CoreCourseOp
): CoreCourseOptionsMenuHandlerData {
return {
icon: 'fas-archive',
title: 'addon.storagemanager.managestorage',
title: 'addon.storagemanager.managecoursestorage',
page: 'storage/' + course.id,
class: 'addon-storagemanager-coursemenu-handler',
};

View File

@ -7,3 +7,5 @@
{{ 'core.percentagenumber' | translate: {$a: text} }}
</div>
</ng-container>
<ion-progress-bar *ngIf="progress < 0" type="indeterminate"></ion-progress-bar>

View File

@ -35,4 +35,11 @@
border-radius: 0;
}
}
ion-progress-bar {
--progress-background: var(--color);
height: var(--height);
margin-top: calc((var(--line-height) - var(--height)) /2);
margin-bottom: calc((var(--line-height) - var(--height)) /2);
}
}

View File

@ -30,7 +30,7 @@ import { DomSanitizer, Translate } from '@singletons';
})
export class CoreProgressBarComponent implements OnChanges {
@Input() progress!: number | string; // Percentage from 0 to 100.
@Input() progress!: number | string; // Percentage from 0 to 100. Negative number will show an indeterminate progress bar.
@Input() text?: string; // Percentage in text to be shown at the right. If not defined, progress will be used.
@Input() a11yText?: string; // Accessibility text to read before the percentage.
@Input() ariaDescribedBy?: string; // ID of the element that described the progress, if any.

View File

@ -314,7 +314,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy {
}
// Add handlers
const result = CoreCourseHelper.addHandlerDataForModules(
const result = await CoreCourseHelper.addHandlerDataForModules(
sections,
this.course.id,
completionStatus,

View File

@ -105,7 +105,7 @@ export class CoreCourseListModTypePage implements OnInit {
return section.modules.length > 0;
});
const result = CoreCourseHelper.addHandlerDataForModules(sections, this.courseId);
const result = await CoreCourseHelper.addHandlerDataForModules(sections, this.courseId);
this.sections = result.sections;
} catch (error) {

View File

@ -168,58 +168,60 @@ export class CoreCourseHelperProvider {
* @param forCoursePage Whether the data will be used to render the course page.
* @return Whether the sections have content.
*/
addHandlerDataForModules(
async addHandlerDataForModules(
sections: CoreCourseWSSection[],
courseId: number,
completionStatus?: Record<string, CoreCourseCompletionActivityStatus>,
courseName?: string,
forCoursePage = false,
): { hasContent: boolean; sections: CoreCourseSection[] } {
): Promise<{ hasContent: boolean; sections: CoreCourseSection[] }> {
let hasContent = false;
const formattedSections = sections.map<CoreCourseSection>((courseSection) => {
const section = {
...courseSection,
hasContent: this.sectionHasContent(courseSection),
};
const formattedSections = await Promise.all(
sections.map<Promise<CoreCourseSection>>(async (courseSection) => {
const section = {
...courseSection,
hasContent: this.sectionHasContent(courseSection),
};
if (!section.hasContent) {
return section;
}
hasContent = true;
section.modules.forEach(async (module) => {
module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
module.modname,
module,
courseId,
section.id,
forCoursePage,
);
if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
const activityStatus = completionStatus[module.id];
module.completiondata = {
state: activityStatus.state,
timecompleted: activityStatus.timecompleted,
overrideby: activityStatus.overrideby || 0,
valueused: activityStatus.valueused,
tracking: activityStatus.tracking,
courseId,
cmid: module.id,
};
if (!section.hasContent) {
return section;
}
// Check if the module is stealth.
module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible);
});
hasContent = true;
return section;
});
await Promise.all(section.modules.map(async (module) => {
module.handlerData = await CoreCourseModuleDelegate.getModuleDataFor(
module.modname,
module,
courseId,
section.id,
forCoursePage,
);
if (!module.completiondata && completionStatus && completionStatus[module.id] !== undefined) {
// Should not happen on > 3.6. Check if activity has completions and if it's marked.
const activityStatus = completionStatus[module.id];
module.completiondata = {
state: activityStatus.state,
timecompleted: activityStatus.timecompleted,
overrideby: activityStatus.overrideby || 0,
valueused: activityStatus.valueused,
tracking: activityStatus.tracking,
courseId,
cmid: module.id,
};
}
// Check if the module is stealth.
module.isStealth = module.visibleoncoursepage === 0 || (!!module.visible && !section.visible);
}));
return section;
}),
);
return { hasContent, sections: formattedSections };
}

View File

@ -7,6 +7,8 @@
(action)="switchDownload()" iconAction="toggle" [(toggle)]="downloadEnabled"></core-context-menu-item>
<core-context-menu-item [priority]="500" [content]="'addon.storagemanager.managestorage' | translate"
(action)="manageCoursesStorage()" iconAction="fas-archive"></core-context-menu-item>
<core-context-menu-item [priority]="400" [content]="'addon.storagemanager.managecoursestorage' | translate"
(action)="manageCourseStorage()" iconAction="fas-archive"></core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<ion-content>

View File

@ -129,7 +129,7 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
// Check "Include a topic section" setting from numsections.
this.section = config.numsections ? sections.find((section) => section.section == 1) : undefined;
if (this.section) {
const result = CoreCourseHelper.addHandlerDataForModules(
const result = await CoreCourseHelper.addHandlerDataForModules(
[this.section],
this.siteHomeId,
undefined,
@ -200,6 +200,13 @@ export class CoreSiteHomeIndexPage implements OnInit, OnDestroy {
CoreNavigator.navigateToSitePath('/storage');
}
/**
* Open page to manage course storage.
*/
manageCourseStorage(): void {
CoreNavigator.navigateToSitePath('/storage/' + this.siteHomeId);
}
/**
* Go to search courses.
*/

View File

@ -213,6 +213,10 @@ ion-button.button-outline {
--background: var(--contrast-background);
}
ion-button ion-spinner {
--color: inherit !important;
}
@each $color-name, $value in $colors {
.text-#{$color-name},
p.text-#{$color-name} {