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
parent
0daf5191ff
commit
cb4fbce618
|
@ -881,6 +881,11 @@
|
||||||
"addon.notifications.notifications": "local_moodlemobileapp",
|
"addon.notifications.notifications": "local_moodlemobileapp",
|
||||||
"addon.notifications.playsound": "local_moodlemobileapp",
|
"addon.notifications.playsound": "local_moodlemobileapp",
|
||||||
"addon.notifications.therearentnotificationsyet": "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.AD": "countries",
|
||||||
"assets.countries.AE": "countries",
|
"assets.countries.AE": "countries",
|
||||||
"assets.countries.AF": "countries",
|
"assets.countries.AF": "countries",
|
||||||
|
|
|
@ -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:"
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 {
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,6 +126,7 @@ import { AddonNotificationsModule } from '@addon/notifications/notifications.mod
|
||||||
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
import { AddonRemoteThemesModule } from '@addon/remotethemes/remotethemes.module';
|
||||||
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
|
import { AddonQbehaviourModule } from '@addon/qbehaviour/qbehaviour.module';
|
||||||
import { AddonQtypeModule } from '@addon/qtype/qtype.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.
|
// For translate loader. AoT requires an exported function for factories.
|
||||||
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
|
||||||
|
@ -244,7 +245,8 @@ export const CORE_PROVIDERS: any[] = [
|
||||||
AddonNotificationsModule,
|
AddonNotificationsModule,
|
||||||
AddonRemoteThemesModule,
|
AddonRemoteThemesModule,
|
||||||
AddonQbehaviourModule,
|
AddonQbehaviourModule,
|
||||||
AddonQtypeModule
|
AddonQtypeModule,
|
||||||
|
AddonStorageManagerModule
|
||||||
],
|
],
|
||||||
bootstrap: [IonicApp],
|
bootstrap: [IonicApp],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
|
|
|
@ -881,6 +881,11 @@
|
||||||
"addon.notifications.notifications": "Notifications",
|
"addon.notifications.notifications": "Notifications",
|
||||||
"addon.notifications.playsound": "Play sound",
|
"addon.notifications.playsound": "Play sound",
|
||||||
"addon.notifications.therearentnotificationsyet": "There are no notifications.",
|
"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.AD": "Andorra",
|
||||||
"assets.countries.AE": "United Arab Emirates",
|
"assets.countries.AE": "United Arab Emirates",
|
||||||
"assets.countries.AF": "Afghanistan",
|
"assets.countries.AF": "Afghanistan",
|
||||||
|
|
Loading…
Reference in New Issue