MOBILE-2905 Allow user to manage storage within course

Adds a 'Manage storage' page on the course menu. This page
allows users to see how much file storage is being used by
each activity, section, and the whole course, and delete
the files if required.
main
sam marshall 2019-03-06 14:29:12 +00:00
parent 0daf5191ff
commit cb4fbce618
10 changed files with 405 additions and 1 deletions

View File

@ -881,6 +881,11 @@
"addon.notifications.notifications": "local_moodlemobileapp",
"addon.notifications.playsound": "local_moodlemobileapp",
"addon.notifications.therearentnotificationsyet": "local_moodlemobileapp",
"addon.storagemanager.deletecourse": "local_moodlemobileapp",
"addon.storagemanager.deletedatafrom": "local_moodlemobileapp",
"addon.storagemanager.info": "local_moodlemobileapp",
"addon.storagemanager.managestorage": "local_moodlemobileapp",
"addon.storagemanager.storageused": "local_moodlemobileapp",
"assets.countries.AD": "countries",
"assets.countries.AE": "countries",
"assets.countries.AF": "countries",

View File

@ -0,0 +1,7 @@
{
"deletecourse": "Offload all course data",
"deletedatafrom": "Offload data from {{name}}",
"info": "Files stored on your device make the app work faster, and when offline. You can safely offload them if you need to free up storage space.",
"managestorage": "Manage storage",
"storageused": "File storage used:"
}

View File

@ -0,0 +1,62 @@
<ion-header>
<ion-navbar core-back-button>
<ion-title>{{ 'addon.storagemanager.managestorage' | translate }}</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<core-loading [hideUntil]="loaded">
<ion-card class="wholecourse">
<ion-card-header>
<h1 text-wrap>{{ course.displayname }}</h1>
<p text-wrap>{{ 'addon.storagemanager.info' | translate }}</p>
<ion-item no-padding padding-top>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ 'addon.storagemanager.storageused' | translate }}
{{ totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only item-end no-padding (click)="deleteForCourse()" [disabled]="totalSize == 0">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletecourse' | translate }}"></core-icon>
</button>
</ion-item>
</ion-card-header>
</ion-card>
<ng-container *ngFor="let section of sections">
<ion-card *ngIf="section.totalSize > 0" class="section">
<ion-card-header>
<ion-item no-padding>
<ion-row>
<h2 text-wrap>{{ section.name }}</h2>
</ion-row>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ section.totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only item-end no-padding (click)="deleteForSection(section)">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: section.name } }}"></core-icon>
</button>
</ion-item>
</ion-card-header>
<ion-card-content>
<ng-container *ngFor="let module of section.modules">
<div *ngIf="module.totalSize > 0">
<ion-item no-padding>
<ion-row class="{{module.handlerData.class}}">
<img *ngIf="module.handlerData.icon" [src]="module.handlerData.icon" alt="" role="presentation" class="core-module-icon"
>{{ module.name }}
</ion-row>
<ion-row class="size">
<ion-icon name="cube"></ion-icon>
{{ module.totalSize | coreBytesToSize }}
</ion-row>
<button ion-button icon-only outline item-end (click)="deleteForModule(module)">
<core-icon name="trash" label="{{ 'addon.storagemanager.deletedatafrom' | translate: { name: module.name } }}"></core-icon>
</button>
</ion-item>
</div>
</ng-container>
</ion-card-content>
</ion-card>
</ng-container>
</core-loading>
</ion-content>

View File

@ -0,0 +1,36 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { IonicPageModule } 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 { AddonStorageManagerCourseStoragePage } from './course-storage';
@NgModule({
declarations: [
AddonStorageManagerCourseStoragePage,
],
imports: [
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
IonicPageModule.forChild(AddonStorageManagerCourseStoragePage),
TranslateModule.forChild()
],
})
export class AddonStorageManagerCourseStoragePageModule {
}

View File

@ -0,0 +1,28 @@
ion-app.app-root page-addon-storagemanager-course-storage {
.item-md.item-block .item-inner {
padding-right: 0;
padding-left: 0;
}
ion-card.section ion-card-header.card-header {
border-bottom: 1px solid $list-border-color;
margin-bottom: 8px;
padding-top: 8px;
padding-bottom: 8px;
}
ion-card.section h2 {
font-weight: bold;
font-size: 2rem;
}
.size {
margin-top: 4px;
}
.size ion-icon {
margin-right: 4px;
}
.core-module-icon {
margin-right: 4px;
width: 16px;
height: 16px;
display: inline;
}
}

View File

@ -0,0 +1,163 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 } from '@angular/core';
import { IonicPage, Content, NavParams } from 'ionic-angular';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { TranslateService } from '@ngx-translate/core';
/**
* Page that displays the amount of file storage used by each activity on the course, and allows
* the user to delete these files.
*/
@IonicPage({ segment: 'addon-storagemanager-course-storage' })
@Component({
selector: 'page-addon-storagemanager-course-storage',
templateUrl: 'course-storage.html',
})
export class AddonStorageManagerCourseStoragePage {
@ViewChild(Content) content: Content;
course: any;
loaded: boolean;
sections: any;
totalSize: number;
constructor(navParams: NavParams,
private courseProvider: CoreCourseProvider,
private prefetchDelegate: CoreCourseModulePrefetchDelegate,
private courseHelperProvider: CoreCourseHelperProvider,
private domUtils: CoreDomUtilsProvider,
private translate: TranslateService) {
this.course = navParams.get('course');
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
this.courseProvider.getSections(this.course.id, false, true).then((sections) => {
this.courseHelperProvider.addHandlerDataForModules(sections, this.course.id);
this.sections = sections;
this.totalSize = 0;
const allPromises = [];
this.sections.forEach((section) => {
section.totalSize = 0;
section.modules.forEach((module) => {
module.parentSection = section;
// Note: This function only gets the size for modules which are downloadable.
// For other modules it always returns 0, even if they have downloaded some files.
// However there is no 100% reliable way to actually track the files in this case.
// You can maybe guess it based on the component and componentid.
// 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 = this.prefetchDelegate.getModuleDownloadedSize(module, this.course.id).
then((size) => {
module.totalSize = size;
section.totalSize += size;
this.totalSize += size;
});
allPromises.push(promise);
});
});
Promise.all(allPromises).then(() => {
this.loaded = true;
});
});
}
/**
* The user has requested a delete for the whole course data.
*
* (This works by deleting data for each module on the course that has data.)
*/
deleteForCourse(): void {
const modules = [];
this.sections.forEach((section) => {
section.modules.forEach((module) => {
if (module.totalSize > 0) {
modules.push(module);
}
});
});
this.deleteModules(modules);
}
/**
* The user has requested a delete for a section's data.
*
* (This works by deleting data for each module in the section that has data.)
*
* @param {any} section Section object with information about section and modules
*/
deleteForSection(section: any): void {
const modules = [];
section.modules.forEach((module) => {
if (module.totalSize > 0) {
modules.push(module);
}
});
this.deleteModules(modules);
}
/**
* The user has requested a delete for a module's data
*
* @param {any} module Module details
*/
deleteForModule(module: any): void {
if (module.totalSize > 0) {
this.deleteModules([module]);
}
}
/**
* Deletes the specified modules, showing the loading overlay while it happens.
*
* @param {any[]} modules Modules to delete
* @return Promise<void> Once deleting has finished
*/
protected deleteModules(modules: any[]): Promise<void> {
const modal = this.domUtils.showModalLoading();
const promises = [];
modules.forEach((module) => {
// Remove the files.
const promise = this.prefetchDelegate.removeModuleFiles(module, this.course.id).then(() => {
// When the files are removed, update the size.
module.parentSection.totalSize -= module.totalSize;
this.totalSize -= module.totalSize;
module.totalSize = 0;
});
promises.push(promise);
});
return Promise.all(promises).then(() => {
modal.dismiss();
}).catch((error) => {
modal.dismiss();
this.domUtils.showErrorModalDefault(error, this.translate.instant('core.errordeletefile'));
});
}
}

View File

@ -0,0 +1,62 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@core/course/providers/options-delegate';
/**
* Handler to inject an option into course menu so that user can get to the manage storage page.
*/
@Injectable()
export class AddonStorageManagerCourseMenuHandler implements CoreCourseOptionsMenuHandler {
name = 'AddonStorageManager';
priority = 500;
isMenuHandler = true;
/**
* Checks if the handler is enabled for specified course. This handler is always available.
*
* @param {number} courseId Course id
* @param {any} accessData Access data
* @param {any} [navOptions] Navigation options if any
* @param {any} [admOptions] Admin options if any
* @return {boolean | Promise<boolean>} True
*/
isEnabledForCourse(courseId: number, accessData: any, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {
return true;
}
/**
* Check if the handler is enabled on a site level.
*
* @return {boolean | Promise<boolean>} Whether or not the handler is enabled on a site level.
*/
isEnabled(): boolean | Promise<boolean> {
return true;
}
/**
* Returns the data needed to render the handler.
*
* @return {CoreCourseOptionsMenuHandlerData} Data needed to render the handler.
*/
getMenuDisplayData(): CoreCourseOptionsMenuHandlerData {
return {
icon: 'cube',
title: 'addon.storagemanager.managestorage',
page: 'AddonStorageManagerCourseStoragePage',
class: 'addon-storagemanager-coursemenu-handler'
};
}
}

View File

@ -0,0 +1,34 @@
// (C) Copyright 2015 Martin Dougiamas
//
// 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 { AddonStorageManagerCourseMenuHandler } from '@addon/storagemanager/providers/coursemenu-handler';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
@NgModule({
declarations: [],
imports: [
],
providers: [
AddonStorageManagerCourseMenuHandler
],
exports: []
})
export class AddonStorageManagerModule {
constructor(private courseOptionsDelegate: CoreCourseOptionsDelegate,
private courseMenuHandler: AddonStorageManagerCourseMenuHandler) {
// Register handlers.
this.courseOptionsDelegate.registerHandler(this.courseMenuHandler);
}
}

View File

@ -126,6 +126,7 @@ import { AddonNotificationsModule } from '@addon/notifications/notifications.mod
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
import { AddonQtypeModule } from '@addon/qtype/qtype.module';
import { AddonStorageManagerModule } from '@addon/storagemanager/storagemanager.module';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
@ -244,7 +245,8 @@ export const CORE_PROVIDERS: any[] = [
AddonNotificationsModule,
AddonRemoteThemesModule,
AddonQbehaviourModule,
AddonQtypeModule
AddonQtypeModule,
AddonStorageManagerModule
],
bootstrap: [IonicApp],
entryComponents: [

View File

@ -881,6 +881,11 @@
"addon.notifications.notifications": "Notifications",
"addon.notifications.playsound": "Play sound",
"addon.notifications.therearentnotificationsyet": "There are no notifications.",
"addon.storagemanager.deletecourse": "Offload all course data",
"addon.storagemanager.deletedatafrom": "Offload data from {{name}}",
"addon.storagemanager.info": "Files stored on your device make the app work faster, and when offline. You can safely offload them if you need to free up storage space.",
"addon.storagemanager.managestorage": "Manage storage",
"addon.storagemanager.storageused": "File storage used:",
"assets.countries.AD": "Andorra",
"assets.countries.AE": "United Arab Emirates",
"assets.countries.AF": "Afghanistan",